brew/Library/Homebrew/upgrade.rb

508 lines
18 KiB
Ruby
Raw Normal View History

# typed: strict
# frozen_string_literal: true
require "reinstall"
require "formula_installer"
require "development_tools"
require "messages"
require "cleanup"
require "utils/topological_hash"
require "utils/output"
module Homebrew
2020-08-24 00:37:22 +02:00
# Helper functions for upgrading formulae.
module Upgrade
extend Utils::Output::Mixin
class Dependents < T::Struct
const :upgradeable, T::Array[Formula]
const :pinned, T::Array[Formula]
const :skipped, T::Array[Formula]
end
class << self
sig {
params(
formulae_to_install: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean,
force_bottle: T::Boolean, build_from_source_formulae: T::Array[String],
dependents: T::Boolean, interactive: T::Boolean, keep_tmp: T::Boolean,
debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean,
debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean
).returns(T::Array[FormulaInstaller])
}
def formula_installers(
formulae_to_install,
flags:,
dry_run: false,
force_bottle: false,
build_from_source_formulae: [],
dependents: false,
interactive: false,
keep_tmp: false,
debug_symbols: false,
force: false,
overwrite: false,
debug: false,
quiet: false,
verbose: false
)
return [] if formulae_to_install.empty?
# Sort keg-only before non-keg-only formulae to avoid any needless conflicts
# with outdated, non-keg-only versions of formulae being upgraded.
formulae_to_install.sort! do |a, b|
if !a.keg_only? && b.keg_only?
1
elsif a.keg_only? && !b.keg_only?
-1
else
0
end
end
dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install)
2020-08-24 00:37:22 +02:00
begin
formulae_to_install = dependency_graph.tsort & formulae_to_install
rescue TSort::Cyclic
if Homebrew::EnvConfig.developer?
raise CyclicDependencyError, dependency_graph.strongly_connected_components
2025-06-20 16:41:32 -04:00
end
end
2025-06-20 16:41:32 -04:00
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
2020-08-24 00:37:22 +02:00
end
end
sig { params(formula_installers: T::Array[FormulaInstaller], dry_run: T::Boolean, verbose: T::Boolean).void }
def upgrade_formulae(formula_installers, dry_run: false, verbose: false)
valid_formula_installers = if dry_run
formula_installers
else
Install.fetch_formulae(formula_installers)
end
2025-07-23 21:13:22 +01:00
valid_formula_installers.each do |fi|
upgrade_formula(fi, dry_run:, verbose:)
Cleanup.install_formula_clean!(fi.formula, dry_run:)
end
2025-06-20 16:41:32 -04:00
end
sig { params(formula: Formula).returns(T::Array[Keg]) }
def outdated_kegs(formula)
[formula, *formula.old_installed_formulae].map(&:linked_keg)
.select(&:directory?)
.map { |k| Keg.new(k.resolved_path) }
end
sig { params(formula: Formula, fi_options: Options).void }
def print_upgrade_message(formula, fi_options)
version_upgrade = if formula.optlinked?
"#{Keg.new(formula.opt_prefix).version} -> #{formula.pkg_version}"
else
"-> #{formula.pkg_version}"
end
oh1 "Upgrading #{Formatter.identifier(formula.full_specified_name)}"
puts " #{version_upgrade} #{fi_options.to_a.join(" ")}"
end
sig {
params(
formulae: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean,
ask: T::Boolean, installed_on_request: T::Boolean, force_bottle: T::Boolean,
build_from_source_formulae: T::Array[String], interactive: T::Boolean,
keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean,
debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean
).returns(Dependents)
}
def dependants(
formulae,
flags:,
dry_run: false,
ask: false,
installed_on_request: false,
force_bottle: false,
build_from_source_formulae: [],
interactive: false,
keep_tmp: false,
debug_symbols: false,
force: false,
debug: false,
quiet: false,
verbose: false
)
no_dependents = Dependents.new(upgradeable: [], pinned: [], skipped: [])
if Homebrew::EnvConfig.no_installed_dependents_check?
unless Homebrew::EnvConfig.no_env_hints?
opoo <<~EOS
`$HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set: not checking for outdated
dependents or dependents with broken linkage!
EOS
end
return no_dependents
end
formulae_to_install = formulae.reject { |f| f.core_formula? && f.versioned_formula? }
return no_dependents if formulae_to_install.empty?
already_broken = check_broken_dependents(formulae_to_install)
# TODO: this should be refactored to use FormulaInstaller new logic
outdated = formulae_to_install.flat_map(&:runtime_installed_formula_dependents)
.uniq
.select(&:outdated?)
2020-08-24 00:37:22 +02:00
# 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)
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
2020-08-24 00:37:22 +02:00
# 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
2020-08-30 01:24:42 -07:00
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
2020-08-30 01:24:42 -07:00
# 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))
2020-08-24 00:37:22 +02:00
reinstall_contexts.each do |reinstall_context|
next unless valid_formula_installers.include?(reinstall_context.formula_installer)
2020-08-24 00:37:22 +02:00
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
2020-08-24 00:37:22 +02:00
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
2020-08-24 00:37:22 +02:00
end
return
2020-08-24 00:37:22 +02:00
end
Install.install_formula(formula_installer, upgrade: true)
rescue BuildError => e
e.dump(verbose:)
puts
Homebrew.failed = true
2020-08-24 00:37:22 +02:00
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
2021-03-18 14:46:48 +00:00
end
2020-08-24 00:37:22 +02:00
sig { void }
def puts_no_installed_dependents_check_disable_message_if_not_already!
return if Homebrew::EnvConfig.no_env_hints?
return if Homebrew::EnvConfig.no_installed_dependents_check?
return if @puts_no_installed_dependents_check_disable_message_if_not_already
puts "Disable this behaviour by setting `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1`."
puts "Hide these hints with `HOMEBREW_NO_ENV_HINTS=1` (see `man brew`)."
@puts_no_installed_dependents_check_disable_message_if_not_already = T.let(true, T.nilable(T::Boolean))
end
sig {
params(formula: Formula, flags: T::Array[String], force_bottle: T::Boolean,
build_from_source_formulae: T::Array[String], interactive: T::Boolean,
keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean,
overwrite: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean).returns(FormulaInstaller)
}
def create_formula_installer(
formula,
flags:,
force_bottle: false,
build_from_source_formulae: [],
interactive: false,
keep_tmp: false,
debug_symbols: false,
force: false,
overwrite: false,
debug: false,
quiet: false,
verbose: false
)
keg = if formula.optlinked?
Keg.new(formula.opt_prefix.resolved_path)
2020-08-24 00:37:22 +02:00
else
formula.installed_kegs.find(&:optlinked?)
2020-08-24 00:37:22 +02:00
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
2020-08-24 00:37:22 +02:00
build_options = BuildOptions.new(Options.create(flags), formula.options)
options = build_options.used_options
options |= formula.build.used_options
options &= formula.options
2021-03-18 14:46:48 +00:00
FormulaInstaller.new(
2021-03-18 14:46:48 +00:00
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,
2025-06-09 00:14:16 -04:00
)
2020-08-24 00:37:22 +02:00
end
sig { params(one: Formula, two: Formula).returns(Integer) }
def depends_on(one, two)
if one.any_installed_keg
&.runtime_dependencies
&.any? { |dependency| dependency["full_name"] == two.full_name }
1
else
T.must(one <=> two)
end
2020-08-24 00:37:22 +02:00
end
end
end
end