2025-06-23 13:52:35 +01:00
|
|
|
# typed: false # rubocop:todo Sorbet/TrueSigil
|
2025-03-18 17:38:37 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "json"
|
|
|
|
require "tsort"
|
|
|
|
|
|
|
|
module Homebrew
|
|
|
|
module Bundle
|
|
|
|
# TODO: refactor into multiple modules
|
|
|
|
module BrewDumper
|
|
|
|
module_function
|
|
|
|
|
|
|
|
def reset!
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brew_services"
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
Homebrew::Bundle::BrewServices.reset!
|
|
|
|
@formulae = nil
|
|
|
|
@formulae_by_full_name = nil
|
|
|
|
@formulae_by_name = nil
|
|
|
|
@formula_aliases = nil
|
|
|
|
@formula_oldnames = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def formulae
|
|
|
|
return @formulae if @formulae
|
|
|
|
|
|
|
|
formulae_by_full_name
|
|
|
|
@formulae
|
|
|
|
end
|
|
|
|
|
|
|
|
def formulae_by_full_name(name = nil)
|
|
|
|
return @formulae_by_full_name[name] if name.present? && @formulae_by_full_name&.key?(name)
|
|
|
|
|
|
|
|
require "formula"
|
|
|
|
require "formulary"
|
|
|
|
Formulary.enable_factory_cache!
|
|
|
|
|
|
|
|
@formulae_by_name ||= {}
|
|
|
|
@formulae_by_full_name ||= {}
|
|
|
|
|
|
|
|
if name.nil?
|
|
|
|
formulae = Formula.installed.map(&method(:add_formula))
|
|
|
|
sort!(formulae)
|
|
|
|
return @formulae_by_full_name
|
|
|
|
end
|
|
|
|
|
|
|
|
formula = Formula[name]
|
|
|
|
add_formula(formula)
|
|
|
|
rescue FormulaUnavailableError => e
|
|
|
|
opoo "'#{name}' formula is unreadable: #{e}"
|
|
|
|
{}
|
|
|
|
end
|
|
|
|
|
|
|
|
def formulae_by_name(name)
|
|
|
|
formulae_by_full_name(name) || @formulae_by_name[name]
|
|
|
|
end
|
|
|
|
|
|
|
|
def dump(describe: false, no_restart: false)
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brew_services"
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
requested_formula = formulae.select do |f|
|
|
|
|
f[:installed_on_request?] || !f[:installed_as_dependency?]
|
|
|
|
end
|
|
|
|
requested_formula.map do |f|
|
|
|
|
brewline = if describe && f[:desc].present?
|
|
|
|
f[:desc].split("\n").map { |s| "# #{s}\n" }.join
|
|
|
|
else
|
|
|
|
""
|
|
|
|
end
|
|
|
|
brewline += "brew \"#{f[:full_name]}\""
|
|
|
|
|
|
|
|
args = f[:args].map { |arg| "\"#{arg}\"" }.sort.join(", ")
|
|
|
|
brewline += ", args: [#{args}]" unless f[:args].empty?
|
|
|
|
brewline += ", restart_service: :changed" if !no_restart && BrewServices.started?(f[:full_name])
|
|
|
|
brewline += ", link: #{f[:link?]}" unless f[:link?].nil?
|
|
|
|
brewline
|
|
|
|
end.join("\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
def formula_aliases
|
|
|
|
return @formula_aliases if @formula_aliases
|
|
|
|
|
|
|
|
@formula_aliases = {}
|
|
|
|
formulae.each do |f|
|
|
|
|
aliases = f[:aliases]
|
|
|
|
next if aliases.blank?
|
|
|
|
|
|
|
|
aliases.each do |a|
|
|
|
|
@formula_aliases[a] = f[:full_name]
|
|
|
|
if f[:full_name].include? "/" # tap formula
|
|
|
|
tap_name = f[:full_name].rpartition("/").first
|
|
|
|
@formula_aliases["#{tap_name}/#{a}"] = f[:full_name]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@formula_aliases
|
|
|
|
end
|
|
|
|
|
|
|
|
def formula_oldnames
|
|
|
|
return @formula_oldnames if @formula_oldnames
|
|
|
|
|
|
|
|
@formula_oldnames = {}
|
|
|
|
formulae.each do |f|
|
|
|
|
oldnames = f[:oldnames]
|
|
|
|
next if oldnames.blank?
|
|
|
|
|
|
|
|
oldnames.each do |oldname|
|
|
|
|
@formula_oldnames[oldname] = f[:full_name]
|
|
|
|
if f[:full_name].include? "/" # tap formula
|
|
|
|
tap_name = f[:full_name].rpartition("/").first
|
|
|
|
@formula_oldnames["#{tap_name}/#{oldname}"] = f[:full_name]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@formula_oldnames
|
|
|
|
end
|
|
|
|
|
|
|
|
def add_formula(formula)
|
|
|
|
hash = formula_to_hash formula
|
|
|
|
|
|
|
|
@formulae_by_name[hash[:name]] = hash
|
|
|
|
@formulae_by_full_name[hash[:full_name]] = hash
|
|
|
|
|
|
|
|
hash
|
|
|
|
end
|
|
|
|
private_class_method :add_formula
|
|
|
|
|
|
|
|
def formula_to_hash(formula)
|
|
|
|
keg = if formula.linked?
|
|
|
|
link = true if formula.keg_only?
|
|
|
|
formula.linked_keg
|
|
|
|
else
|
|
|
|
link = false unless formula.keg_only?
|
|
|
|
formula.any_installed_prefix
|
|
|
|
end
|
|
|
|
|
|
|
|
if keg
|
|
|
|
require "tab"
|
|
|
|
|
|
|
|
tab = Tab.for_keg(keg)
|
|
|
|
args = tab.used_options.map(&:name)
|
|
|
|
version = begin
|
|
|
|
keg.realpath.basename
|
|
|
|
rescue
|
|
|
|
# silently handle broken symlinks
|
|
|
|
nil
|
|
|
|
end.to_s
|
|
|
|
args << "HEAD" if version.start_with?("HEAD")
|
|
|
|
installed_as_dependency = tab.installed_as_dependency
|
|
|
|
installed_on_request = tab.installed_on_request
|
|
|
|
runtime_dependencies = if (runtime_deps = tab.runtime_dependencies)
|
|
|
|
runtime_deps.filter_map { |d| d["full_name"] }
|
|
|
|
|
|
|
|
end
|
|
|
|
poured_from_bottle = tab.poured_from_bottle
|
|
|
|
end
|
|
|
|
|
|
|
|
runtime_dependencies ||= formula.runtime_dependencies.map(&:name)
|
|
|
|
|
|
|
|
bottled = if (stable = formula.stable) && stable.bottle_defined?
|
|
|
|
bottle_hash = formula.bottle_hash.deep_symbolize_keys
|
|
|
|
stable.bottled?
|
|
|
|
end
|
|
|
|
|
|
|
|
{
|
|
|
|
name: formula.name,
|
|
|
|
desc: formula.desc,
|
|
|
|
oldnames: formula.oldnames,
|
|
|
|
full_name: formula.full_name,
|
|
|
|
aliases: formula.aliases,
|
|
|
|
any_version_installed?: formula.any_version_installed?,
|
|
|
|
args: Array(args).uniq,
|
|
|
|
version:,
|
|
|
|
installed_as_dependency?: installed_as_dependency || false,
|
|
|
|
installed_on_request?: installed_on_request || false,
|
|
|
|
dependencies: runtime_dependencies,
|
|
|
|
build_dependencies: formula.deps.select(&:build?).map(&:name).uniq,
|
|
|
|
conflicts_with: formula.conflicts.map(&:name),
|
|
|
|
pinned?: formula.pinned? || false,
|
|
|
|
outdated?: formula.outdated? || false,
|
|
|
|
link?: link,
|
|
|
|
poured_from_bottle?: poured_from_bottle || false,
|
|
|
|
bottle: bottle_hash || false,
|
|
|
|
bottled: bottled || false,
|
|
|
|
official_tap: formula.tap&.official? || false,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
private_class_method :formula_to_hash
|
|
|
|
|
|
|
|
class Topo < Hash
|
|
|
|
include TSort
|
|
|
|
alias tsort_each_node each_key
|
|
|
|
def tsort_each_child(node, &block)
|
|
|
|
fetch(node.downcase).sort.each(&block)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def sort!(formulae)
|
|
|
|
# Step 1: Sort by formula full name while putting tap formulae behind core formulae.
|
|
|
|
# So we can have a nicer output.
|
|
|
|
formulae = formulae.sort do |a, b|
|
|
|
|
if a[:full_name].exclude?("/") && b[:full_name].include?("/")
|
|
|
|
-1
|
|
|
|
elsif a[:full_name].include?("/") && b[:full_name].exclude?("/")
|
|
|
|
1
|
|
|
|
else
|
|
|
|
a[:full_name] <=> b[:full_name]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Step 2: Sort by formula dependency topology.
|
|
|
|
topo = Topo.new
|
|
|
|
formulae.each do |f|
|
|
|
|
topo[f[:name]] = topo[f[:full_name]] = f[:dependencies].filter_map do |dep|
|
|
|
|
ff = formulae_by_name(dep)
|
|
|
|
next if ff.blank?
|
|
|
|
next unless ff[:any_version_installed?]
|
|
|
|
|
|
|
|
ff[:full_name]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@formulae = topo.tsort
|
|
|
|
.map { |name| @formulae_by_full_name[name] || @formulae_by_name[name] }
|
|
|
|
.uniq { |f| f[:full_name] }
|
|
|
|
rescue TSort::Cyclic => e
|
|
|
|
e.message =~ /\["([^"]*)".*"([^"]*)"\]/
|
|
|
|
cycle_first = Regexp.last_match(1)
|
|
|
|
cycle_last = Regexp.last_match(2)
|
|
|
|
odie e.message if !cycle_first || !cycle_last
|
|
|
|
|
|
|
|
odie <<~EOS
|
|
|
|
Formulae dependency graph sorting failed (likely due to a circular dependency):
|
2025-06-23 13:52:35 +01:00
|
|
|
#{cycle_first}: #{topo[cycle_first]}
|
|
|
|
#{cycle_last}: #{topo[cycle_last]}
|
2025-03-18 17:38:37 +00:00
|
|
|
Please run the following commands and try again:
|
|
|
|
brew update
|
|
|
|
brew uninstall --ignore-dependencies --force #{cycle_first} #{cycle_last}
|
|
|
|
brew install #{cycle_first} #{cycle_last}
|
|
|
|
EOS
|
|
|
|
end
|
|
|
|
private_class_method :sort!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|