diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb index e00109bac0..92b3beff47 100644 --- a/Library/Homebrew/cmd/uninstall.rb +++ b/Library/Homebrew/cmd/uninstall.rb @@ -6,6 +6,7 @@ require "keg" require "formula" +require "diagnostic" require "migrator" module Homebrew @@ -75,17 +76,45 @@ module Homebrew end def check_for_dependents(kegs) + return false unless result = find_some_installed_dependents(kegs) + + requireds, dependents = result + + msg = "Refusing to uninstall #{requireds.join(", ")} because " + msg << (requireds.count == 1 ? "it is" : "they are") + msg << " required by #{dependents.join(", ")}, which " + msg << (dependents.count == 1 ? "is" : "are") + msg << " currently installed." + ofail msg + print "You can override this and force removal with " + puts "`brew uninstall --ignore-dependencies #{requireds.map(&:name).join(" ")}`." + + true + end + + # Will return some kegs, and some dependencies, if they're present. + # For efficiency, we don't bother trying to get complete data. + def find_some_installed_dependents(kegs) kegs.each do |keg| dependents = keg.installed_dependents - kegs - next if dependents.empty? - - dependents_output = dependents.map { |k| "#{k.name} #{k.version}" }.join(", ") - conjugation = dependents.count == 1 ? "is" : "are" - ofail "Refusing to uninstall #{keg} because it is required by #{dependents_output}, which #{conjugation} currently installed." - puts "You can override this and force removal with `brew uninstall --ignore-dependencies #{keg.name}`." - return true + dependents.map! { |d| "#{d.name} #{d.version}" } + return [keg], dependents if dependents.any? end - false + + # Find formulae that didn't have dependencies saved in all of their kegs, + # so need them to be calculated now. + # + # This happens after the initial dependency check because it's sloooow. + remaining_formulae = Formula.installed.select { |f| + f.installed_kegs.any? { |k| Tab.for_keg(k).runtime_dependencies.nil? } + } + Diagnostic.missing_deps(remaining_formulae, kegs.map(&:name)) do |dependent, required| + kegs_by_name = kegs.group_by(&:to_formula) + required_kegs = required.map { |f| kegs_by_name[f].sort_by(&:version).last } + return required_kegs, [dependent] + end + + nil end def rm_pin(rack) diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 257594671a..16717ea4ed 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -298,7 +298,9 @@ class Keg def installed_dependents Formula.installed.flat_map(&:installed_kegs).select do |keg| - Tab.for_keg(keg).runtime_dependencies.any? do |dep| + tab = Tab.for_keg(keg) + next if tab.runtime_dependencies.nil? # no dependency information saved. + tab.runtime_dependencies.any? do |dep| # Resolve formula rather than directly comparing names # in case of conflicts between formulae from different taps. dep_formula = Formulary.factory(dep["full_name"]) diff --git a/Library/Homebrew/test/test_uninstall.rb b/Library/Homebrew/test/test_uninstall.rb index ede1e321b3..c36a14477c 100644 --- a/Library/Homebrew/test/test_uninstall.rb +++ b/Library/Homebrew/test/test_uninstall.rb @@ -43,6 +43,24 @@ class IntegrationCommandTestUninstall < IntegrationCommandTestCase end end + def test_uninstall_leaving_dependents_no_runtime_dependencies_in_tab + cmd("install", "testball_f2") + + f2_keg = f2.installed_kegs.first + f2_tab = Tab.for_keg(f2_keg) + f2_tab.runtime_dependencies = nil + f2_tab.write + + run_as_not_developer do + assert_match "Refusing to uninstall", + cmd_fail("uninstall", "testball_f1") + refute_empty f1.installed_kegs + assert_match "Uninstalling #{f2.rack}", + cmd("uninstall", "testball_f2") + assert_empty f2.installed_kegs + end + end + def test_uninstall_force_leaving_dependents cmd("install", "testball_f2") run_as_not_developer do