diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb
index 4eb4689c9b..e381088a1d 100644
--- a/Library/Homebrew/cleanup.rb
+++ b/Library/Homebrew/cleanup.rb
@@ -160,7 +160,7 @@ module Homebrew
cleanup = Cleanup.new(dry_run: dry_run)
if cleanup.periodic_clean_due?
cleanup.periodic_clean!
- elsif f.latest_version_installed? && !cleanup.skip_clean_formula?(f)
+ elsif f.latest_version_installed? && !Cleanup.skip_clean_formula?(f)
ohai "Running `brew cleanup #{f}`..."
puts_no_install_cleanup_disable_message_if_not_already!
cleanup.cleanup_formula(f)
@@ -177,7 +177,7 @@ module Homebrew
@puts_no_install_cleanup_disable_message_if_not_already = true
end
- def skip_clean_formula?(f)
+ def self.skip_clean_formula?(f)
return false if Homebrew::EnvConfig.no_cleanup_formulae.blank?
skip_clean_formulae = Homebrew::EnvConfig.no_cleanup_formulae.split(",")
@@ -215,10 +215,13 @@ module Homebrew
if args.empty?
Formula.installed
.sort_by(&:name)
- .reject { |f| skip_clean_formula?(f) }
+ .reject { |f| Cleanup.skip_clean_formula?(f) }
.each do |formula|
cleanup_formula(formula, quiet: quiet, ds_store: false, cache_db: false)
end
+
+ Cleanup.autoremove(dry_run: dry_run?) if Homebrew::EnvConfig.autoremove?
+
cleanup_cache
cleanup_logs
cleanup_lockfiles
@@ -253,7 +256,7 @@ module Homebrew
nil
end
- if formula && skip_clean_formula?(formula)
+ if formula && Cleanup.skip_clean_formula?(formula)
onoe "Refusing to clean #{formula} because it is listed in " \
"#{Tty.bold}HOMEBREW_NO_CLEANUP_FORMULAE#{Tty.reset}!"
elsif formula
@@ -519,5 +522,36 @@ module Homebrew
print "and #{d} directories " if d.positive?
puts "from #{HOMEBREW_PREFIX}"
end
+
+ def self.autoremove(dry_run: false)
+ require "cask/caskroom"
+
+ # If this runs after install, uninstall, reinstall or upgrade,
+ # the cache of installed formulae may no longer be valid.
+ Formula.clear_cache unless dry_run
+
+ # Remove formulae listed in HOMEBREW_NO_CLEANUP_FORMULAE.
+ formulae = Formula.installed.reject(&method(:skip_clean_formula?))
+ casks = Cask::Caskroom.casks
+
+ removable_formulae = Formula.unused_formulae_with_no_dependents(formulae, casks)
+
+ return if removable_formulae.blank?
+
+ formulae_names = removable_formulae.map(&:full_name).sort
+
+ verb = dry_run ? "Would autoremove" : "Autoremoving"
+ oh1 "#{verb} #{formulae_names.count} unneeded #{"formula".pluralize(formulae_names.count)}:"
+ puts formulae_names.join("\n")
+ return if dry_run
+
+ require "uninstall"
+
+ kegs_by_rack = removable_formulae.map(&:any_installed_keg).group_by(&:rack)
+ Uninstall.uninstall_kegs(kegs_by_rack)
+
+ # The installed formula cache will be invalid after uninstalling.
+ Formula.clear_cache
+ end
end
end
diff --git a/Library/Homebrew/cmd/autoremove.rb b/Library/Homebrew/cmd/autoremove.rb
index a80cb4ae49..94dea99605 100644
--- a/Library/Homebrew/cmd/autoremove.rb
+++ b/Library/Homebrew/cmd/autoremove.rb
@@ -1,9 +1,8 @@
# typed: true
# frozen_string_literal: true
-require "formula"
+require "cleanup"
require "cli/parser"
-require "uninstall"
module Homebrew
module_function
@@ -20,37 +19,9 @@ module Homebrew
end
end
- def get_removable_formulae(formulae)
- removable_formulae = Formula.installed_formulae_with_no_dependents(formulae).reject do |f|
- Tab.for_keg(f.any_installed_keg).installed_on_request
- end
-
- removable_formulae += get_removable_formulae(formulae - removable_formulae) if removable_formulae.present?
-
- removable_formulae
- end
-
def autoremove
args = autoremove_args.parse
- removable_formulae = get_removable_formulae(Formula.installed)
-
- if (casks = Cask::Caskroom.casks.presence)
- removable_formulae -= casks.flat_map { |cask| cask.depends_on[:formula] }
- .compact
- .map { |f| Formula[f] }
- .flat_map { |f| [f, *f.runtime_formula_dependencies].compact }
- end
- return if removable_formulae.blank?
-
- formulae_names = removable_formulae.map(&:full_name).sort
-
- verb = args.dry_run? ? "Would uninstall" : "Uninstalling"
- oh1 "#{verb} #{formulae_names.count} unneeded #{"formula".pluralize(formulae_names.count)}:"
- puts formulae_names.join("\n")
- return if args.dry_run?
-
- kegs_by_rack = removable_formulae.map(&:any_installed_keg).group_by(&:rack)
- Uninstall.uninstall_kegs(kegs_by_rack)
+ Cleanup.autoremove(dry_run: args.dry_run?)
end
end
diff --git a/Library/Homebrew/cmd/leaves.rb b/Library/Homebrew/cmd/leaves.rb
index 1c003b4b96..ec134de4a1 100644
--- a/Library/Homebrew/cmd/leaves.rb
+++ b/Library/Homebrew/cmd/leaves.rb
@@ -37,7 +37,7 @@ module Homebrew
def leaves
args = leaves_args.parse
- leaves_list = Formula.installed_formulae_with_no_dependents
+ leaves_list = Formula.formulae_with_no_formula_dependents(Formula.installed)
leaves_list.select!(&method(:installed_on_request?)) if args.installed_on_request?
leaves_list.select!(&method(:installed_as_dependency?)) if args.installed_as_dependency?
diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb
index 8c40d4219d..50a9abd5d1 100644
--- a/Library/Homebrew/cmd/uninstall.rb
+++ b/Library/Homebrew/cmd/uninstall.rb
@@ -50,6 +50,11 @@ module Homebrew
all_kegs: args.force?,
)
+ # If ignore_unavailable is true and the named args
+ # are a series of invalid kegs and casks,
+ # #to_kegs_to_casks will return empty arrays.
+ return if all_kegs.blank? && casks.blank?
+
kegs_by_rack = all_kegs.group_by(&:rack)
Uninstall.uninstall_kegs(
@@ -73,5 +78,7 @@ module Homebrew
force: args.force?,
)
end
+
+ Cleanup.autoremove if Homebrew::EnvConfig.autoremove?
end
end
diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb
index 6397f0abb4..014b24d27d 100644
--- a/Library/Homebrew/env_config.rb
+++ b/Library/Homebrew/env_config.rb
@@ -36,6 +36,12 @@ module Homebrew
"disable auto-update entirely with HOMEBREW_NO_AUTO_UPDATE.",
default: 300,
},
+ HOMEBREW_AUTOREMOVE: {
+ description: "If set, calls to `brew cleanup` and `brew uninstall` will automatically " \
+ "remove unused formula dependents and if HOMEBREW_NO_INSTALL_CLEANUP is not set, " \
+ "`brew cleanup` will start running `brew autoremove` periodically.",
+ boolean: true,
+ },
HOMEBREW_BAT: {
description: "If set, use `bat` for the `brew cat` command.",
boolean: true,
@@ -263,8 +269,8 @@ module Homebrew
boolean: true,
},
HOMEBREW_NO_CLEANUP_FORMULAE: {
- description: "A comma-separated list of formulae. Homebrew will refuse to clean up a " \
- "formula if it appears on this list.",
+ description: "A comma-separated list of formulae. Homebrew will refuse to clean up " \
+ "or autoremove a formula if it appears on this list.",
},
HOMEBREW_NO_COLOR: {
description: "If set, do not print text with colour added.",
diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb
index d2f9ea56f2..0f2b71767d 100644
--- a/Library/Homebrew/formula.rb
+++ b/Library/Homebrew/formula.rb
@@ -1706,14 +1706,46 @@ class Formula
end.uniq(&:name)
end
- # An array of all installed {Formula} without dependents
+ # An array of all installed {Formula} with {Cask} dependents.
# @private
- def self.installed_formulae_with_no_dependents(formulae = installed)
+ def self.formulae_with_cask_dependents(casks)
+ casks.flat_map { |cask| cask.depends_on[:formula] }
+ .compact
+ .map { |f| Formula[f] }
+ .flat_map { |f| [f, *f.runtime_formula_dependencies].compact }
+ end
+
+ # An array of all installed {Formula} without {Formula} dependents
+ # @private
+ def self.formulae_with_no_formula_dependents(formulae)
return [] if formulae.blank?
formulae - formulae.flat_map(&:runtime_formula_dependencies)
end
+ # Recursive function that returns an array of {Formula} without
+ # {Formula} dependents that weren't installed on request.
+ # @private
+ def self.unused_formulae_with_no_formula_dependents(formulae)
+ unused_formulae = formulae_with_no_formula_dependents(formulae).reject do |f|
+ Tab.for_keg(f.any_installed_keg).installed_on_request
+ end
+
+ if unused_formulae.present?
+ unused_formulae += unused_formulae_with_no_formula_dependents(formulae - unused_formulae)
+ end
+
+ unused_formulae
+ end
+
+ # An array of {Formula} without {Formula} or {Cask}
+ # dependents that weren't installed on request.
+ # @private
+ def self.unused_formulae_with_no_dependents(formulae, casks)
+ unused_formulae = unused_formulae_with_no_formula_dependents(formulae)
+ unused_formulae - formulae_with_cask_dependents(casks)
+ end
+
def self.installed_with_alias_path(alias_path)
return [] if alias_path.nil?
diff --git a/Library/Homebrew/sorbet/rbi/hidden-definitions/hidden.rbi b/Library/Homebrew/sorbet/rbi/hidden-definitions/hidden.rbi
index ba6af6ace7..f22b19bd3a 100644
--- a/Library/Homebrew/sorbet/rbi/hidden-definitions/hidden.rbi
+++ b/Library/Homebrew/sorbet/rbi/hidden-definitions/hidden.rbi
@@ -2442,6 +2442,8 @@ module Homebrew::EnvConfig
def self.artifact_domain(); end
+ def self.autoremove?(); end
+
def self.auto_update_secs(); end
def self.bat?(); end
diff --git a/Library/Homebrew/test/cmd/autoremove_spec.rb b/Library/Homebrew/test/cmd/autoremove_spec.rb
index 24069e0bb7..1644fbf009 100644
--- a/Library/Homebrew/test/cmd/autoremove_spec.rb
+++ b/Library/Homebrew/test/cmd/autoremove_spec.rb
@@ -5,4 +5,34 @@ require "cmd/shared_examples/args_parse"
describe "brew autoremove" do
it_behaves_like "parseable arguments"
+
+ describe "integration test" do
+ let(:requested_formula) { Formula["testball1"] }
+ let(:unused_formula) { Formula["testball2"] }
+
+ before do
+ install_test_formula "testball1"
+ install_test_formula "testball2"
+
+ # Make testball2 an unused dependency
+ tab = Tab.for_name("testball2")
+ tab.installed_on_request = false
+ tab.installed_as_dependency = true
+ tab.write
+ end
+
+ it "only removes unused dependencies", :integration_test do
+ expect(requested_formula.any_version_installed?).to be true
+ expect(unused_formula.any_version_installed?).to be true
+
+ # When there are unused dependencies
+ expect { brew "autoremove" }
+ .to be_a_success
+ .and output(/Autoremoving/).to_stdout
+ .and not_to_output.to_stderr
+
+ expect(requested_formula.any_version_installed?).to be true
+ expect(unused_formula.any_version_installed?).to be false
+ end
+ end
end
diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb
index 0883985623..928f53809b 100644
--- a/Library/Homebrew/test/formula_spec.rb
+++ b/Library/Homebrew/test/formula_spec.rb
@@ -446,40 +446,133 @@ describe Formula do
end
end
- describe "::installed_formulae_with_no_dependents" do
- let(:formula_is_dep) do
- formula "foo" do
- url "foo-1.1"
+ shared_context "with formulae for dependency testing" do
+ let(:formula_with_deps) do
+ formula "zero" do
+ url "zero-1.0"
end
end
- let(:formula_with_deps) do
- formula "bar" do
- url "bar-1.0"
+ let(:formula_is_dep1) do
+ formula "one" do
+ url "one-1.1"
+ end
+ end
+
+ let(:formula_is_dep2) do
+ formula "two" do
+ url "two-1.1"
end
end
let(:formulae) do
[
formula_with_deps,
- formula_is_dep,
+ formula_is_dep1,
+ formula_is_dep2,
]
end
before do
- allow(formula_with_deps).to receive(:runtime_formula_dependencies).and_return([formula_is_dep])
+ allow(formula_with_deps).to receive(:runtime_formula_dependencies).and_return([formula_is_dep1,
+ formula_is_dep2])
+ allow(formula_is_dep1).to receive(:runtime_formula_dependencies).and_return([formula_is_dep2])
end
+ end
- specify "without formulae parameter" do
- allow(described_class).to receive(:installed).and_return(formulae)
+ describe "::formulae_with_no_formula_dependents" do
+ include_context "with formulae for dependency testing"
- expect(described_class.installed_formulae_with_no_dependents)
+ it "filters out dependencies" do
+ expect(described_class.formulae_with_no_formula_dependents(formulae))
.to eq([formula_with_deps])
end
+ end
- specify "with formulae parameter" do
- expect(described_class.installed_formulae_with_no_dependents(formulae))
- .to eq([formula_with_deps])
+ describe "::unused_formulae_with_no_formula_dependents" do
+ include_context "with formulae for dependency testing"
+
+ let(:tab_from_keg) { double }
+
+ before do
+ allow(Tab).to receive(:for_keg).and_return(tab_from_keg)
+ end
+
+ specify "installed on request" do
+ allow(tab_from_keg).to receive(:installed_on_request).and_return(true)
+ expect(described_class.unused_formulae_with_no_formula_dependents(formulae))
+ .to eq([])
+ end
+
+ specify "not installed on request" do
+ allow(tab_from_keg).to receive(:installed_on_request).and_return(false)
+ expect(described_class.unused_formulae_with_no_formula_dependents(formulae))
+ .to eq(formulae)
+ end
+ end
+
+ shared_context "with formulae and casks for dependency testing" do
+ include_context "with formulae for dependency testing"
+
+ require "cask/cask_loader"
+
+ let(:cask_one_dep) do
+ Cask::CaskLoader.load(+<<-RUBY)
+ cask "red" do
+ depends_on formula: "two"
+ end
+ RUBY
+ end
+
+ let(:cask_multiple_deps) do
+ Cask::CaskLoader.load(+<<-RUBY)
+ cask "blue" do
+ depends_on formula: "zero"
+ end
+ RUBY
+ end
+
+ let(:cask_no_deps1) do
+ Cask::CaskLoader.load(+<<-RUBY)
+ cask "green" do
+ end
+ RUBY
+ end
+
+ let(:cask_no_deps2) do
+ Cask::CaskLoader.load(+<<-RUBY)
+ cask "purple" do
+ end
+ RUBY
+ end
+
+ let(:casks_no_deps) { [cask_no_deps1, cask_no_deps2] }
+ let(:casks_one_dep) { [cask_no_deps1, cask_no_deps2, cask_one_dep] }
+ let(:casks_multiple_deps) { [cask_no_deps1, cask_no_deps2, cask_multiple_deps] }
+
+ before do
+ allow(described_class).to receive("[]").with("zero").and_return(formula_with_deps)
+ allow(described_class).to receive("[]").with("one").and_return(formula_is_dep1)
+ allow(described_class).to receive("[]").with("two").and_return(formula_is_dep2)
+ end
+ end
+
+ describe "::formulae_with_cask_dependents" do
+ include_context "with formulae and casks for dependency testing"
+
+ specify "no dependents" do
+ expect(described_class.formulae_with_cask_dependents(casks_no_deps))
+ .to eq([])
+ end
+
+ specify "one dependent" do
+ expect(described_class.formulae_with_cask_dependents(casks_one_dep))
+ .to eq([formula_is_dep2])
+ end
+
+ specify "multiple dependents" do
+ expect(described_class.formulae_with_cask_dependents(casks_multiple_deps))
+ .to eq(formulae)
end
end
diff --git a/docs/Manpage.md b/docs/Manpage.md
index b29bae2c63..c08de40f3c 100644
--- a/docs/Manpage.md
+++ b/docs/Manpage.md
@@ -1960,6 +1960,9 @@ example, run `export HOMEBREW_NO_INSECURE_REDIRECT=1` rather than just
*Default:* `300`.
+- `HOMEBREW_AUTOREMOVE`
+
If set, calls to `brew cleanup` and `brew uninstall` will automatically remove unused formula dependents and if HOMEBREW_NO_INSTALL_CLEANUP is not set, `brew cleanup` will start running `brew autoremove` periodically.
+
- `HOMEBREW_BAT`
If set, use `bat` for the `brew cat` command.
@@ -2140,7 +2143,7 @@ example, run `export HOMEBREW_NO_INSECURE_REDIRECT=1` rather than just
If set, do not check for broken linkage of dependents or outdated dependents after installing, upgrading or reinstalling formulae. This will result in fewer dependents (and their dependencies) being upgraded or reinstalled but may result in more breakage from running `brew install *`formula`*` or `brew upgrade *`formula`*`.
- `HOMEBREW_NO_CLEANUP_FORMULAE`
-
A comma-separated list of formulae. Homebrew will refuse to clean up a formula if it appears on this list.
+
A comma-separated list of formulae. Homebrew will refuse to clean up or autoremove a formula if it appears on this list.
- `HOMEBREW_NO_COLOR`
If set, do not print text with colour added.
diff --git a/manpages/brew.1 b/manpages/brew.1
index 5d2cfe0259..4ff661d7fa 100644
--- a/manpages/brew.1
+++ b/manpages/brew.1
@@ -2779,6 +2779,12 @@ Run \fBbrew update\fR once every \fBHOMEBREW_AUTO_UPDATE_SECS\fR seconds before
\fIDefault:\fR \fB300\fR\.
.
.TP
+\fBHOMEBREW_AUTOREMOVE\fR
+.
+.br
+If set, calls to \fBbrew cleanup\fR and \fBbrew uninstall\fR will automatically remove unused formula dependents and if HOMEBREW_NO_INSTALL_CLEANUP is not set, \fBbrew cleanup\fR will start running \fBbrew autoremove\fR periodically\.
+.
+.TP
\fBHOMEBREW_BAT\fR
.
.br
@@ -3118,7 +3124,7 @@ If set, do not check for broken linkage of dependents or outdated dependents aft
\fBHOMEBREW_NO_CLEANUP_FORMULAE\fR
.
.br
-A comma\-separated list of formulae\. Homebrew will refuse to clean up a formula if it appears on this list\.
+A comma\-separated list of formulae\. Homebrew will refuse to clean up or autoremove a formula if it appears on this list\.
.
.TP
\fBHOMEBREW_NO_COLOR\fR