Merge pull request #20296 from Homebrew/upgrade_reinstall_download_queue

Optionally use DownloadQueue for reinstall, upgrade.
This commit is contained in:
Mike McQuaid 2025-07-24 15:03:36 +00:00 committed by GitHub
commit 097e2a351c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 775 additions and 667 deletions

View File

@ -585,13 +585,15 @@ module Cask
}.compact }.compact
Homebrew::Install.perform_preinstall_checks_once Homebrew::Install.perform_preinstall_checks_once
valid_formula_installers = Homebrew::Install.fetch_formulae(primary_container.dependencies)
primary_container.dependencies.each do |dep| primary_container.dependencies.each do |dep|
next unless valid_formula_installers.include?(dep)
fi = FormulaInstaller.new( fi = FormulaInstaller.new(
dep, dep,
**install_options, **install_options,
) )
fi.prelude
fi.fetch
fi.install fi.install
fi.finish fi.finish
end end

View File

@ -426,6 +426,9 @@ on_request: true)
end end
ohai "Installing dependencies: #{missing_formulae_and_casks.map(&:to_s).join(", ")}" 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| missing_formulae_and_casks.each do |cask_or_formula|
if cask_or_formula.is_a?(Cask) if cask_or_formula.is_a?(Cask)
if skip_cask_deps? if skip_cask_deps?
@ -433,7 +436,7 @@ on_request: true)
next next
end end
Installer.new( cask_installers << Installer.new(
cask_or_formula, cask_or_formula,
adopt: adopt?, adopt: adopt?,
binaries: binaries?, binaries: binaries?,
@ -444,10 +447,9 @@ on_request: true)
quiet: quiet?, quiet: quiet?,
require_sha: require_sha?, require_sha: require_sha?,
verbose: verbose?, verbose: verbose?,
).install )
else else
Homebrew::Install.perform_preinstall_checks_once formula_installers << FormulaInstaller.new(
fi = FormulaInstaller.new(
cask_or_formula, cask_or_formula,
**{ **{
show_header: true, show_header: true,
@ -456,12 +458,18 @@ on_request: true)
verbose: verbose?, verbose: verbose?,
}.compact, }.compact,
) )
fi.prelude
fi.fetch
fi.install
fi.finish
end end
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 end
def caveats def caveats

View File

@ -130,7 +130,7 @@ module Homebrew
unless formulae.empty? unless formulae.empty?
Install.perform_preinstall_checks_once Install.perform_preinstall_checks_once
install_context = formulae.map do |formula| reinstall_contexts = formulae.filter_map do |formula|
if formula.pinned? if formula.pinned?
onoe "#{formula.full_name} is pinned. You must unpin it to reinstall." onoe "#{formula.full_name} is pinned. You must unpin it to reinstall."
next next
@ -167,27 +167,18 @@ module Homebrew
verbose: args.verbose?, 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. # 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| valid_formula_installers = Install.fetch_formulae(formulae_installers)
Homebrew::Reinstall.reinstall_formula(
f, reinstall_contexts.each do |reinstall_context|
flags: args.flags_only, next unless valid_formula_installers.include?(reinstall_context.formula_installer)
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae, Homebrew::Reinstall.reinstall_formula(reinstall_context)
interactive: args.interactive?, Cleanup.install_formula_clean!(reinstall_context.formula)
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)
end end
Upgrade.upgrade_dependents( Upgrade.upgrade_dependents(

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
module OS module OS
@ -30,18 +30,21 @@ module OS
libstdc++.so.6 libstdc++.so.6
].freeze ].freeze
sig { params(all_fatal: T::Boolean).void }
def perform_preinstall_checks(all_fatal: false) def perform_preinstall_checks(all_fatal: false)
super super
symlink_ld_so symlink_ld_so
setup_preferred_gcc_libs setup_preferred_gcc_libs
end end
sig { void }
def global_post_install def global_post_install
super super
symlink_ld_so symlink_ld_so
setup_preferred_gcc_libs setup_preferred_gcc_libs
end end
sig { void }
def check_cpu def check_cpu
return if ::Hardware::CPU.intel? && ::Hardware::CPU.is_64_bit? return if ::Hardware::CPU.intel? && ::Hardware::CPU.is_64_bit?
return if ::Hardware::CPU.arm? return if ::Hardware::CPU.arm?
@ -56,6 +59,7 @@ module OS
::Kernel.abort message ::Kernel.abort message
end end
sig { void }
def symlink_ld_so def symlink_ld_so
brew_ld_so = HOMEBREW_PREFIX/"lib/ld.so" brew_ld_so = HOMEBREW_PREFIX/"lib/ld.so"
@ -75,6 +79,7 @@ module OS
FileUtils.ln_sf ld_so, brew_ld_so FileUtils.ln_sf ld_so, brew_ld_so
end end
sig { void }
def setup_preferred_gcc_libs def setup_preferred_gcc_libs
gcc_opt_prefix = HOMEBREW_PREFIX/"opt/#{OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA}" gcc_opt_prefix = HOMEBREW_PREFIX/"opt/#{OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA}"
glibc_installed = (HOMEBREW_PREFIX/"opt/glibc/bin/ld.so").readable? glibc_installed = (HOMEBREW_PREFIX/"opt/glibc/bin/ld.so").readable?

View File

@ -13,7 +13,6 @@ require "sandbox"
require "development_tools" require "development_tools"
require "cache_store" require "cache_store"
require "linkage_checker" require "linkage_checker"
require "install"
require "messages" require "messages"
require "cask/cask_loader" require "cask/cask_loader"
require "cmd/install" require "cmd/install"
@ -61,8 +60,8 @@ class FormulaInstaller
bottle_arch: T.nilable(String), bottle_arch: T.nilable(String),
ignore_deps: T::Boolean, ignore_deps: T::Boolean,
only_deps: T::Boolean, only_deps: T::Boolean,
include_test_formulae: T::Array[Formula], include_test_formulae: T::Array[String],
build_from_source_formulae: T::Array[Formula], build_from_source_formulae: T::Array[String],
env: T.nilable(String), env: T.nilable(String),
git: T::Boolean, git: T::Boolean,
interactive: T::Boolean, interactive: T::Boolean,
@ -507,7 +506,10 @@ class FormulaInstaller
lock lock
start_time = Time.now 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. # Warn if a more recent version of this formula is available in the tap.
begin begin
@ -896,6 +898,7 @@ on_request: installed_on_request?, options:)
verbose: verbose?, verbose: verbose?,
) )
oh1 "Installing #{formula.full_name} dependency: #{Formatter.identifier(dep.name)}" 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.prelude
fi.install fi.install
fi.finish fi.finish
@ -955,6 +958,7 @@ on_request: installed_on_request?, options:)
fix_dynamic_linkage(keg) if !@poured_bottle || !formula.bottle_specification.skip_relocation? fix_dynamic_linkage(keg) if !@poured_bottle || !formula.bottle_specification.skip_relocation?
require "install"
Homebrew::Install.global_post_install Homebrew::Install.global_post_install
if build_bottle? || skip_post_install? if build_bottle? || skip_post_install?

View File

@ -21,6 +21,7 @@ module Homebrew
end end
end end
sig { params(cc: T.nilable(String)).void }
def check_cc_argv(cc) def check_cc_argv(cc)
return unless cc return unless cc
@ -32,13 +33,16 @@ module Homebrew
EOS EOS
end end
sig { params(all_fatal: T::Boolean).void }
def perform_build_from_source_checks(all_fatal: false) def perform_build_from_source_checks(all_fatal: false)
Diagnostic.checks(:fatal_build_from_source_checks) Diagnostic.checks(:fatal_build_from_source_checks)
Diagnostic.checks(:build_from_source_checks, fatal: all_fatal) Diagnostic.checks(:build_from_source_checks, fatal: all_fatal)
end end
sig { void }
def global_post_install; end def global_post_install; end
sig { void }
def check_prefix def check_prefix
if (Hardware::CPU.intel? || Hardware::CPU.in_rosetta2?) && if (Hardware::CPU.intel? || Hardware::CPU.in_rosetta2?) &&
HOMEBREW_PREFIX.to_s == HOMEBREW_MACOS_ARM_DEFAULT_PREFIX HOMEBREW_PREFIX.to_s == HOMEBREW_MACOS_ARM_DEFAULT_PREFIX
@ -64,6 +68,11 @@ module Homebrew
end end
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?( def install_formula?(
formula, formula,
head: false, head: false,
@ -232,6 +241,16 @@ module Homebrew
false false
end 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( def formula_installers(
formulae_to_install, formulae_to_install,
installed_on_request: true, installed_on_request: true,
@ -289,6 +308,53 @@ module Homebrew
end end
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( def install_formulae(
formula_installers, formula_installers,
installed_on_request: true, installed_on_request: true,
@ -328,41 +394,17 @@ module Homebrew
return return
end end
formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence valid_formula_installers = fetch_formulae(formula_installers)
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.each do |fi| valid_formula_installers.each do |fi|
install_formula(fi) formula = fi.formula
Cleanup.install_formula_clean!(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
end end
sig { params(formula: Formula, dependencies: T::Array[[Dependency, Options]]).void }
def print_dry_run_dependencies(formula, dependencies) def print_dry_run_dependencies(formula, dependencies)
return if dependencies.empty? return if dependencies.empty?
@ -373,6 +415,7 @@ module Homebrew
end end
# If asking the user is enabled, show dependency and size information. # 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:) def ask_formulae(formulae_installer, dependants, args:)
return if formulae_installer.empty? return if formulae_installer.empty?
@ -391,6 +434,7 @@ module Homebrew
ask_input ask_input
end end
sig { params(casks: T::Array[Cask::Cask]).void }
def ask_casks(casks) def ask_casks(casks)
return if casks.empty? return if casks.empty?
@ -400,8 +444,51 @@ module Homebrew
ask_input ask_input
end 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 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) def perform_preinstall_checks(all_fatal: false)
check_prefix check_prefix
check_cpu check_cpu
@ -410,6 +497,7 @@ module Homebrew
Diagnostic.checks(:fatal_preinstall_checks) Diagnostic.checks(:fatal_preinstall_checks)
end end
sig { void }
def attempt_directory_creation def attempt_directory_creation
Keg.must_exist_directories.each do |dir| Keg.must_exist_directories.each do |dir|
FileUtils.mkdir_p(dir) unless dir.exist? FileUtils.mkdir_p(dir) unless dir.exist?
@ -418,6 +506,7 @@ module Homebrew
end end
end end
sig { void }
def check_cpu def check_cpu
return unless Hardware::CPU.ppc? return unless Hardware::CPU.ppc?
@ -428,14 +517,7 @@ module Homebrew
EOS EOS
end end
def install_formula(formula_installer) sig { void }
formula = formula_installer.formula
upgrade = formula.linked? && formula.outdated? && !formula.head? && !Homebrew::EnvConfig.no_install_upgrade?
Upgrade.install_formula(formula_installer, upgrade:)
end
def ask_input def ask_input
ohai "Do you want to proceed with the installation? [Y/y/yes/N/n/no]" ohai "Do you want to proceed with the installation? [Y/y/yes/N/n/no]"
accepted_inputs = %w[y yes] accepted_inputs = %w[y yes]
@ -456,13 +538,16 @@ module Homebrew
end end
# Compute the total sizes (download, installed, and net) for the given formulae. # 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) def compute_total_sizes(sized_formulae, debug: false)
total_download_size = 0 total_download_size = 0
total_installed_size = 0 total_installed_size = 0
total_net_size = 0 total_net_size = 0
sized_formulae.select(&:bottle).each do |formula| sized_formulae.each do |formula|
bottle = formula.bottle bottle = formula.bottle
next unless bottle
# Fetch additional bottle metadata (if necessary). # Fetch additional bottle metadata (if necessary).
bottle.fetch_tab(quiet: !debug) bottle.fetch_tab(quiet: !debug)
@ -481,11 +566,15 @@ module Homebrew
net: total_net_size } net: total_net_size }
end end
sig {
params(formulae_installer: T::Array[FormulaInstaller],
dependants: Homebrew::Upgrade::Dependents).returns(T::Array[Formula])
}
def collect_dependencies(formulae_installer, dependants) def collect_dependencies(formulae_installer, dependants)
formulae_dependencies = formulae_installer.flat_map do |f| formulae_dependencies = formulae_installer.flat_map do |f|
[f.formula, f.compute_dependencies.flatten.grep(Dependency).flat_map(&:to_formula)] [f.formula, f.compute_dependencies.flatten.grep(Dependency).flat_map(&:to_formula)]
end.flatten.uniq end.flatten.uniq
formulae_dependencies.concat(dependants.upgradeable) if dependants&.upgradeable formulae_dependencies.concat(dependants.upgradeable) if dependants.upgradeable
formulae_dependencies.uniq formulae_dependencies.uniq
end end
end end

View File

@ -1,142 +1,153 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "formula_installer"
require "development_tools" require "development_tools"
require "messages" require "messages"
# Needed to handle circular require dependency.
# rubocop:disable Lint/EmptyClass
class FormulaInstaller; end
# rubocop:enable Lint/EmptyClass
module Homebrew module Homebrew
module Reinstall module Reinstall
# struct to keep context of the current installer, keg, formula and option class InstallationContext < T::Struct
InstallationContext = Struct.new(:formula_installer, :keg, :formula, :options) const :formula_installer, ::FormulaInstaller
def self.build_install_context( const :keg, T.nilable(Keg)
formula, const :formula, Formula
flags:, const :options, Options
force_bottle: false, end
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
build_options = BuildOptions.new(Options.create(flags), formula.options) class << self
options = build_options.used_options sig {
options |= formula.build.used_options params(
options &= formula.options formula: Formula, flags: T::Array[String], force_bottle: T::Boolean,
build_from_source_formulae: T::Array[String], interactive: T::Boolean, keep_tmp: T::Boolean,
fi = FormulaInstaller.new( 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, formula,
**{ flags:,
options:, force_bottle: false,
link_keg:, build_from_source_formulae: [],
installed_as_dependency:, interactive: false,
installed_on_request:, keep_tmp: false,
build_bottle:, debug_symbols: false,
force_bottle:, force: false,
build_from_source_formulae:, debug: false,
git:, quiet: false,
interactive:, verbose: false,
keep_tmp:, git: false
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
}.compact,
) )
InstallationContext.new(fi, keg, formula, options) if formula.opt_prefix.directory?
end 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( build_options = BuildOptions.new(Options.create(flags), formula.options)
install_context, options = build_options.used_options
flags:, options |= formula.build.used_options
force_bottle: false, options &= formula.options
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
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 sig { params(install_context: InstallationContext).void }
formula_installer.finish def reinstall_formula(install_context)
rescue FormulaInstallationAlreadyAttemptedError formula_installer = install_context.formula_installer
nil keg = install_context.keg
# Any other exceptions we want to restore the previous keg and report the error. formula = install_context.formula
rescue Exception # rubocop:disable Lint/RescueException options = install_context.options
ignore_interrupts { restore_backup(keg, link_keg, verbose:) } link_keg = keg&.linked?
raise verbose = formula_installer.verbose?
else
begin oh1 "Reinstalling #{Formatter.identifier(formula.full_name)} #{options.to_a.join " "}"
FileUtils.rm_r(backup_path(keg)) if backup_path(keg).exist?
rescue Errno::EACCES, Errno::ENOTEMPTY formula_installer.install
odie <<~EOS formula_installer.finish
Could not remove #{backup_path(keg).parent.basename} backup keg! Do so manually: rescue FormulaInstallationAlreadyAttemptedError
sudo rm -rf #{backup_path(keg)} nil
EOS # 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
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
end end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "utils" require "utils"
require "cask/info"
RSpec.describe Cask::Info, :cask do RSpec.describe Cask::Info, :cask do
let(:args) { instance_double(Homebrew::Cmd::Info::Args) } let(:args) { instance_double(Homebrew::Cmd::Info::Args) }

View File

@ -208,6 +208,7 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
def install_test_formula(name, content = nil, build_bottle: false) def install_test_formula(name, content = nil, build_bottle: false)
setup_test_formula(name, content) setup_test_formula(name, content)
fi = FormulaInstaller.new(Formula[name], build_bottle:, installed_on_request: true) fi = FormulaInstaller.new(Formula[name], build_bottle:, installed_on_request: true)
fi.prelude_fetch
fi.prelude fi.prelude
fi.fetch fi.fetch
fi.install fi.install

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "reinstall" require "reinstall"
@ -11,52 +11,471 @@ require "utils/topological_hash"
module Homebrew module Homebrew
# Helper functions for upgrading formulae. # Helper functions for upgrading formulae.
module Upgrade 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( class << self
formulae_to_install, sig {
flags:, params(
dry_run: false, formulae_to_install: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean,
force_bottle: false, force_bottle: T::Boolean, build_from_source_formulae: T::Array[String],
build_from_source_formulae: [], dependents: T::Boolean, interactive: T::Boolean, keep_tmp: T::Boolean,
dependents: false, debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean,
interactive: false, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean
keep_tmp: false, ).returns(T::Array[FormulaInstaller])
debug_symbols: false, }
force: false, def formula_installers(
overwrite: false, formulae_to_install,
debug: false, flags:,
quiet: false, dry_run: false,
verbose: false force_bottle: false,
) build_from_source_formulae: [],
return if formulae_to_install.empty? 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 # Sort keg-only before non-keg-only formulae to avoid any needless conflicts
# with outdated, non-keg-only versions of formulae being upgraded. # with outdated, non-keg-only versions of formulae being upgraded.
formulae_to_install.sort! do |a, b| formulae_to_install.sort! do |a, b|
if !a.keg_only? && b.keg_only? if !a.keg_only? && b.keg_only?
1 1
elsif a.keg_only? && !b.keg_only? elsif a.keg_only? && !b.keg_only?
-1 -1
else else
0 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
end end
dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install) sig { params(formula_installers: T::Array[FormulaInstaller], dry_run: T::Boolean, verbose: T::Boolean).void }
begin def upgrade_formulae(formula_installers, dry_run: false, verbose: false)
formulae_to_install = dependency_graph.tsort & formulae_to_install valid_formula_installers = if dry_run
rescue TSort::Cyclic formula_installers
raise CyclicDependencyError, dependency_graph.strongly_connected_components if Homebrew::EnvConfig.developer? 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 end
formulae_to_install.filter_map do |formula| sig { params(formula: Formula).returns(T::Array[Keg]) }
Migrator.migrate_if_needed(formula, force:, dry_run:) def outdated_kegs(formula)
begin [formula, *formula.old_installed_formulae].map(&:linked_keg)
fi = create_formula_installer( .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, formula,
flags:, flags:,
force_bottle:, 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:, build_from_source_formulae:,
interactive:, interactive:,
keep_tmp:, keep_tmp:,
@ -66,442 +485,19 @@ module Homebrew
debug:, debug:,
quiet:, quiet:,
verbose:, verbose:,
) }.compact,
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:,
) )
upgrade_formulae(dependent_installers, dry_run: dry_run, verbose: verbose)
end end
# Update installed formulae after upgrading sig { params(one: Formula, two: Formula).returns(Integer) }
installed_formulae = FormulaInstaller.installed.to_a def depends_on(one, two)
if one.any_installed_keg
# Assess the dependents tree again now we've upgraded. &.runtime_dependencies
unless dry_run &.any? { |dependency| dependency["full_name"] == two.full_name }
oh1 "Checking for dependents of upgraded formulae..." 1
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."
else else
ohai "No broken dependents found!" T.must(one <=> two)
end 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 end
end end