245 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: false # rubocop:todo Sorbet/TrueSigil
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| require "json"
 | |
| require "tsort"
 | |
| 
 | |
| module Homebrew
 | |
|   module Bundle
 | |
|     # TODO: refactor into multiple modules
 | |
|     module BrewDumper
 | |
|       module_function
 | |
| 
 | |
|       def reset!
 | |
|         require "bundle/brew_services"
 | |
| 
 | |
|         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)
 | |
|         require "bundle/brew_services"
 | |
| 
 | |
|         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):
 | |
|           #{cycle_first}: #{topo[cycle_first]}
 | |
|           #{cycle_last}: #{topo[cycle_last]}
 | |
|           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
 | 
