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