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:
L. E. Segovia 2018-09-12 19:28:02 +00:00
parent da52ba68e9
commit d442905719
No known key found for this signature in database
GPG Key ID: D5D1DC48B52B7AD5
4 changed files with 270 additions and 56 deletions

View File

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

View File

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

View 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

View File

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