505 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			505 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: strict
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| require "reinstall"
 | |
| require "formula_installer"
 | |
| require "development_tools"
 | |
| require "messages"
 | |
| require "cleanup"
 | |
| require "utils/topological_hash"
 | |
| 
 | |
| module Homebrew
 | |
|   # Helper functions for upgrading formulae.
 | |
|   module Upgrade
 | |
|     class Dependents < T::Struct
 | |
|       const :upgradeable, T::Array[Formula]
 | |
|       const :pinned, T::Array[Formula]
 | |
|       const :skipped, T::Array[Formula]
 | |
|     end
 | |
| 
 | |
|     class << self
 | |
|       sig {
 | |
|         params(
 | |
|           formulae_to_install: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean,
 | |
|           force_bottle: T::Boolean, build_from_source_formulae: T::Array[String],
 | |
|           dependents: T::Boolean, interactive: T::Boolean, keep_tmp: T::Boolean,
 | |
|           debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean,
 | |
|           debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean
 | |
|         ).returns(T::Array[FormulaInstaller])
 | |
|       }
 | |
|       def formula_installers(
 | |
|         formulae_to_install,
 | |
|         flags:,
 | |
|         dry_run: false,
 | |
|         force_bottle: false,
 | |
|         build_from_source_formulae: [],
 | |
|         dependents: false,
 | |
|         interactive: false,
 | |
|         keep_tmp: false,
 | |
|         debug_symbols: false,
 | |
|         force: false,
 | |
|         overwrite: false,
 | |
|         debug: false,
 | |
|         quiet: false,
 | |
|         verbose: false
 | |
|       )
 | |
|         return [] if formulae_to_install.empty?
 | |
| 
 | |
|         # Sort keg-only before non-keg-only formulae to avoid any needless conflicts
 | |
|         # with outdated, non-keg-only versions of formulae being upgraded.
 | |
|         formulae_to_install.sort! do |a, b|
 | |
|           if !a.keg_only? && b.keg_only?
 | |
|             1
 | |
|           elsif a.keg_only? && !b.keg_only?
 | |
|             -1
 | |
|           else
 | |
|             0
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install)
 | |
|         begin
 | |
|           formulae_to_install = dependency_graph.tsort & formulae_to_install
 | |
|         rescue TSort::Cyclic
 | |
|           if Homebrew::EnvConfig.developer?
 | |
|             raise CyclicDependencyError, dependency_graph.strongly_connected_components
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         formulae_to_install.filter_map do |formula|
 | |
|           Migrator.migrate_if_needed(formula, force:, dry_run:)
 | |
|           begin
 | |
|             fi = create_formula_installer(
 | |
|               formula,
 | |
|               flags:,
 | |
|               force_bottle:,
 | |
|               build_from_source_formulae:,
 | |
|               interactive:,
 | |
|               keep_tmp:,
 | |
|               debug_symbols:,
 | |
|               force:,
 | |
|               overwrite:,
 | |
|               debug:,
 | |
|               quiet:,
 | |
|               verbose:,
 | |
|             )
 | |
|             fi.fetch_bottle_tab(quiet: !debug)
 | |
| 
 | |
|             all_runtime_deps_installed = fi.bottle_tab_runtime_dependencies.presence&.all? do |dependency, hash|
 | |
|               minimum_version = if (version = hash["version"])
 | |
|                 Version.new(version)
 | |
|               end
 | |
|               Dependency.new(dependency).installed?(minimum_version:, minimum_revision: hash["revision"].to_i)
 | |
|             end
 | |
| 
 | |
|             if !dry_run && dependents && all_runtime_deps_installed
 | |
|               # Don't need to install this bottle if all of the runtime
 | |
|               # dependencies have the same or newer version already installed.
 | |
|               next
 | |
|             end
 | |
| 
 | |
|             fi
 | |
|           rescue CannotInstallFormulaError => e
 | |
|             ofail e
 | |
|             nil
 | |
|           rescue UnsatisfiedRequirements, DownloadError => e
 | |
|             ofail "#{formula}: #{e}"
 | |
|             nil
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       sig { params(formula_installers: T::Array[FormulaInstaller], dry_run: T::Boolean, verbose: T::Boolean).void }
 | |
|       def upgrade_formulae(formula_installers, dry_run: false, verbose: false)
 | |
|         valid_formula_installers = if dry_run
 | |
|           formula_installers
 | |
|         else
 | |
|           Install.fetch_formulae(formula_installers)
 | |
|         end
 | |
| 
 | |
|         valid_formula_installers.each do |fi|
 | |
|           upgrade_formula(fi, dry_run:, verbose:)
 | |
|           Cleanup.install_formula_clean!(fi.formula, dry_run:)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       sig { params(formula: Formula).returns(T::Array[Keg]) }
 | |
|       def outdated_kegs(formula)
 | |
|         [formula, *formula.old_installed_formulae].map(&:linked_keg)
 | |
|                                                   .select(&:directory?)
 | |
|                                                   .map { |k| Keg.new(k.resolved_path) }
 | |
|       end
 | |
| 
 | |
|       sig { params(formula: Formula, fi_options: Options).void }
 | |
|       def print_upgrade_message(formula, fi_options)
 | |
|         version_upgrade = if formula.optlinked?
 | |
|           "#{Keg.new(formula.opt_prefix).version} -> #{formula.pkg_version}"
 | |
|         else
 | |
|           "-> #{formula.pkg_version}"
 | |
|         end
 | |
|         oh1 "Upgrading #{Formatter.identifier(formula.full_specified_name)}"
 | |
|         puts "  #{version_upgrade} #{fi_options.to_a.join(" ")}"
 | |
|       end
 | |
| 
 | |
|       sig {
 | |
|         params(
 | |
|           formulae: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean,
 | |
|           ask: T::Boolean, installed_on_request: T::Boolean, force_bottle: T::Boolean,
 | |
|           build_from_source_formulae: T::Array[String], interactive: T::Boolean,
 | |
|           keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean,
 | |
|           debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean
 | |
|         ).returns(Dependents)
 | |
|       }
 | |
|       def dependants(
 | |
|         formulae,
 | |
|         flags:,
 | |
|         dry_run: false,
 | |
|         ask: false,
 | |
|         installed_on_request: false,
 | |
|         force_bottle: false,
 | |
|         build_from_source_formulae: [],
 | |
|         interactive: false,
 | |
|         keep_tmp: false,
 | |
|         debug_symbols: false,
 | |
|         force: false,
 | |
|         debug: false,
 | |
|         quiet: false,
 | |
|         verbose: false
 | |
|       )
 | |
|         no_dependents = Dependents.new(upgradeable: [], pinned: [], skipped: [])
 | |
|         if Homebrew::EnvConfig.no_installed_dependents_check?
 | |
|           unless Homebrew::EnvConfig.no_env_hints?
 | |
|             opoo <<~EOS
 | |
|               `$HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set: not checking for outdated
 | |
|               dependents or dependents with broken linkage!
 | |
|             EOS
 | |
|           end
 | |
|           return no_dependents
 | |
|         end
 | |
|         formulae_to_install = formulae.reject { |f| f.core_formula? && f.versioned_formula? }
 | |
|         return no_dependents if formulae_to_install.empty?
 | |
| 
 | |
|         already_broken = check_broken_dependents(formulae_to_install)
 | |
| 
 | |
|         # TODO: this should be refactored to use FormulaInstaller new logic
 | |
|         outdated = formulae_to_install.flat_map(&:runtime_installed_formula_dependents)
 | |
|                                       .uniq
 | |
|                                       .select(&:outdated?)
 | |
| 
 | |
|         # Ensure we never attempt a source build for outdated dependents of upgraded formulae.
 | |
|         outdated, skipped = outdated.partition do |dependent|
 | |
|           dependent.bottled? && dependent.deps.map(&:to_formula).all?(&:bottled?)
 | |
|         end
 | |
|         return no_dependents if outdated.blank? && already_broken.blank?
 | |
| 
 | |
|         outdated -= formulae_to_install if dry_run
 | |
|         upgradeable = outdated.reject(&:pinned?)
 | |
|                               .sort { |a, b| depends_on(a, b) }
 | |
|         pinned = outdated.select(&:pinned?)
 | |
|                          .sort { |a, b| depends_on(a, b) }
 | |
| 
 | |
|         Dependents.new(upgradeable:, pinned:, skipped:)
 | |
|       end
 | |
| 
 | |
|       sig {
 | |
|         params(deps: Dependents, formulae: T::Array[Formula], flags: T::Array[String],
 | |
|                dry_run: T::Boolean, installed_on_request: T::Boolean, force_bottle: T::Boolean,
 | |
|                build_from_source_formulae: T::Array[String], interactive: T::Boolean, keep_tmp: T::Boolean,
 | |
|                debug_symbols: T::Boolean, force: T::Boolean, debug: T::Boolean, quiet: T::Boolean,
 | |
|                verbose: T::Boolean).void
 | |
|       }
 | |
|       def upgrade_dependents(deps, formulae,
 | |
|                              flags:,
 | |
|                              dry_run: false,
 | |
|                              installed_on_request: false,
 | |
|                              force_bottle: false,
 | |
|                              build_from_source_formulae: [],
 | |
|                              interactive: false,
 | |
|                              keep_tmp: false,
 | |
|                              debug_symbols: false,
 | |
|                              force: false,
 | |
|                              debug: false,
 | |
|                              quiet: false,
 | |
|                              verbose: false)
 | |
|         return if deps.blank?
 | |
| 
 | |
|         upgradeable = deps.upgradeable
 | |
|         pinned      = deps.pinned
 | |
|         skipped     = deps.skipped
 | |
|         if pinned.present?
 | |
|           plural = Utils.pluralize("dependent", pinned.count)
 | |
|           opoo "Not upgrading #{pinned.count} pinned #{plural}:"
 | |
|           puts(pinned.map do |f|
 | |
|             "#{f.full_specified_name} #{f.pkg_version}"
 | |
|           end.join(", "))
 | |
|         end
 | |
|         if skipped.present?
 | |
|           opoo <<~EOS
 | |
|             The following dependents of upgraded formulae are outdated but will not
 | |
|             be upgraded because they are not bottled:
 | |
|               #{skipped * "\n  "}
 | |
|           EOS
 | |
|         end
 | |
| 
 | |
|         upgradeable.reject! { |f| FormulaInstaller.installed.include?(f) }
 | |
| 
 | |
|         # Print the upgradable dependents.
 | |
|         if upgradeable.blank?
 | |
|           ohai "No outdated dependents to upgrade!" unless dry_run
 | |
|         else
 | |
|           installed_formulae = (dry_run ? formulae : FormulaInstaller.installed.to_a).dup
 | |
|           formula_plural = Utils.pluralize("formula", installed_formulae.count, plural: "e")
 | |
|           upgrade_verb = dry_run ? "Would upgrade" : "Upgrading"
 | |
|           ohai "#{upgrade_verb} #{Utils.pluralize("dependent", upgradeable.count,
 | |
|                                                   include_count: true)} of upgraded #{formula_plural}:"
 | |
|           puts_no_installed_dependents_check_disable_message_if_not_already!
 | |
|           formulae_upgrades = upgradeable.map do |f|
 | |
|             name = f.full_specified_name
 | |
|             if f.optlinked?
 | |
|               "#{name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
 | |
|             else
 | |
|               "#{name} #{f.pkg_version}"
 | |
|             end
 | |
|           end
 | |
|           puts formulae_upgrades.join(", ")
 | |
|         end
 | |
| 
 | |
|         return if upgradeable.blank?
 | |
| 
 | |
|         unless dry_run
 | |
|           dependent_installers = formula_installers(
 | |
|             upgradeable,
 | |
|             flags:,
 | |
|             force_bottle:,
 | |
|             build_from_source_formulae:,
 | |
|             dependents:                 true,
 | |
|             interactive:,
 | |
|             keep_tmp:,
 | |
|             debug_symbols:,
 | |
|             force:,
 | |
|             debug:,
 | |
|             quiet:,
 | |
|             verbose:,
 | |
|           )
 | |
|           upgrade_formulae(dependent_installers, dry_run:, verbose:)
 | |
|         end
 | |
| 
 | |
|         # Update installed formulae after upgrading
 | |
|         installed_formulae = FormulaInstaller.installed.to_a
 | |
| 
 | |
|         # Assess the dependents tree again now we've upgraded.
 | |
|         unless dry_run
 | |
|           oh1 "Checking for dependents of upgraded formulae..."
 | |
|           puts_no_installed_dependents_check_disable_message_if_not_already!
 | |
|         end
 | |
| 
 | |
|         broken_dependents = check_broken_dependents(installed_formulae)
 | |
|         if broken_dependents.blank?
 | |
|           if dry_run
 | |
|             ohai "No currently broken dependents found!"
 | |
|             opoo "If they are broken by the upgrade they will also be upgraded or reinstalled."
 | |
|           else
 | |
|             ohai "No broken dependents found!"
 | |
|           end
 | |
|           return
 | |
|         end
 | |
| 
 | |
|         reinstallable_broken_dependents =
 | |
|           broken_dependents.reject(&:outdated?)
 | |
|                            .reject(&:pinned?)
 | |
|                            .sort { |a, b| depends_on(a, b) }
 | |
|         outdated_pinned_broken_dependents =
 | |
|           broken_dependents.select(&:outdated?)
 | |
|                            .select(&:pinned?)
 | |
|                            .sort { |a, b| depends_on(a, b) }
 | |
| 
 | |
|         # Print the pinned dependents.
 | |
|         if outdated_pinned_broken_dependents.present?
 | |
|           count = outdated_pinned_broken_dependents.count
 | |
|           plural = Utils.pluralize("dependent", outdated_pinned_broken_dependents.count)
 | |
|           onoe "Not reinstalling #{count} broken and outdated, but pinned #{plural}:"
 | |
|           $stderr.puts(outdated_pinned_broken_dependents.map do |f|
 | |
|             "#{f.full_specified_name} #{f.pkg_version}"
 | |
|           end.join(", "))
 | |
|         end
 | |
| 
 | |
|         # Print the broken dependents.
 | |
|         if reinstallable_broken_dependents.blank?
 | |
|           ohai "No broken dependents to reinstall!"
 | |
|         else
 | |
|           ohai "Reinstalling #{Utils.pluralize("dependent", reinstallable_broken_dependents.count,
 | |
|                                                include_count: true)} with broken linkage from source:"
 | |
|           puts_no_installed_dependents_check_disable_message_if_not_already!
 | |
|           puts reinstallable_broken_dependents.map(&:full_specified_name)
 | |
|                                               .join(", ")
 | |
|         end
 | |
| 
 | |
|         return if dry_run
 | |
| 
 | |
|         reinstall_contexts = reinstallable_broken_dependents.map do |formula|
 | |
|           Reinstall.build_install_context(
 | |
|             formula,
 | |
|             flags:,
 | |
|             force_bottle:,
 | |
|             build_from_source_formulae: build_from_source_formulae + [formula.full_name],
 | |
|             interactive:,
 | |
|             keep_tmp:,
 | |
|             debug_symbols:,
 | |
|             force:,
 | |
|             debug:,
 | |
|             quiet:,
 | |
|             verbose:,
 | |
|           )
 | |
|         end
 | |
| 
 | |
|         valid_formula_installers = Install.fetch_formulae(reinstall_contexts.map(&:formula_installer))
 | |
| 
 | |
|         reinstall_contexts.each do |reinstall_context|
 | |
|           next unless valid_formula_installers.include?(reinstall_context.formula_installer)
 | |
| 
 | |
|           Reinstall.reinstall_formula(reinstall_context)
 | |
|         rescue FormulaInstallationAlreadyAttemptedError
 | |
|           # We already attempted to reinstall f as part of the dependency tree of
 | |
|           # another formula. In that case, don't generate an error, just move on.
 | |
|           nil
 | |
|         rescue CannotInstallFormulaError, DownloadError => e
 | |
|           ofail e
 | |
|         rescue BuildError => e
 | |
|           e.dump(verbose:)
 | |
|           puts
 | |
|           Homebrew.failed = true
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       private
 | |
| 
 | |
|       sig { params(formula_installer: FormulaInstaller, dry_run: T::Boolean, verbose: T::Boolean).void }
 | |
|       def upgrade_formula(formula_installer, dry_run: false, verbose: false)
 | |
|         formula = formula_installer.formula
 | |
| 
 | |
|         if dry_run
 | |
|           Install.print_dry_run_dependencies(formula, formula_installer.compute_dependencies) do |f|
 | |
|             name = f.full_specified_name
 | |
|             if f.optlinked?
 | |
|               "#{name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
 | |
|             else
 | |
|               "#{name} #{f.pkg_version}"
 | |
|             end
 | |
|           end
 | |
|           return
 | |
|         end
 | |
| 
 | |
|         Install.install_formula(formula_installer, upgrade: true)
 | |
|       rescue BuildError => e
 | |
|         e.dump(verbose:)
 | |
|         puts
 | |
|         Homebrew.failed = true
 | |
|       end
 | |
| 
 | |
|       sig { params(installed_formulae: T::Array[Formula]).returns(T::Array[Formula]) }
 | |
|       def check_broken_dependents(installed_formulae)
 | |
|         CacheStoreDatabase.use(:linkage) do |db|
 | |
|           installed_formulae.flat_map(&:runtime_installed_formula_dependents)
 | |
|                             .uniq
 | |
|                             .select do |f|
 | |
|             keg = f.any_installed_keg
 | |
|             next unless keg
 | |
|             next unless keg.directory?
 | |
| 
 | |
|             LinkageChecker.new(keg, cache_db: db)
 | |
|                           .broken_library_linkage?
 | |
|           end.compact
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       sig { void }
 | |
|       def puts_no_installed_dependents_check_disable_message_if_not_already!
 | |
|         return if Homebrew::EnvConfig.no_env_hints?
 | |
|         return if Homebrew::EnvConfig.no_installed_dependents_check?
 | |
|         return if @puts_no_installed_dependents_check_disable_message_if_not_already
 | |
| 
 | |
|         puts "Disable this behaviour by setting `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1`."
 | |
|         puts "Hide these hints with `HOMEBREW_NO_ENV_HINTS=1` (see `man brew`)."
 | |
|         @puts_no_installed_dependents_check_disable_message_if_not_already = T.let(true, T.nilable(T::Boolean))
 | |
|       end
 | |
| 
 | |
|       sig {
 | |
|         params(formula: Formula, flags: T::Array[String], force_bottle: T::Boolean,
 | |
|                build_from_source_formulae: T::Array[String], interactive: T::Boolean,
 | |
|                keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean,
 | |
|                overwrite: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean).returns(FormulaInstaller)
 | |
|       }
 | |
|       def create_formula_installer(
 | |
|         formula,
 | |
|         flags:,
 | |
|         force_bottle: false,
 | |
|         build_from_source_formulae: [],
 | |
|         interactive: false,
 | |
|         keep_tmp: false,
 | |
|         debug_symbols: false,
 | |
|         force: false,
 | |
|         overwrite: false,
 | |
|         debug: false,
 | |
|         quiet: false,
 | |
|         verbose: false
 | |
|       )
 | |
|         keg = if formula.optlinked?
 | |
|           Keg.new(formula.opt_prefix.resolved_path)
 | |
|         else
 | |
|           formula.installed_kegs.find(&:optlinked?)
 | |
|         end
 | |
| 
 | |
|         if keg
 | |
|           tab = keg.tab
 | |
|           link_keg = keg.linked?
 | |
|           installed_as_dependency = tab.installed_as_dependency == true
 | |
|           installed_on_request = tab.installed_on_request == true
 | |
|           build_bottle = tab.built_bottle?
 | |
|         else
 | |
|           link_keg = nil
 | |
|           installed_as_dependency = false
 | |
|           installed_on_request = true
 | |
|           build_bottle = false
 | |
|         end
 | |
| 
 | |
|         build_options = BuildOptions.new(Options.create(flags), formula.options)
 | |
|         options = build_options.used_options
 | |
|         options |= formula.build.used_options
 | |
|         options &= formula.options
 | |
| 
 | |
|         FormulaInstaller.new(
 | |
|           formula,
 | |
|           **{
 | |
|             options:,
 | |
|             link_keg:,
 | |
|             installed_as_dependency:,
 | |
|             installed_on_request:,
 | |
|             build_bottle:,
 | |
|             force_bottle:,
 | |
|             build_from_source_formulae:,
 | |
|             interactive:,
 | |
|             keep_tmp:,
 | |
|             debug_symbols:,
 | |
|             force:,
 | |
|             overwrite:,
 | |
|             debug:,
 | |
|             quiet:,
 | |
|             verbose:,
 | |
|           }.compact,
 | |
|         )
 | |
|       end
 | |
| 
 | |
|       sig { params(one: Formula, two: Formula).returns(Integer) }
 | |
|       def depends_on(one, two)
 | |
|         if one.any_installed_keg
 | |
|               &.runtime_dependencies
 | |
|               &.any? { |dependency| dependency["full_name"] == two.full_name }
 | |
|           1
 | |
|         else
 | |
|           T.must(one <=> two)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 | 
