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
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

View File

@ -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

View File

@ -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(

View File

@ -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?

View File

@ -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?

View File

@ -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

View File

@ -1,15 +1,33 @@
# 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(
class InstallationContext < T::Struct
const :formula_installer, ::FormulaInstaller
const :keg, T.nilable(Keg)
const :formula, Formula
const :options, Options
end
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,
flags:,
force_bottle: false,
@ -43,7 +61,7 @@ module Homebrew
options |= formula.build.used_options
options &= formula.options
fi = FormulaInstaller.new(
formula_installer = FormulaInstaller.new(
formula,
**{
options:,
@ -63,30 +81,17 @@ module Homebrew
verbose:,
}.compact,
)
InstallationContext.new(fi, keg, formula, options)
InstallationContext.new(formula_installer:, keg:, formula:, options:)
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
)
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?
formula_installer.prelude
formula_installer.fetch
verbose = formula_installer.verbose?
oh1 "Reinstalling #{Formatter.identifier(formula.full_name)} #{options.to_a.join " "}"
@ -96,20 +101,26 @@ module Homebrew
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:) }
ignore_interrupts { restore_backup(keg, link_keg, verbose:) if keg }
raise
else
if keg
backup_keg = backup_path(keg)
begin
FileUtils.rm_r(backup_path(keg)) if backup_path(keg).exist?
FileUtils.rm_r(backup_keg) if backup_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)}
Could not remove #{backup_keg.parent.basename} backup keg! Do so manually:
sudo rm -rf #{backup_keg}
EOS
end
end
end
def self.backup(keg)
private
sig { params(keg: Keg).void }
def backup(keg)
keg.unlink
begin
keg.rename backup_path(keg)
@ -120,23 +131,23 @@ module Homebrew
EOS
end
end
private_class_method :backup
def self.restore_backup(keg, keg_was_linked, verbose:)
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
path.rename keg.to_s
keg.link(verbose:) if keg_was_linked
end
private_class_method :restore_backup
def self.backup_path(path)
Pathname.new "#{path}.reinstall"
sig { params(keg: Keg).returns(Pathname) }
def backup_path(keg)
Pathname.new "#{keg}.reinstall"
end
end
private_class_method :backup_path
end
end

View File

@ -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) }

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)
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

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "reinstall"
@ -11,9 +11,23 @@ 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(
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,
@ -29,7 +43,7 @@ module Homebrew
quiet: false,
verbose: false
)
return if formulae_to_install.empty?
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.
@ -47,7 +61,9 @@ module Homebrew
begin
formulae_to_install = dependency_graph.tsort & formulae_to_install
rescue TSort::Cyclic
raise CyclicDependencyError, dependency_graph.strongly_connected_components if Homebrew::EnvConfig.developer?
if Homebrew::EnvConfig.developer?
raise CyclicDependencyError, dependency_graph.strongly_connected_components
end
end
formulae_to_install.filter_map do |formula|
@ -70,8 +86,10 @@ module Homebrew
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"])
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
@ -91,21 +109,12 @@ module Homebrew
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
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|
@ -114,13 +123,15 @@ module Homebrew
end
end
private_class_method def self.outdated_kegs(formula)
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
private_class_method def self.print_upgrade_message(formula, fi_options)
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
@ -130,7 +141,295 @@ module Homebrew
puts " #{version_upgrade} #{fi_options.to_a.join(" ")}"
end
private_class_method def self.create_formula_installer(
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,
@ -190,318 +489,15 @@ module Homebrew
)
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
# 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."
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:"
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)
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
one <=> two
T.must(one <=> two)
end
end
end
end