mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
Upgrade: implement linkage repair
After upgrading existing kegs, we now search and upgrade their dependents as well. If any are detected that have broken linkage, they are reinstalled from source. If there are any formulae in the dependents tree that are pinned, they are only reinstalled if they're not outdated; in all cases, a suitable message is printed detailing the kegs that will be acted upon.
This commit is contained in:
parent
da52ba68e9
commit
d442905719
@ -7,6 +7,7 @@
|
||||
require "formula_installer"
|
||||
require "development_tools"
|
||||
require "messages"
|
||||
require "reinstall"
|
||||
|
||||
module Homebrew
|
||||
module_function
|
||||
@ -26,59 +27,4 @@ module Homebrew
|
||||
end
|
||||
Homebrew.messages.display_messages
|
||||
end
|
||||
|
||||
def reinstall_formula(f)
|
||||
if f.opt_prefix.directory?
|
||||
keg = Keg.new(f.opt_prefix.resolved_path)
|
||||
keg_had_linked_opt = true
|
||||
keg_was_linked = keg.linked?
|
||||
backup keg
|
||||
end
|
||||
|
||||
build_options = BuildOptions.new(Options.create(ARGV.flags_only), f.options)
|
||||
options = build_options.used_options
|
||||
options |= f.build.used_options
|
||||
options &= f.options
|
||||
|
||||
fi = FormulaInstaller.new(f)
|
||||
fi.options = options
|
||||
fi.invalid_option_names = build_options.invalid_option_names
|
||||
fi.build_bottle = ARGV.build_bottle? || (!f.bottled? && f.build.bottle?)
|
||||
fi.interactive = ARGV.interactive?
|
||||
fi.git = ARGV.git?
|
||||
fi.link_keg ||= keg_was_linked if keg_had_linked_opt
|
||||
fi.prelude
|
||||
|
||||
oh1 "Reinstalling #{Formatter.identifier(f.full_name)} #{options.to_a.join " "}"
|
||||
|
||||
fi.install
|
||||
fi.finish
|
||||
rescue FormulaInstallationAlreadyAttemptedError
|
||||
nil
|
||||
rescue Exception # rubocop:disable Lint/RescueException
|
||||
ignore_interrupts { restore_backup(keg, keg_was_linked) }
|
||||
raise
|
||||
else
|
||||
backup_path(keg).rmtree if backup_path(keg).exist?
|
||||
end
|
||||
|
||||
def backup(keg)
|
||||
keg.unlink
|
||||
keg.rename backup_path(keg)
|
||||
end
|
||||
|
||||
def restore_backup(keg, keg_was_linked)
|
||||
path = backup_path(keg)
|
||||
|
||||
return unless path.directory?
|
||||
|
||||
Pathname.new(keg).rmtree if keg.exist?
|
||||
|
||||
path.rename keg
|
||||
keg.link if keg_was_linked
|
||||
end
|
||||
|
||||
def backup_path(path)
|
||||
Pathname.new "#{path}.reinstall"
|
||||
end
|
||||
end
|
||||
|
@ -21,6 +21,7 @@
|
||||
#: are pinned; see `pin`, `unpin`).
|
||||
|
||||
require "install"
|
||||
require "reinstall"
|
||||
require "formula_installer"
|
||||
require "cleanup"
|
||||
require "development_tools"
|
||||
@ -80,6 +81,16 @@ module Homebrew
|
||||
puts formulae_upgrades.join(", ")
|
||||
end
|
||||
|
||||
upgrade_formulae(formulae_to_install)
|
||||
|
||||
check_dependents(formulae_to_install)
|
||||
|
||||
Homebrew.messages.display_messages
|
||||
end
|
||||
|
||||
def upgrade_formulae(formulae_to_install)
|
||||
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|
|
||||
@ -104,7 +115,6 @@ module Homebrew
|
||||
onoe "#{f}: #{e}"
|
||||
end
|
||||
end
|
||||
Homebrew.messages.display_messages
|
||||
end
|
||||
|
||||
def upgrade_formula(f)
|
||||
@ -171,4 +181,189 @@ module Homebrew
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def upgradable_dependents(kegs, formulae)
|
||||
formulae_to_upgrade = Set.new
|
||||
formulae_pinned = Set.new
|
||||
|
||||
formulae.each do |formula|
|
||||
descendants = Set.new
|
||||
|
||||
dependents = kegs.select do |keg|
|
||||
keg.runtime_dependencies
|
||||
.any? { |d| d["full_name"] == formula.full_name }
|
||||
end
|
||||
|
||||
next if dependents.empty?
|
||||
|
||||
dependent_formulae = dependents.map(&:to_formula)
|
||||
|
||||
dependent_formulae.each do |f|
|
||||
next if formulae_to_upgrade.include?(f)
|
||||
next if formulae_pinned.include?(f)
|
||||
|
||||
if f.outdated?(fetch_head: ARGV.fetch_head?)
|
||||
if f.pinned?
|
||||
formulae_pinned << f
|
||||
else
|
||||
formulae_to_upgrade << f
|
||||
end
|
||||
end
|
||||
|
||||
descendants << f
|
||||
end
|
||||
|
||||
upgradable_descendants, pinned_descendants = upgradable_dependents(kegs, descendants)
|
||||
|
||||
formulae_to_upgrade.merge upgradable_descendants
|
||||
formulae_pinned.merge pinned_descendants
|
||||
end
|
||||
|
||||
[formulae_to_upgrade, formulae_pinned]
|
||||
end
|
||||
|
||||
def broken_dependents(kegs, formulae)
|
||||
formulae_to_reinstall = Set.new
|
||||
formulae_pinned_and_outdated = Set.new
|
||||
|
||||
CacheStoreDatabase.use(:linkage) do |db|
|
||||
formulae.each do |formula|
|
||||
descendants = Set.new
|
||||
|
||||
dependents = kegs.select do |keg|
|
||||
keg.runtime_dependencies
|
||||
.any? { |d| d["full_name"] == formula.full_name }
|
||||
end
|
||||
|
||||
next if dependents.empty?
|
||||
|
||||
dependents.each do |keg|
|
||||
f = keg.to_formula
|
||||
|
||||
next if formulae_to_reinstall.include?(f)
|
||||
next if formulae_pinned_and_outdated.include?(f)
|
||||
|
||||
checker = LinkageChecker.new(keg, cache_db: db)
|
||||
|
||||
if checker.broken_library_linkage?
|
||||
if f.outdated?(fetch_head: ARGV.fetch_head?)
|
||||
# Outdated formulae = pinned formulae (see function above)
|
||||
formulae_pinned_and_outdated << f
|
||||
else
|
||||
formulae_to_reinstall << f
|
||||
end
|
||||
end
|
||||
|
||||
descendants << f
|
||||
end
|
||||
|
||||
descendants_to_reinstall, descendants_pinned = broken_dependents(kegs, descendants)
|
||||
|
||||
formulae_to_reinstall.merge descendants_to_reinstall
|
||||
formulae_pinned_and_outdated.merge descendants_pinned
|
||||
end
|
||||
end
|
||||
|
||||
[formulae_to_reinstall, formulae_pinned_and_outdated]
|
||||
end
|
||||
|
||||
# @private
|
||||
def depends_on(a, b)
|
||||
if a.opt_or_installed_prefix_keg
|
||||
.runtime_dependencies
|
||||
.any? { |d| d["full_name"] == b.full_name }
|
||||
1
|
||||
else
|
||||
a <=> b
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def formulae_with_runtime_dependencies
|
||||
Formula.installed
|
||||
.map(&:opt_or_installed_prefix_keg)
|
||||
.reject(&:nil?)
|
||||
.reject { |f| f.runtime_dependencies.to_a.empty? }
|
||||
end
|
||||
|
||||
def check_dependents(formulae)
|
||||
return if formulae.empty?
|
||||
|
||||
# First find all the outdated dependents.
|
||||
kegs = formulae_with_runtime_dependencies
|
||||
|
||||
return if kegs.empty?
|
||||
|
||||
oh1 "Checking dependents for outdated formulae" if ARGV.verbose?
|
||||
upgradable, pinned = upgradable_dependents(kegs, formulae).map(&:to_a)
|
||||
|
||||
upgradable.sort! { |a, b| depends_on(a, b) }
|
||||
|
||||
pinned.sort! { |a, b| depends_on(a, b) }
|
||||
|
||||
# Print the pinned dependents.
|
||||
unless pinned.empty?
|
||||
ohai "Not upgrading #{Formatter.pluralize(pinned.length, "pinned dependent")}:"
|
||||
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
|
||||
end
|
||||
|
||||
# Print the upgradable dependents.
|
||||
if upgradable.empty?
|
||||
ohai "No dependents to upgrade" if ARGV.verbose?
|
||||
else
|
||||
ohai "Upgrading #{Formatter.pluralize(upgradable.length, "dependent")}:"
|
||||
formulae_upgrades = upgradable.map do |f|
|
||||
if f.optlinked?
|
||||
"#{f.full_specified_name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
|
||||
else
|
||||
"#{f.full_specified_name} #{f.pkg_version}"
|
||||
end
|
||||
end
|
||||
puts formulae_upgrades.join(", ")
|
||||
end
|
||||
|
||||
upgrade_formulae(upgradable)
|
||||
|
||||
# Assess the dependents tree again.
|
||||
kegs = formulae_with_runtime_dependencies
|
||||
|
||||
oh1 "Checking dependents for broken library links" if ARGV.verbose?
|
||||
reinstallable, pinned = broken_dependents(kegs, formulae).map(&:to_a)
|
||||
|
||||
reinstallable.sort! { |a, b| depends_on(a, b) }
|
||||
|
||||
pinned.sort! { |a, b| depends_on(a, b) }
|
||||
|
||||
# Print the pinned dependents.
|
||||
unless pinned.empty?
|
||||
onoe "Not reinstalling #{Formatter.pluralize(pinned.length, "broken and outdated, but pinned dependent")}:"
|
||||
$stderr.puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
|
||||
end
|
||||
|
||||
# Print the broken dependents.
|
||||
if reinstallable.empty?
|
||||
ohai "No broken dependents to reinstall" if ARGV.verbose?
|
||||
else
|
||||
ohai "Reinstalling #{Formatter.pluralize(reinstallable.length, "broken dependent")} from source:"
|
||||
puts reinstallable.map(&:full_specified_name).join(", ")
|
||||
end
|
||||
|
||||
reinstallable.each do |f|
|
||||
begin
|
||||
reinstall_formula(f, build_from_source: true)
|
||||
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 => e
|
||||
ofail e
|
||||
rescue BuildError => e
|
||||
e.dump
|
||||
puts
|
||||
Homebrew.failed = true
|
||||
rescue DownloadError => e
|
||||
ofail e
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
63
Library/Homebrew/reinstall.rb
Normal file
63
Library/Homebrew/reinstall.rb
Normal file
@ -0,0 +1,63 @@
|
||||
require "formula_installer"
|
||||
require "development_tools"
|
||||
require "messages"
|
||||
|
||||
module Homebrew
|
||||
module_function
|
||||
|
||||
def reinstall_formula(f, build_from_source: false)
|
||||
if f.opt_prefix.directory?
|
||||
keg = Keg.new(f.opt_prefix.resolved_path)
|
||||
keg_had_linked_opt = true
|
||||
keg_was_linked = keg.linked?
|
||||
backup keg
|
||||
end
|
||||
|
||||
build_options = BuildOptions.new(Options.create(ARGV.flags_only), f.options)
|
||||
options = build_options.used_options
|
||||
options |= f.build.used_options
|
||||
options &= f.options
|
||||
|
||||
fi = FormulaInstaller.new(f)
|
||||
fi.options = options
|
||||
fi.invalid_option_names = build_options.invalid_option_names
|
||||
fi.build_bottle = ARGV.build_bottle? || (!f.bottled? && f.build.bottle?)
|
||||
fi.interactive = ARGV.interactive?
|
||||
fi.git = ARGV.git?
|
||||
fi.link_keg ||= keg_was_linked if keg_had_linked_opt
|
||||
fi.build_from_source = true if build_from_source
|
||||
fi.prelude
|
||||
|
||||
oh1 "Reinstalling #{Formatter.identifier(f.full_name)} #{options.to_a.join " "}"
|
||||
|
||||
fi.install
|
||||
fi.finish
|
||||
rescue FormulaInstallationAlreadyAttemptedError
|
||||
nil
|
||||
rescue Exception # rubocop:disable Lint/RescueException
|
||||
ignore_interrupts { restore_backup(keg, keg_was_linked) }
|
||||
raise
|
||||
else
|
||||
backup_path(keg).rmtree if backup_path(keg).exist?
|
||||
end
|
||||
|
||||
def backup(keg)
|
||||
keg.unlink
|
||||
keg.rename backup_path(keg)
|
||||
end
|
||||
|
||||
def restore_backup(keg, keg_was_linked)
|
||||
path = backup_path(keg)
|
||||
|
||||
return unless path.directory?
|
||||
|
||||
Pathname.new(keg).rmtree if keg.exist?
|
||||
|
||||
path.rename keg
|
||||
keg.link if keg_was_linked
|
||||
end
|
||||
|
||||
def backup_path(path)
|
||||
Pathname.new "#{path}.reinstall"
|
||||
end
|
||||
end
|
@ -7,4 +7,14 @@ describe "brew upgrade", :integration_test do
|
||||
|
||||
expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
|
||||
end
|
||||
|
||||
it "upgrades a Formula and cleans up old versions" do
|
||||
setup_test_formula "testball"
|
||||
(HOMEBREW_CELLAR/"testball/0.0.1/foo").mkpath
|
||||
|
||||
expect { brew "upgrade", "--cleanup" }.to be_a_success
|
||||
|
||||
expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
|
||||
expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user