# typed: strict # frozen_string_literal: true require "reinstall" require "formula_installer" require "development_tools" require "messages" require "cleanup" require "utils/topological_hash" require "utils/output" module Homebrew # Helper functions for upgrading formulae. module Upgrade extend Utils::Output::Mixin 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