brew/Library/Homebrew/upgrade.rb

493 lines
16 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "reinstall"
require "formula_installer"
require "development_tools"
require "messages"
require "cleanup"
require "utils/topological_hash"
module Homebrew
2020-08-24 00:37:22 +02:00
# Helper functions for upgrading formulae.
module Upgrade
Dependents = Struct.new(:upgradeable, :pinned, :skipped)
2025-06-11 22:35:00 -04:00
def self.formulae_installer(
2021-03-18 14:46:48 +00:00
formulae_to_install,
flags:,
dry_run: false,
2021-03-18 14:46:48 +00:00
force_bottle: false,
build_from_source_formulae: [],
dependents: false,
2021-03-18 14:46:48 +00:00
interactive: false,
keep_tmp: false,
debug_symbols: false,
2021-03-18 14:46:48 +00:00
force: false,
overwrite: false,
2021-03-18 14:46:48 +00:00
debug: false,
quiet: false,
verbose: false
)
2020-08-24 00:37:22 +02:00
return if formulae_to_install.empty?
# Sort keg-only before non-keg-only formulae to avoid any needless conflicts
# with outdated, non-keg-only versions of formulae being upgraded.
formulae_to_install.sort! do |a, b|
if !a.keg_only? && b.keg_only?
1
elsif a.keg_only? && !b.keg_only?
-1
else
0
end
end
dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install)
begin
formulae_to_install = dependency_graph.tsort & formulae_to_install
rescue TSort::Cyclic
raise CyclicDependencyError, dependency_graph.strongly_connected_components if Homebrew::EnvConfig.developer?
end
formulae_to_install.filter_map do |formula|
2024-03-07 16:20:20 +00:00
Migrator.migrate_if_needed(formula, force:, dry_run:)
2020-08-24 00:37:22 +02:00
begin
fi = create_formula_installer(
2021-03-18 14:46:48 +00:00
formula,
2024-03-07 16:20:20 +00:00
flags:,
force_bottle:,
build_from_source_formulae:,
interactive:,
keep_tmp:,
debug_symbols:,
force:,
overwrite:,
2024-03-07 16:20:20 +00:00
debug:,
quiet:,
verbose:,
2021-03-18 14:46:48 +00:00
)
if !dry_run && dependents && 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
# 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
2021-03-18 14:46:48 +00:00
ofail "#{formula}: #{e}"
nil
2020-08-24 00:37:22 +02:00
end
end
end
def self.upgrade_formulae(formula_installers, dry_run: false, verbose: false)
formula_installers.each do |fi|
begin
fi.prelude
fi.fetch
rescue CannotInstallFormulaError => e
ofail e
rescue UnsatisfiedRequirements, DownloadError => e
2025-06-11 22:35:00 -04:00
ofail "#{fi.formula.full_name}: #{e}"
end
2024-03-07 16:20:20 +00:00
upgrade_formula(fi, dry_run:, verbose:)
Cleanup.install_formula_clean!(fi.formula, dry_run:)
end
end
2024-08-20 15:33:08 +01:00
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
2024-08-20 15:33:08 +01:00
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
2024-08-20 15:33:08 +01:00
private_class_method def self.create_formula_installer(
2021-03-18 14:46:48 +00:00
formula,
flags:,
force_bottle: false,
build_from_source_formulae: [],
interactive: false,
keep_tmp: false,
debug_symbols: false,
2021-03-18 14:46:48 +00:00
force: false,
overwrite: false,
2021-03-18 14:46:48 +00:00
debug: false,
quiet: false,
verbose: false
)
keg = if formula.optlinked?
Keg.new(formula.opt_prefix.resolved_path)
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
2021-03-18 14:46:48 +00:00
build_options = BuildOptions.new(Options.create(flags), formula.options)
2020-08-24 00:37:22 +02:00
options = build_options.used_options
2021-03-18 14:46:48 +00:00
options |= formula.build.used_options
options &= formula.options
2020-08-24 00:37:22 +02:00
FormulaInstaller.new(
2021-03-18 14:46:48 +00:00
formula,
2020-11-18 05:37:12 +01:00
**{
2024-03-07 16:20:20 +00:00
options:,
link_keg:,
installed_as_dependency:,
installed_on_request:,
build_bottle:,
2024-03-07 16:20:20 +00:00
force_bottle:,
build_from_source_formulae:,
interactive:,
keep_tmp:,
debug_symbols:,
force:,
overwrite:,
2024-03-07 16:20:20 +00:00
debug:,
quiet:,
verbose:,
2020-11-18 05:37:12 +01:00
}.compact,
)
end
2024-08-20 15:33:08 +01:00
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
2024-03-07 16:20:20 +00:00
e.dump(verbose:)
puts
Homebrew.failed = true
end
2024-08-20 15:33:08 +01:00
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
2020-08-24 00:37:22 +02:00
# 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
2020-08-24 00:37:22 +02:00
# 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
2020-08-24 00:37:22 +02:00
ensure
# restore previous installation state if build failed
begin
2023-02-14 08:48:00 -08:00
linked_kegs&.each(&:link) unless formula.latest_version_installed?
2020-08-24 00:37:22 +02:00
rescue
nil
end
end
2020-08-24 00:37:22 +02:00
2024-08-20 15:33:08 +01:00
private_class_method def self.check_broken_dependents(installed_formulae)
2020-08-30 01:24:42 -07:00
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
2020-08-30 01:24:42 -07:00
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
2025-06-11 22:35:00 -04:00
def self.dependants(
2021-03-18 14:46:48 +00:00
formulae,
flags:,
dry_run: false,
ask: false,
2021-03-18 14:46:48 +00:00
installed_on_request: false,
force_bottle: false,
build_from_source_formulae: [],
interactive: false,
keep_tmp: false,
debug_symbols: false,
2021-03-18 14:46:48 +00:00
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
installed_formulae = formulae.dup
installed_formulae.reject! { |f| f.core_formula? && f.versioned_formula? }
2020-08-30 01:24:42 -07:00
return if installed_formulae.empty?
already_broken_dependents = check_broken_dependents(installed_formulae)
2023-09-03 09:18:58 -04:00
# TODO: this should be refactored to use FormulaInstaller new logic
2020-08-24 00:37:22 +02:00
outdated_dependents =
installed_formulae.flat_map(&:runtime_installed_formula_dependents)
.uniq
2020-08-24 00:37:22 +02:00
.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?
2020-08-24 00:37:22 +02:00
2021-03-18 14:46:48 +00:00
outdated_dependents -= installed_formulae if dry_run
2020-08-24 00:37:22 +02:00
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|
2020-08-24 00:37:22 +02:00
"#{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
2020-08-24 00:37:22 +02:00
# Print the upgradable dependents.
if upgradeable.blank?
2021-03-18 14:46:48 +00:00
ohai "No outdated dependents to upgrade!" unless dry_run
2020-08-24 00:37:22 +02:00
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|
2020-08-24 00:37:22 +02:00
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
2021-03-18 14:46:48 +00:00
unless dry_run
2025-06-11 22:35:00 -04:00
formulae_dependencies = formulae_installer(
upgradeable,
2024-03-07 16:20:20 +00:00
flags:,
force_bottle:,
build_from_source_formulae:,
dependents: true,
2024-03-07 16:20:20 +00:00
interactive:,
keep_tmp:,
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
2021-03-18 14:46:48 +00:00
)
upgrade_formulae formulae_dependencies
2021-03-18 14:46:48 +00:00
end
2020-08-24 00:37:22 +02:00
# 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..."
Upgrade.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)
2020-08-24 00:37:22 +02:00
if broken_dependents.blank?
2021-03-18 14:46:48 +00:00
if dry_run
2020-08-24 00:37:22 +02:00
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)
2020-08-24 00:37:22 +02:00
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
2023-03-19 23:17:15 -04:00
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!
2020-08-24 00:37:22 +02:00
puts reinstallable_broken_dependents.map(&:full_specified_name)
.join(", ")
end
2021-03-18 14:46:48 +00:00
return if dry_run
reinstallable_broken_dependents.each do |formula|
2025-06-11 22:35:00 -04:00
formula_installer = Reinstall.formula_installer(
2021-03-18 14:46:48 +00:00
formula,
2024-03-07 16:20:20 +00:00
flags:,
force_bottle:,
build_from_source_formulae: build_from_source_formulae + [formula.full_name],
2024-03-07 16:20:20 +00:00
interactive:,
keep_tmp:,
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
2021-03-18 14:46:48 +00:00
)
2025-06-09 00:14:16 -04:00
Reinstall.reinstall_formula(
formula_installer,
2025-06-09 00:42:24 -04:00
flags:,
force_bottle:,
build_from_source_formulae:,
interactive:,
keep_tmp:,
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
2025-06-09 00:14:16 -04:00
)
2020-08-24 00:37:22 +02:00
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
2020-11-13 10:07:02 -05:00
rescue CannotInstallFormulaError, DownloadError => e
2020-08-24 00:37:22 +02:00
ofail e
rescue BuildError => e
2024-03-07 16:20:20 +00:00
e.dump(verbose:)
2020-08-24 00:37:22 +02:00
puts
Homebrew.failed = true
end
end
2024-08-20 15:33:08 +01:00
private_class_method def self.depends_on(one, two)
if one.any_installed_keg
&.runtime_dependencies
&.any? { |dependency| dependency["full_name"] == two.full_name }
2020-08-24 00:37:22 +02:00
1
else
one <=> two
2020-08-24 00:37:22 +02:00
end
end
end
end