From e10d4c43c28d2dfaef8d565d34c70296a949e6f5 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Thu, 24 Jul 2025 09:42:11 +0100 Subject: [PATCH] Optionally use DownloadQueue for reinstall, upgrade. Follow up on `DownloadQueue` for download concurrency on `brew fetch` and `brew install` to also add support for `brew reinstall` and `brew upgrade`. This required a fair bit of refactoring to make this work so I've also made `install.rb`, `reinstall.rb` and `upgrade.rb` `typed: strict` to add some extra guardrails from Sorbet here. Co-authored-by: Carlo Cabrera --- Library/Homebrew/cask/audit.rb | 6 +- Library/Homebrew/cask/installer.rb | 24 +- Library/Homebrew/cmd/reinstall.rb | 29 +- Library/Homebrew/extend/os/linux/install.rb | 7 +- Library/Homebrew/formula_installer.rb | 12 +- Library/Homebrew/install.rb | 169 +++- Library/Homebrew/reinstall.rb | 261 ++--- Library/Homebrew/test/cask/info_spec.rb | 1 + .../spec/shared_context/integration_test.rb | 1 + Library/Homebrew/upgrade.rb | 932 +++++++++--------- 10 files changed, 775 insertions(+), 667 deletions(-) diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index 3d84afe543..6696b120c6 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -585,13 +585,15 @@ module Cask }.compact Homebrew::Install.perform_preinstall_checks_once + valid_formula_installers = Homebrew::Install.fetch_formulae(primary_container.dependencies) + primary_container.dependencies.each do |dep| + next unless valid_formula_installers.include?(dep) + fi = FormulaInstaller.new( dep, **install_options, ) - fi.prelude - fi.fetch fi.install fi.finish end diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 689ba95a0b..b985dd56ac 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -426,6 +426,9 @@ on_request: true) end ohai "Installing dependencies: #{missing_formulae_and_casks.map(&:to_s).join(", ")}" + cask_installers = T.let([], T::Array[Installer]) + formula_installers = T.let([], T::Array[FormulaInstaller]) + missing_formulae_and_casks.each do |cask_or_formula| if cask_or_formula.is_a?(Cask) if skip_cask_deps? @@ -433,7 +436,7 @@ on_request: true) next end - Installer.new( + cask_installers << Installer.new( cask_or_formula, adopt: adopt?, binaries: binaries?, @@ -444,10 +447,9 @@ on_request: true) quiet: quiet?, require_sha: require_sha?, verbose: verbose?, - ).install + ) else - Homebrew::Install.perform_preinstall_checks_once - fi = FormulaInstaller.new( + formula_installers << FormulaInstaller.new( cask_or_formula, **{ show_header: true, @@ -456,12 +458,18 @@ on_request: true) verbose: verbose?, }.compact, ) - fi.prelude - fi.fetch - fi.install - fi.finish end end + + cask_installers.each(&:install) + return if formula_installers.blank? + + Homebrew::Install.perform_preinstall_checks_once + valid_formula_installers = Homebrew::Install.fetch_formulae(formula_installers) + valid_formula_installers.each do |formula_installer| + formula_installer.install + formula_installer.finish + end end def caveats diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index e0dd7182dc..53185b637d 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -130,7 +130,7 @@ module Homebrew unless formulae.empty? Install.perform_preinstall_checks_once - install_context = formulae.map do |formula| + reinstall_contexts = formulae.filter_map do |formula| if formula.pinned? onoe "#{formula.full_name} is pinned. You must unpin it to reinstall." next @@ -167,27 +167,18 @@ module Homebrew verbose: args.verbose?, ) - formulae_installer = install_context.map(&:formula_installer) + formulae_installers = reinstall_contexts.map(&:formula_installer) # Main block: if asking the user is enabled, show dependency and size information. - Install.ask_formulae(formulae_installer, dependants, args: args) if args.ask? + Install.ask_formulae(formulae_installers, dependants, args: args) if args.ask? - install_context.each do |f| - Homebrew::Reinstall.reinstall_formula( - f, - flags: args.flags_only, - force_bottle: args.force_bottle?, - build_from_source_formulae: args.build_from_source_formulae, - interactive: args.interactive?, - keep_tmp: args.keep_tmp?, - debug_symbols: args.debug_symbols?, - force: args.force?, - debug: args.debug?, - quiet: args.quiet?, - verbose: args.verbose?, - git: args.git?, - ) - Cleanup.install_formula_clean!(f.formula) + valid_formula_installers = Install.fetch_formulae(formulae_installers) + + reinstall_contexts.each do |reinstall_context| + next unless valid_formula_installers.include?(reinstall_context.formula_installer) + + Homebrew::Reinstall.reinstall_formula(reinstall_context) + Cleanup.install_formula_clean!(reinstall_context.formula) end Upgrade.upgrade_dependents( diff --git a/Library/Homebrew/extend/os/linux/install.rb b/Library/Homebrew/extend/os/linux/install.rb index d035a2f547..2420531048 100644 --- a/Library/Homebrew/extend/os/linux/install.rb +++ b/Library/Homebrew/extend/os/linux/install.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true module OS @@ -30,18 +30,21 @@ module OS libstdc++.so.6 ].freeze + sig { params(all_fatal: T::Boolean).void } def perform_preinstall_checks(all_fatal: false) super symlink_ld_so setup_preferred_gcc_libs end + sig { void } def global_post_install super symlink_ld_so setup_preferred_gcc_libs end + sig { void } def check_cpu return if ::Hardware::CPU.intel? && ::Hardware::CPU.is_64_bit? return if ::Hardware::CPU.arm? @@ -56,6 +59,7 @@ module OS ::Kernel.abort message end + sig { void } def symlink_ld_so brew_ld_so = HOMEBREW_PREFIX/"lib/ld.so" @@ -75,6 +79,7 @@ module OS FileUtils.ln_sf ld_so, brew_ld_so end + sig { void } def setup_preferred_gcc_libs gcc_opt_prefix = HOMEBREW_PREFIX/"opt/#{OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA}" glibc_installed = (HOMEBREW_PREFIX/"opt/glibc/bin/ld.so").readable? diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index b1f7d3db99..5d2bde7bf0 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -13,7 +13,6 @@ require "sandbox" require "development_tools" require "cache_store" require "linkage_checker" -require "install" require "messages" require "cask/cask_loader" require "cmd/install" @@ -61,8 +60,8 @@ class FormulaInstaller bottle_arch: T.nilable(String), ignore_deps: T::Boolean, only_deps: T::Boolean, - include_test_formulae: T::Array[Formula], - build_from_source_formulae: T::Array[Formula], + include_test_formulae: T::Array[String], + build_from_source_formulae: T::Array[String], env: T.nilable(String), git: T::Boolean, interactive: T::Boolean, @@ -507,7 +506,10 @@ class FormulaInstaller lock start_time = Time.now - Homebrew::Install.perform_build_from_source_checks if !pour_bottle? && DevelopmentTools.installed? + if !pour_bottle? && DevelopmentTools.installed? + require "install" + Homebrew::Install.perform_build_from_source_checks + end # Warn if a more recent version of this formula is available in the tap. begin @@ -896,6 +898,7 @@ on_request: installed_on_request?, options:) verbose: verbose?, ) oh1 "Installing #{formula.full_name} dependency: #{Formatter.identifier(dep.name)}" + # prelude only needed to populate bottle_tab_runtime_dependencies, fetching has already been done. fi.prelude fi.install fi.finish @@ -955,6 +958,7 @@ on_request: installed_on_request?, options:) fix_dynamic_linkage(keg) if !@poured_bottle || !formula.bottle_specification.skip_relocation? + require "install" Homebrew::Install.global_post_install if build_bottle? || skip_post_install? diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index 54d6c8e335..fc20c85800 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -21,6 +21,7 @@ module Homebrew end end + sig { params(cc: T.nilable(String)).void } def check_cc_argv(cc) return unless cc @@ -32,13 +33,16 @@ module Homebrew EOS end + sig { params(all_fatal: T::Boolean).void } def perform_build_from_source_checks(all_fatal: false) Diagnostic.checks(:fatal_build_from_source_checks) Diagnostic.checks(:build_from_source_checks, fatal: all_fatal) end + sig { void } def global_post_install; end + sig { void } def check_prefix if (Hardware::CPU.intel? || Hardware::CPU.in_rosetta2?) && HOMEBREW_PREFIX.to_s == HOMEBREW_MACOS_ARM_DEFAULT_PREFIX @@ -64,6 +68,11 @@ module Homebrew end end + sig { + params(formula: Formula, head: T::Boolean, fetch_head: T::Boolean, + only_dependencies: T::Boolean, force: T::Boolean, quiet: T::Boolean, + skip_link: T::Boolean, overwrite: T::Boolean).returns(T::Boolean) + } def install_formula?( formula, head: false, @@ -232,6 +241,16 @@ module Homebrew false end + sig { + params(formulae_to_install: T::Array[Formula], installed_on_request: T::Boolean, + installed_as_dependency: T::Boolean, build_bottle: T::Boolean, force_bottle: T::Boolean, + bottle_arch: T.nilable(String), ignore_deps: T::Boolean, only_deps: T::Boolean, + include_test_formulae: T::Array[String], build_from_source_formulae: T::Array[String], + cc: T.nilable(String), git: 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, dry_run: T::Boolean, skip_post_install: T::Boolean, + skip_link: T::Boolean).returns(T::Array[FormulaInstaller]) + } def formula_installers( formulae_to_install, installed_on_request: true, @@ -289,6 +308,53 @@ module Homebrew end end + sig { params(formula_installers: T::Array[FormulaInstaller]).returns(T::Array[FormulaInstaller]) } + def fetch_formulae(formula_installers) + formulae_names_to_install = formula_installers.map { |fi| fi.formula.name } + return formula_installers if formulae_names_to_install.empty? + + formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence + oh1 "Fetching downloads for: #{formula_sentence}", truncate: false + if EnvConfig.download_concurrency > 1 + download_queue = Homebrew::DownloadQueue.new(pour: true) + formula_installers.each do |fi| + fi.download_queue = download_queue + end + end + + valid_formula_installers = formula_installers.dup + + begin + [:prelude_fetch, :prelude, :fetch].each do |step| + valid_formula_installers.select! do |fi| + fi.public_send(step) + true + rescue CannotInstallFormulaError => e + ofail e.message + false + rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e + ofail "#{fi.formula}: #{e}" + false + end + download_queue&.fetch + end + ensure + download_queue&.shutdown + end + + valid_formula_installers + end + + sig { + params(formula_installers: T::Array[FormulaInstaller], installed_on_request: T::Boolean, + installed_as_dependency: T::Boolean, build_bottle: T::Boolean, force_bottle: T::Boolean, + bottle_arch: T.nilable(String), ignore_deps: T::Boolean, only_deps: T::Boolean, + include_test_formulae: T::Array[String], build_from_source_formulae: T::Array[String], + cc: T.nilable(String), git: 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, dry_run: T::Boolean, + skip_post_install: T::Boolean, skip_link: T::Boolean).void + } def install_formulae( formula_installers, installed_on_request: true, @@ -328,41 +394,17 @@ module Homebrew return end - formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence - oh1 "Fetching downloads for: #{formula_sentence}", truncate: false - if EnvConfig.download_concurrency > 1 - download_queue = Homebrew::DownloadQueue.new(pour: true) - formula_installers.each do |fi| - fi.download_queue = download_queue - end - end - - valid_formula_installers = formula_installers.dup - - begin - [:prelude_fetch, :prelude, :fetch].each do |step| - valid_formula_installers.select! do |fi| - fi.public_send(step) - true - rescue CannotInstallFormulaError => e - ofail e.message - false - rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e - ofail "#{fi.formula}: #{e}" - false - end - download_queue&.fetch - end - ensure - download_queue&.shutdown - end + valid_formula_installers = fetch_formulae(formula_installers) valid_formula_installers.each do |fi| - install_formula(fi) - Cleanup.install_formula_clean!(fi.formula) + formula = fi.formula + upgrade = formula.linked? && formula.outdated? && !formula.head? && !Homebrew::EnvConfig.no_install_upgrade? + install_formula(fi, upgrade:) + Cleanup.install_formula_clean!(formula) end end + sig { params(formula: Formula, dependencies: T::Array[[Dependency, Options]]).void } def print_dry_run_dependencies(formula, dependencies) return if dependencies.empty? @@ -373,6 +415,7 @@ module Homebrew end # If asking the user is enabled, show dependency and size information. + sig { params(formulae_installer: T::Array[FormulaInstaller], dependants: Homebrew::Upgrade::Dependents, args: Homebrew::CLI::Args).void } def ask_formulae(formulae_installer, dependants, args:) return if formulae_installer.empty? @@ -391,6 +434,7 @@ module Homebrew ask_input end + sig { params(casks: T::Array[Cask::Cask]).void } def ask_casks(casks) return if casks.empty? @@ -400,8 +444,51 @@ module Homebrew ask_input end + sig { params(formula_installer: FormulaInstaller, upgrade: T::Boolean).void } + def install_formula(formula_installer, upgrade:) + formula = formula_installer.formula + + formula_installer.check_installation_already_attempted + + if upgrade + Upgrade.print_upgrade_message(formula, formula_installer.options) + + kegs = Upgrade.outdated_kegs(formula) + linked_kegs = kegs.select(&:linked?) + else + formula.print_tap_action + end + + # first we unlink the currently active keg for this formula otherwise it is + # possible for the existing build to interfere with the build we are about to + # do! Seriously, it happens! + kegs.each(&:unlink) if kegs.present? + + formula_installer.install + formula_installer.finish + rescue FormulaInstallationAlreadyAttemptedError + # We already attempted to upgrade f as part of the dependency tree of + # another formula. In that case, don't generate an error, just move on. + nil + ensure + # restore previous installation state if build failed + begin + linked_kegs&.each(&:link) unless formula&.latest_version_installed? + rescue + nil + end + end + private + 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(all_fatal: T::Boolean).void } def perform_preinstall_checks(all_fatal: false) check_prefix check_cpu @@ -410,6 +497,7 @@ module Homebrew Diagnostic.checks(:fatal_preinstall_checks) end + sig { void } def attempt_directory_creation Keg.must_exist_directories.each do |dir| FileUtils.mkdir_p(dir) unless dir.exist? @@ -418,6 +506,7 @@ module Homebrew end end + sig { void } def check_cpu return unless Hardware::CPU.ppc? @@ -428,14 +517,7 @@ module Homebrew EOS end - def install_formula(formula_installer) - formula = formula_installer.formula - - upgrade = formula.linked? && formula.outdated? && !formula.head? && !Homebrew::EnvConfig.no_install_upgrade? - - Upgrade.install_formula(formula_installer, upgrade:) - end - + sig { void } def ask_input ohai "Do you want to proceed with the installation? [Y/y/yes/N/n/no]" accepted_inputs = %w[y yes] @@ -456,13 +538,16 @@ module Homebrew end # Compute the total sizes (download, installed, and net) for the given formulae. + sig { params(sized_formulae: T::Array[Formula], debug: T::Boolean).returns(T::Hash[Symbol, Integer]) } def compute_total_sizes(sized_formulae, debug: false) total_download_size = 0 total_installed_size = 0 total_net_size = 0 - sized_formulae.select(&:bottle).each do |formula| + sized_formulae.each do |formula| bottle = formula.bottle + next unless bottle + # Fetch additional bottle metadata (if necessary). bottle.fetch_tab(quiet: !debug) @@ -481,11 +566,15 @@ module Homebrew net: total_net_size } end + sig { + params(formulae_installer: T::Array[FormulaInstaller], + dependants: Homebrew::Upgrade::Dependents).returns(T::Array[Formula]) + } def collect_dependencies(formulae_installer, dependants) formulae_dependencies = formulae_installer.flat_map do |f| [f.formula, f.compute_dependencies.flatten.grep(Dependency).flat_map(&:to_formula)] end.flatten.uniq - formulae_dependencies.concat(dependants.upgradeable) if dependants&.upgradeable + formulae_dependencies.concat(dependants.upgradeable) if dependants.upgradeable formulae_dependencies.uniq end end diff --git a/Library/Homebrew/reinstall.rb b/Library/Homebrew/reinstall.rb index 9c1e7563e5..cfb9a3db78 100644 --- a/Library/Homebrew/reinstall.rb +++ b/Library/Homebrew/reinstall.rb @@ -1,142 +1,153 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true -require "formula_installer" require "development_tools" require "messages" +# Needed to handle circular require dependency. +# rubocop:disable Lint/EmptyClass +class FormulaInstaller; end +# rubocop:enable Lint/EmptyClass + module Homebrew module Reinstall - # struct to keep context of the current installer, keg, formula and option - InstallationContext = Struct.new(:formula_installer, :keg, :formula, :options) - def self.build_install_context( - formula, - flags:, - force_bottle: false, - build_from_source_formulae: [], - interactive: false, - keep_tmp: false, - debug_symbols: false, - force: false, - debug: false, - quiet: false, - verbose: false, - git: false - ) - if formula.opt_prefix.directory? - keg = Keg.new(formula.opt_prefix.resolved_path) - 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? - backup keg - else - link_keg = nil - installed_as_dependency = false - installed_on_request = true - build_bottle = false - end + class InstallationContext < T::Struct + const :formula_installer, ::FormulaInstaller + const :keg, T.nilable(Keg) + const :formula, Formula + const :options, Options + end - build_options = BuildOptions.new(Options.create(flags), formula.options) - options = build_options.used_options - options |= formula.build.used_options - options &= formula.options - - fi = FormulaInstaller.new( + class << self + 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, debug: T::Boolean, quiet: T::Boolean, + verbose: T::Boolean, git: T::Boolean + ).returns(InstallationContext) + } + def build_install_context( formula, - **{ - options:, - link_keg:, - installed_as_dependency:, - installed_on_request:, - build_bottle:, - force_bottle:, - build_from_source_formulae:, - git:, - interactive:, - keep_tmp:, - debug_symbols:, - force:, - debug:, - quiet:, - verbose:, - }.compact, + flags:, + force_bottle: false, + build_from_source_formulae: [], + interactive: false, + keep_tmp: false, + debug_symbols: false, + force: false, + debug: false, + quiet: false, + verbose: false, + git: false ) - InstallationContext.new(fi, keg, formula, options) - end + if formula.opt_prefix.directory? + keg = Keg.new(formula.opt_prefix.resolved_path) + 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? + backup keg + else + link_keg = nil + installed_as_dependency = false + installed_on_request = true + build_bottle = false + end - def self.reinstall_formula( - install_context, - flags:, - force_bottle: false, - build_from_source_formulae: [], - interactive: false, - keep_tmp: false, - debug_symbols: false, - force: false, - debug: false, - quiet: false, - verbose: false, - git: false - ) - formula_installer = install_context.formula_installer - keg = install_context.keg - formula = install_context.formula - options = install_context.options - link_keg = keg&.linked? - formula_installer.prelude - formula_installer.fetch + build_options = BuildOptions.new(Options.create(flags), formula.options) + options = build_options.used_options + options |= formula.build.used_options + options &= formula.options - oh1 "Reinstalling #{Formatter.identifier(formula.full_name)} #{options.to_a.join " "}" + formula_installer = FormulaInstaller.new( + formula, + **{ + options:, + link_keg:, + installed_as_dependency:, + installed_on_request:, + build_bottle:, + force_bottle:, + build_from_source_formulae:, + git:, + interactive:, + keep_tmp:, + debug_symbols:, + force:, + debug:, + quiet:, + verbose:, + }.compact, + ) + InstallationContext.new(formula_installer:, keg:, formula:, options:) + end - formula_installer.install - formula_installer.finish - rescue FormulaInstallationAlreadyAttemptedError - nil - # Any other exceptions we want to restore the previous keg and report the error. - rescue Exception # rubocop:disable Lint/RescueException - ignore_interrupts { restore_backup(keg, link_keg, verbose:) } - raise - else - begin - FileUtils.rm_r(backup_path(keg)) if backup_path(keg).exist? - rescue Errno::EACCES, Errno::ENOTEMPTY - odie <<~EOS - Could not remove #{backup_path(keg).parent.basename} backup keg! Do so manually: - sudo rm -rf #{backup_path(keg)} - EOS + sig { params(install_context: InstallationContext).void } + def reinstall_formula(install_context) + formula_installer = install_context.formula_installer + keg = install_context.keg + formula = install_context.formula + options = install_context.options + link_keg = keg&.linked? + verbose = formula_installer.verbose? + + oh1 "Reinstalling #{Formatter.identifier(formula.full_name)} #{options.to_a.join " "}" + + formula_installer.install + formula_installer.finish + rescue FormulaInstallationAlreadyAttemptedError + nil + # Any other exceptions we want to restore the previous keg and report the error. + rescue Exception # rubocop:disable Lint/RescueException + ignore_interrupts { restore_backup(keg, link_keg, verbose:) if keg } + raise + else + if keg + backup_keg = backup_path(keg) + begin + FileUtils.rm_r(backup_keg) if backup_keg.exist? + rescue Errno::EACCES, Errno::ENOTEMPTY + odie <<~EOS + Could not remove #{backup_keg.parent.basename} backup keg! Do so manually: + sudo rm -rf #{backup_keg} + EOS + end + end + end + + private + + sig { params(keg: Keg).void } + def backup(keg) + keg.unlink + begin + keg.rename backup_path(keg) + rescue Errno::EACCES, Errno::ENOTEMPTY + odie <<~EOS + Could not rename #{keg.name} keg! Check/fix its permissions: + sudo chown -R #{ENV.fetch("USER", "$(whoami)")} #{keg} + EOS + end + end + + sig { params(keg: Keg, keg_was_linked: T::Boolean, verbose: T::Boolean).void } + def restore_backup(keg, keg_was_linked, verbose:) + path = backup_path(keg) + + return unless path.directory? + + FileUtils.rm_r(Pathname.new(keg)) if keg.exist? + + path.rename keg.to_s + keg.link(verbose:) if keg_was_linked + end + + sig { params(keg: Keg).returns(Pathname) } + def backup_path(keg) + Pathname.new "#{keg}.reinstall" end end - - def self.backup(keg) - keg.unlink - begin - keg.rename backup_path(keg) - rescue Errno::EACCES, Errno::ENOTEMPTY - odie <<~EOS - Could not rename #{keg.name} keg! Check/fix its permissions: - sudo chown -R #{ENV.fetch("USER", "$(whoami)")} #{keg} - EOS - end - end - private_class_method :backup - - def self.restore_backup(keg, keg_was_linked, verbose:) - path = backup_path(keg) - - return unless path.directory? - - FileUtils.rm_r(Pathname.new(keg)) if keg.exist? - - path.rename keg - keg.link(verbose:) if keg_was_linked - end - private_class_method :restore_backup - - def self.backup_path(path) - Pathname.new "#{path}.reinstall" - end - private_class_method :backup_path end end diff --git a/Library/Homebrew/test/cask/info_spec.rb b/Library/Homebrew/test/cask/info_spec.rb index dda7abf46c..ea41b57d94 100644 --- a/Library/Homebrew/test/cask/info_spec.rb +++ b/Library/Homebrew/test/cask/info_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "utils" +require "cask/info" RSpec.describe Cask::Info, :cask do let(:args) { instance_double(Homebrew::Cmd::Info::Args) } diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb index 6c24f3635e..a7e8938bce 100644 --- a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb +++ b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb @@ -208,6 +208,7 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin def install_test_formula(name, content = nil, build_bottle: false) setup_test_formula(name, content) fi = FormulaInstaller.new(Formula[name], build_bottle:, installed_on_request: true) + fi.prelude_fetch fi.prelude fi.fetch fi.install diff --git a/Library/Homebrew/upgrade.rb b/Library/Homebrew/upgrade.rb index 8cf4845a1b..2f4855709f 100644 --- a/Library/Homebrew/upgrade.rb +++ b/Library/Homebrew/upgrade.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true require "reinstall" @@ -11,52 +11,471 @@ require "utils/topological_hash" module Homebrew # Helper functions for upgrading formulae. module Upgrade - Dependents = Struct.new(:upgradeable, :pinned, :skipped) + class Dependents < T::Struct + const :upgradeable, T::Array[Formula] + const :pinned, T::Array[Formula] + const :skipped, T::Array[Formula] + end - def self.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? + 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 + # 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 - dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install) - begin - formulae_to_install = dependency_graph.tsort & formulae_to_install - rescue TSort::Cyclic - raise CyclicDependencyError, dependency_graph.strongly_connected_components if Homebrew::EnvConfig.developer? + 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 - formulae_to_install.filter_map do |formula| - Migrator.migrate_if_needed(formula, force:, dry_run:) - begin - fi = create_formula_installer( + 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." + puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (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:, @@ -66,442 +485,19 @@ module Homebrew 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 = Version.new(hash["version"]) if hash["version"].present? - Dependency.new(dependency).installed?(minimum_version:, minimum_revision: hash["revision"]) - 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 - - def self.upgrade_formulae(formula_installers, dry_run: false, verbose: false) - valid_formula_installers = formula_installers.dup - - unless dry_run - valid_formula_installers.select! do |fi| - fi.prelude - fi.fetch - true - rescue CannotInstallFormulaError => e - ofail e - false - rescue UnsatisfiedRequirements, DownloadError => e - ofail "#{fi.formula.full_name}: #{e}" - false - end - end - - valid_formula_installers.each do |fi| - upgrade_formula(fi, dry_run:, verbose:) - Cleanup.install_formula_clean!(fi.formula, dry_run:) - end - end - - private_class_method def self.outdated_kegs(formula) - [formula, *formula.old_installed_formulae].map(&:linked_keg) - .select(&:directory?) - .map { |k| Keg.new(k.resolved_path) } - end - - private_class_method def self.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 - - private_class_method def self.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 - - def self.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_formula(formula_installer, upgrade: true) - rescue BuildError => e - e.dump(verbose:) - puts - Homebrew.failed = true - end - - def self.install_formula(formula_installer, upgrade:) - formula = formula_installer.formula - - formula_installer.check_installation_already_attempted - - if upgrade - print_upgrade_message(formula, formula_installer.options) - - kegs = outdated_kegs(formula) - linked_kegs = kegs.select(&:linked?) - else - formula.print_tap_action - end - - # first we unlink the currently active keg for this formula otherwise it is - # possible for the existing build to interfere with the build we are about to - # do! Seriously, it happens! - kegs.each(&:unlink) if kegs.present? - - formula_installer.install - formula_installer.finish - rescue FormulaInstallationAlreadyAttemptedError - # We already attempted to upgrade f as part of the dependency tree of - # another formula. In that case, don't generate an error, just move on. - nil - ensure - # restore previous installation state if build failed - begin - linked_kegs&.each(&:link) unless formula.latest_version_installed? - rescue - nil - end - end - - private_class_method def self.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 - - def self.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." - puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`)." - @puts_no_installed_dependents_check_disable_message_if_not_already = true - end - - def self.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 - ) - 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 - end - formulae_to_install = formulae.dup - formulae_to_install.reject! { |f| f.core_formula? && f.versioned_formula? } - return if formulae_to_install.empty? - - already_broken_dependents = check_broken_dependents(formulae_to_install) - - # TODO: this should be refactored to use FormulaInstaller new logic - outdated_dependents = - 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_dependents, skipped_dependents = outdated_dependents.partition do |dependent| - dependent.bottled? && dependent.deps.map(&:to_formula).all?(&:bottled?) - end - - return if outdated_dependents.blank? && already_broken_dependents.blank? - - outdated_dependents -= formulae_to_install if dry_run - - upgradeable_dependents = - outdated_dependents.reject(&:pinned?) - .sort { |a, b| depends_on(a, b) } - pinned_dependents = - outdated_dependents.select(&:pinned?) - .sort { |a, b| depends_on(a, b) } - - Dependents.new(upgradeable_dependents, pinned_dependents, skipped_dependents) - end - - def self.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}:" - Upgrade.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:, + }.compact, ) - upgrade_formulae(dependent_installers, dry_run: dry_run, verbose: 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..." - Upgrade.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." + 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 - ohai "No broken dependents found!" + T.must(one <=> two) 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:" - Upgrade.puts_no_installed_dependents_check_disable_message_if_not_already! - puts reinstallable_broken_dependents.map(&:full_specified_name) - .join(", ") - end - - return if dry_run - - reinstallable_broken_dependents.each do |formula| - formula_installer = 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:, - ) - Reinstall.reinstall_formula( - formula_installer, - flags:, - force_bottle:, - build_from_source_formulae:, - interactive:, - keep_tmp:, - debug_symbols:, - force:, - debug:, - quiet:, - verbose:, - ) - 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_class_method def self.depends_on(one, two) - if one.any_installed_keg - &.runtime_dependencies - &.any? { |dependency| dependency["full_name"] == two.full_name } - 1 - else - one <=> two end end end