From d44290571931b5c83934b31d97b58b8e2bc32c06 Mon Sep 17 00:00:00 2001 From: "L. E. Segovia" <13498015+amyspark@users.noreply.github.com> Date: Wed, 12 Sep 2018 19:28:02 +0000 Subject: [PATCH] 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. --- Library/Homebrew/cmd/reinstall.rb | 56 +----- Library/Homebrew/cmd/upgrade.rb | 197 +++++++++++++++++++++- Library/Homebrew/reinstall.rb | 63 +++++++ Library/Homebrew/test/cmd/upgrade_spec.rb | 10 ++ 4 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 Library/Homebrew/reinstall.rb diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index fac1a3096d..8f73846cb0 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -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 diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index f170d1f220..858ca47e10 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -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 diff --git a/Library/Homebrew/reinstall.rb b/Library/Homebrew/reinstall.rb new file mode 100644 index 0000000000..33f7c37b6a --- /dev/null +++ b/Library/Homebrew/reinstall.rb @@ -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 diff --git a/Library/Homebrew/test/cmd/upgrade_spec.rb b/Library/Homebrew/test/cmd/upgrade_spec.rb index 10d5386a11..cb6934d4c3 100644 --- a/Library/Homebrew/test/cmd/upgrade_spec.rb +++ b/Library/Homebrew/test/cmd/upgrade_spec.rb @@ -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