diff --git a/Library/Homebrew/cmd/missing.rb b/Library/Homebrew/cmd/missing.rb
index 148fe5bef0..8a1dc506df 100644
--- a/Library/Homebrew/cmd/missing.rb
+++ b/Library/Homebrew/cmd/missing.rb
@@ -1,6 +1,9 @@
-#: * `missing` []:
+#: * `missing` [`--hide=`] []:
#: Check the given for missing dependencies. If no are
#: given, check all installed brews.
+#:
+#: If `--hide=` is passed, act as if none of are installed.
+#: should be a comma-separated list of formulae.
require "formula"
require "tab"
@@ -18,8 +21,11 @@ module Homebrew
ARGV.resolved_formulae
end
- Diagnostic.missing_deps(ff) do |name, missing|
- print "#{name}: " if ff.size > 1
+ ff.each do |f|
+ missing = f.missing_dependencies(hide: ARGV.values("hide"))
+ next if missing.empty?
+
+ print "#{f}: " if ff.size > 1
puts missing.join(" ")
end
end
diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb
index 8bcfc31fbb..d4a64c5055 100644
--- a/Library/Homebrew/cmd/uninstall.rb
+++ b/Library/Homebrew/cmd/uninstall.rb
@@ -1,11 +1,15 @@
-#: * `uninstall`, `rm`, `remove` [`--force`] :
+#: * `uninstall`, `rm`, `remove` [`--force`] [`--ignore-dependencies`] :
#: Uninstall .
#:
#: If `--force` is passed, and there are multiple versions of
#: installed, delete all installed versions.
+#:
+#: If `--ignore-dependencies` is passed, uninstalling won't fail, even if
+#: formulae depending on would still be installed.
require "keg"
require "formula"
+require "diagnostic"
require "migrator"
module Homebrew
@@ -14,38 +18,50 @@ module Homebrew
def uninstall
raise KegUnspecifiedError if ARGV.named.empty?
- if !ARGV.force?
- ARGV.kegs.each do |keg|
- keg.lock do
- puts "Uninstalling #{keg}... (#{keg.abv})"
- keg.unlink
- keg.uninstall
- rack = keg.rack
- rm_pin rack
-
- if rack.directory?
- versions = rack.subdirs.map(&:basename)
- verb = versions.length == 1 ? "is" : "are"
- puts "#{keg.name} #{versions.join(", ")} #{verb} still installed."
- puts "Remove all versions with `brew uninstall --force #{keg.name}`."
- end
- end
- end
- else
- ARGV.named.each do |name|
+ kegs_by_rack = if ARGV.force?
+ Hash[ARGV.named.map do |name|
rack = Formulary.to_rack(name)
+ [rack, rack.subdirs.map { |d| Keg.new(d) }]
+ end]
+ else
+ ARGV.kegs.group_by(&:rack)
+ end
+
+ if should_check_for_dependents?
+ all_kegs = kegs_by_rack.values.flatten(1)
+ return if check_for_dependents all_kegs
+ end
+
+ kegs_by_rack.each do |rack, kegs|
+ if ARGV.force?
name = rack.basename
if rack.directory?
puts "Uninstalling #{name}... (#{rack.abv})"
- rack.subdirs.each do |d|
- keg = Keg.new(d)
+ kegs.each do |keg|
keg.unlink
keg.uninstall
end
end
rm_pin rack
+ else
+ kegs.each do |keg|
+ keg.lock do
+ puts "Uninstalling #{keg}... (#{keg.abv})"
+ keg.unlink
+ keg.uninstall
+ rack = keg.rack
+ rm_pin rack
+
+ if rack.directory?
+ versions = rack.subdirs.map(&:basename)
+ verb = versions.length == 1 ? "is" : "are"
+ puts "#{keg.name} #{versions.join(", ")} #{verb} still installed."
+ puts "Remove all versions with `brew uninstall --force #{keg.name}`."
+ end
+ end
+ end
end
end
rescue MultipleVersionsInstalledError => e
@@ -61,6 +77,30 @@ module Homebrew
end
end
+ def should_check_for_dependents?
+ # --ignore-dependencies, to be consistent with install
+ return false if ARGV.include?("--ignore-dependencies")
+ return false if ARGV.homebrew_developer?
+ true
+ end
+
+ def check_for_dependents(kegs)
+ return false unless result = Keg.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
+
def rm_pin(rack)
Formulary.from_rack(rack).unpin
rescue
diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb
index e499c4d3b8..216b298fc9 100644
--- a/Library/Homebrew/diagnostic.rb
+++ b/Library/Homebrew/diagnostic.rb
@@ -7,24 +7,14 @@ require "utils/shell"
module Homebrew
module Diagnostic
- def self.missing_deps(ff)
+ def self.missing_deps(ff, hide = nil)
missing = {}
ff.each do |f|
- missing_deps = f.recursive_dependencies do |dependent, dep|
- if dep.optional? || dep.recommended?
- tab = Tab.for_formula(dependent)
- Dependency.prune unless tab.with?(dep)
- elsif dep.build?
- Dependency.prune
- end
- end
+ missing_dependencies = f.missing_dependencies(hide: hide)
- missing_deps.map!(&:to_formula)
- missing_deps.reject! { |d| d.installed_prefixes.any? }
-
- unless missing_deps.empty?
- yield f.full_name, missing_deps if block_given?
- missing[f.full_name] = missing_deps
+ unless missing_dependencies.empty?
+ yield f.full_name, missing_dependencies if block_given?
+ missing[f.full_name] = missing_dependencies
end
end
missing
diff --git a/Library/Homebrew/extend/ARGV.rb b/Library/Homebrew/extend/ARGV.rb
index 0adf8d548b..bd60cbeccb 100644
--- a/Library/Homebrew/extend/ARGV.rb
+++ b/Library/Homebrew/extend/ARGV.rb
@@ -121,6 +121,13 @@ module HomebrewArgvExtension
flag_with_value.strip_prefix(arg_prefix) if flag_with_value
end
+ # Returns an array of values that were given as a comma-seperated list.
+ # @see value
+ def values(name)
+ return unless val = value(name)
+ val.split(",")
+ end
+
def force?
flag? "--force"
end
diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb
index ab05548a8e..5434d87c2e 100644
--- a/Library/Homebrew/formula.rb
+++ b/Library/Homebrew/formula.rb
@@ -1337,6 +1337,13 @@ class Formula
end
end
+ # Clear caches of .racks and .installed.
+ # @private
+ def self.clear_cache
+ @racks = nil
+ @installed = nil
+ end
+
# An array of all racks currently installed.
# @private
def self.racks
@@ -1459,6 +1466,26 @@ class Formula
recursive_dependencies.reject(&:build?)
end
+ # Returns a list of formulae depended on by this formula that aren't
+ # installed
+ def missing_dependencies(hide: nil)
+ hide ||= []
+ missing_dependencies = recursive_dependencies do |dependent, dep|
+ if dep.optional? || dep.recommended?
+ tab = Tab.for_formula(dependent)
+ Dependency.prune unless tab.with?(dep)
+ elsif dep.build?
+ Dependency.prune
+ end
+ end
+
+ missing_dependencies.map!(&:to_formula)
+ missing_dependencies.select! do |d|
+ hide.include?(d.name) || d.installed_prefixes.empty?
+ end
+ missing_dependencies
+ end
+
# @private
def to_hash
hsh = {
diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb
index d2c9e12e8c..e2719582d6 100644
--- a/Library/Homebrew/keg.rb
+++ b/Library/Homebrew/keg.rb
@@ -87,6 +87,41 @@ class Keg
mime-info pixmaps sounds postgresql
].freeze
+ # Will return some kegs, and some dependencies, if they're present.
+ # For efficiency, we don't bother trying to get complete data.
+ def self.find_some_installed_dependents(kegs)
+ # First, check in the tabs of installed Formulae.
+ kegs.each do |keg|
+ dependents = keg.installed_dependents - kegs
+ dependents.map! { |d| "#{d.name} #{d.version}" }
+ return [keg], dependents if dependents.any?
+ end
+
+ # Some kegs won't have modern Tabs with the dependencies listed.
+ # In this case, fall back to Formula#missing_dependencies.
+
+ # 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 do |f|
+ f.installed_kegs.any? { |k| Tab.for_keg(k).runtime_dependencies.nil? }
+ end
+
+ keg_names = kegs.map(&:name)
+ kegs_by_name = kegs.group_by(&:to_formula)
+ remaining_formulae.each do |dependent|
+ required = dependent.missing_dependencies(hide: keg_names)
+ required.select! { |f| kegs_by_name.key?(f) }
+ next unless required.any?
+
+ required_kegs = required.map { |f| kegs_by_name[f].sort_by(&:version).last }
+ return required_kegs, [dependent.to_s]
+ end
+
+ nil
+ end
+
# if path is a file in a keg then this will return the containing Keg object
def self.for(path)
path = path.realpath
@@ -292,6 +327,23 @@ class Keg
PkgVersion.parse(path.basename.to_s)
end
+ def to_formula
+ Formulary.from_keg(self)
+ end
+
+ def installed_dependents
+ Formula.installed.flat_map(&:installed_kegs).select do |keg|
+ 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"])
+ dep_formula == to_formula && dep["version"] == version.to_s
+ end
+ end
+ end
+
def find(*args, &block)
path.find(*args, &block)
end
diff --git a/Library/Homebrew/test/helper/integration_command_test_case.rb b/Library/Homebrew/test/helper/integration_command_test_case.rb
index 20c0fc48ea..2f137e14a3 100644
--- a/Library/Homebrew/test/helper/integration_command_test_case.rb
+++ b/Library/Homebrew/test/helper/integration_command_test_case.rb
@@ -73,10 +73,12 @@ class IntegrationCommandTestCase < Homebrew::TestCase
cmd_args << "-rintegration_mocks"
cmd_args << (HOMEBREW_LIBRARY_PATH/"brew.rb").resolved_path.to_s
cmd_args += args
+ developer = ENV["HOMEBREW_DEVELOPER"]
Bundler.with_original_env do
ENV["HOMEBREW_BREW_FILE"] = HOMEBREW_PREFIX/"bin/brew"
ENV["HOMEBREW_INTEGRATION_TEST"] = cmd_id_from_args(args)
ENV["HOMEBREW_TEST_TMPDIR"] = TEST_TMPDIR
+ ENV["HOMEBREW_DEVELOPER"] = developer
env.each_pair do |k, v|
ENV[k] = v
end
@@ -127,7 +129,6 @@ class IntegrationCommandTestCase < Homebrew::TestCase
sha256 "#{TESTBALL_SHA256}"
option "with-foo", "Build with foo"
- #{content}
def install
(prefix/"foo"/"test").write("test") if build.with? "foo"
@@ -138,6 +139,8 @@ class IntegrationCommandTestCase < Homebrew::TestCase
system ENV.cc, "test.c", "-o", bin/"test"
end
+ #{content}
+
# something here
EOS
when "foo"
diff --git a/Library/Homebrew/test/test_ARGV.rb b/Library/Homebrew/test/test_ARGV.rb
index 39f32f4523..6805e0c620 100644
--- a/Library/Homebrew/test/test_ARGV.rb
+++ b/Library/Homebrew/test/test_ARGV.rb
@@ -62,4 +62,19 @@ class ArgvExtensionTests < Homebrew::TestCase
assert !@argv.flag?("--frotz")
assert !@argv.flag?("--debug")
end
+
+ def test_value
+ @argv << "--foo=" << "--bar=ab"
+ assert_equal "", @argv.value("foo")
+ assert_equal "ab", @argv.value("bar")
+ assert_nil @argv.value("baz")
+ end
+
+ def test_values
+ @argv << "--foo=" << "--bar=a" << "--baz=b,c"
+ assert_equal [], @argv.values("foo")
+ assert_equal ["a"], @argv.values("bar")
+ assert_equal ["b", "c"], @argv.values("baz")
+ assert_nil @argv.values("qux")
+ end
end
diff --git a/Library/Homebrew/test/test_keg.rb b/Library/Homebrew/test/test_keg.rb
index 3abf3fc273..7450d9c0f1 100644
--- a/Library/Homebrew/test/test_keg.rb
+++ b/Library/Homebrew/test/test_keg.rb
@@ -5,15 +5,22 @@ require "stringio"
class LinkTests < Homebrew::TestCase
include FileUtils
- def setup
- keg = HOMEBREW_CELLAR.join("foo", "1.0")
- keg.join("bin").mkpath
+ def setup_test_keg(name, version)
+ path = HOMEBREW_CELLAR.join(name, version)
+ path.join("bin").mkpath
%w[hiworld helloworld goodbye_cruel_world].each do |file|
- touch keg.join("bin", file)
+ touch path.join("bin", file)
end
- @keg = Keg.new(keg)
+ keg = Keg.new(path)
+ @kegs ||= []
+ @kegs << keg
+ keg
+ end
+
+ def setup
+ @keg = setup_test_keg("foo", "1.0")
@dst = HOMEBREW_PREFIX.join("bin", "helloworld")
@nonexistent = Pathname.new("/some/nonexistent/path")
@@ -27,8 +34,10 @@ class LinkTests < Homebrew::TestCase
end
def teardown
- @keg.unlink
- @keg.uninstall
+ @kegs.each do |keg|
+ keg.unlink
+ keg.uninstall
+ end
$stdout = @old_stdout
@@ -305,3 +314,72 @@ class LinkTests < Homebrew::TestCase
keg.uninstall
end
end
+
+class InstalledDependantsTests < LinkTests
+ def stub_formula_name(name)
+ stub_formula_loader formula(name) { url "foo-1.0" }
+ end
+
+ def setup_test_keg(name, version)
+ stub_formula_name(name)
+ keg = super
+ Formula.clear_cache
+ keg
+ end
+
+ def setup
+ super
+ @dependent = setup_test_keg("bar", "1.0")
+ end
+
+ def alter_tab(keg = @dependent)
+ tab = Tab.for_keg(keg)
+ yield tab
+ tab.write
+ end
+
+ def dependencies(deps)
+ alter_tab do |tab|
+ tab.tabfile = @dependent.join("INSTALL_RECEIPT.json")
+ tab.runtime_dependencies = deps
+ end
+ end
+
+ def test_no_dependencies_anywhere
+ dependencies nil
+ assert_empty @keg.installed_dependents
+ assert_nil Keg.find_some_installed_dependents([@keg])
+ end
+
+ def test_missing_formula_dependency
+ dependencies nil
+ Formula["bar"].class.depends_on "foo"
+ assert_empty @keg.installed_dependents
+ assert_equal [[@keg], ["bar"]], Keg.find_some_installed_dependents([@keg])
+ end
+
+ def test_empty_dependencies_in_tab
+ dependencies []
+ assert_empty @keg.installed_dependents
+ assert_nil Keg.find_some_installed_dependents([@keg])
+ end
+
+ def test_same_name_different_version_in_tab
+ dependencies [{ "full_name" => "foo", "version" => "1.1" }]
+ assert_empty @keg.installed_dependents
+ assert_nil Keg.find_some_installed_dependents([@keg])
+ end
+
+ def test_different_name_same_version_in_tab
+ stub_formula_name("baz")
+ dependencies [{ "full_name" => "baz", "version" => @keg.version.to_s }]
+ assert_empty @keg.installed_dependents
+ assert_nil Keg.find_some_installed_dependents([@keg])
+ end
+
+ def test_same_name_and_version_in_tab
+ dependencies [{ "full_name" => "foo", "version" => "1.0" }]
+ assert_equal [@dependent], @keg.installed_dependents
+ assert_equal [[@keg], ["bar 1.0"]], Keg.find_some_installed_dependents([@keg])
+ end
+end
diff --git a/Library/Homebrew/test/test_missing.rb b/Library/Homebrew/test/test_missing.rb
index 3a5fd3df09..565f413daf 100644
--- a/Library/Homebrew/test/test_missing.rb
+++ b/Library/Homebrew/test/test_missing.rb
@@ -1,11 +1,34 @@
require "helper/integration_command_test_case"
class IntegrationCommandTestMissing < IntegrationCommandTestCase
- def test_missing
+ def setup
+ super
+
setup_test_formula "foo"
setup_test_formula "bar"
+ end
+
+ def make_prefix(name)
+ (HOMEBREW_CELLAR/name/"1.0").mkpath
+ end
+
+ def test_missing_missing
+ make_prefix "bar"
- (HOMEBREW_CELLAR/"bar/1.0").mkpath
assert_match "foo", cmd("missing")
end
+
+ def test_missing_not_missing
+ make_prefix "foo"
+ make_prefix "bar"
+
+ assert_empty cmd("missing")
+ end
+
+ def test_missing_hide
+ make_prefix "foo"
+ make_prefix "bar"
+
+ assert_match "foo", cmd("missing", "--hide=foo")
+ end
end
diff --git a/Library/Homebrew/test/test_uninstall.rb b/Library/Homebrew/test/test_uninstall.rb
index 050934238e..a7859b7ad9 100644
--- a/Library/Homebrew/test/test_uninstall.rb
+++ b/Library/Homebrew/test/test_uninstall.rb
@@ -1,4 +1,26 @@
require "helper/integration_command_test_case"
+require "cmd/uninstall"
+
+class UninstallTests < Homebrew::TestCase
+ def test_check_for_testball_f2s_when_developer
+ refute_predicate Homebrew, :should_check_for_dependents?
+ end
+
+ def test_check_for_dependents_when_not_developer
+ run_as_not_developer do
+ assert_predicate Homebrew, :should_check_for_dependents?
+ end
+ end
+
+ def test_check_for_dependents_when_ignore_dependencies
+ ARGV << "--ignore-dependencies"
+ run_as_not_developer do
+ refute_predicate Homebrew, :should_check_for_dependents?
+ end
+ ensure
+ ARGV.delete("--ignore-dependencies")
+ end
+end
class IntegrationCommandTestUninstall < IntegrationCommandTestCase
def test_uninstall
diff --git a/docs/brew.1.html b/docs/brew.1.html
index 7af76e6f09..51cb543318 100644
--- a/docs/brew.1.html
+++ b/docs/brew.1.html
@@ -245,8 +245,11 @@ packages.
If --force
is passed, then treat installed formulae and passed formulae
like if they are from same taps and migrate them anyway.
-missing
[formulae]Check the given formulae for missing dependencies. If no formulae are
-given, check all installed brews.
+missing
[--hide=
hidden] [formulae]Check the given formulae for missing dependencies. If no formulae are
+given, check all installed brews.
+
+If --hide=
hidden is passed, act as if none of hidden are installed.
+hidden should be a comma-separated list of formulae.
options
[--compact
] (--all
|--installed
|formulae)Display install options specific to formulae.
If --compact
is passed, show all options on a single line separated by
@@ -348,10 +351,13 @@ for version is v1
.
tap-pin
tapPin tap, prioritizing its formulae over core when formula names are supplied
by the user. See also tap-unpin
.
tap-unpin
tapUnpin tap so its formulae are no longer prioritized. See also tap-pin
.
-uninstall
, rm
, remove
[--force
] formulaUninstall formula.
+uninstall
, rm
, remove
[--force
] [--ignore-dependencies
] formulaUninstall formula.
If --force
is passed, and there are multiple versions of formula
-installed, delete all installed versions.
+installed, delete all installed versions.
+
+If --ignore-dependencies
is passed, uninstalling won't fail, even if
+formulae depending on formula would still be installed.
unlink
[--dry-run
] formulaRemove symlinks for formula from the Homebrew prefix. This can be useful
for temporarily disabling a formula:
brew unlink foo && commands && brew link foo
.
diff --git a/manpages/brew.1 b/manpages/brew.1
index fb528c0f41..d20d907f41 100644
--- a/manpages/brew.1
+++ b/manpages/brew.1
@@ -329,9 +329,12 @@ Migrate renamed packages to new name, where \fIformulae\fR are old names of pack
If \fB\-\-force\fR is passed, then treat installed \fIformulae\fR and passed \fIformulae\fR like if they are from same taps and migrate them anyway\.
.
.TP
-\fBmissing\fR [\fIformulae\fR]
+\fBmissing\fR [\fB\-\-hide=\fR\fIhidden\fR] [\fIformulae\fR]
Check the given \fIformulae\fR for missing dependencies\. If no \fIformulae\fR are given, check all installed brews\.
.
+.IP
+If \fB\-\-hide=\fR\fIhidden\fR is passed, act as if none of \fIhidden\fR are installed\. \fIhidden\fR should be a comma\-separated list of formulae\.
+.
.TP
\fBoptions\fR [\fB\-\-compact\fR] (\fB\-\-all\fR|\fB\-\-installed\fR|\fIformulae\fR)
Display install options specific to \fIformulae\fR\.
@@ -484,12 +487,15 @@ Pin \fItap\fR, prioritizing its formulae over core when formula names are suppli
Unpin \fItap\fR so its formulae are no longer prioritized\. See also \fBtap\-pin\fR\.
.
.TP
-\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fB\-\-force\fR] \fIformula\fR
+\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fB\-\-force\fR] [\fB\-\-ignore\-dependencies\fR] \fIformula\fR
Uninstall \fIformula\fR\.
.
.IP
If \fB\-\-force\fR is passed, and there are multiple versions of \fIformula\fR installed, delete all installed versions\.
.
+.IP
+If \fB\-\-ignore\-dependencies\fR is passed, uninstalling won\'t fail, even if formulae depending on \fIformula\fR would still be installed\.
+.
.TP
\fBunlink\fR [\fB\-\-dry\-run\fR] \fIformula\fR
Remove symlinks for \fIformula\fR from the Homebrew prefix\. This can be useful for temporarily disabling a formula: \fBbrew unlink foo && commands && brew link foo\fR\.