Merge pull request #9047 from tie624/add_autoremove

Add autoremove
This commit is contained in:
Mike McQuaid 2020-11-06 16:47:59 +00:00 committed by GitHub
commit 2ac5cffd0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 390 additions and 211 deletions

View File

@ -0,0 +1,50 @@
# typed: false
# frozen_string_literal: true
require "formula"
require "cli/parser"
require "uninstall"
module Homebrew
module_function
def autoremove_args
Homebrew::CLI::Parser.new do
usage_banner <<~EOS
`autoremove` [<options>]
Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed.
EOS
switch "-n", "--dry-run",
description: "List what would be uninstalled, but do not actually uninstall anything."
named 0
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)
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)
end
end

View File

@ -2,7 +2,6 @@
# frozen_string_literal: true
require "formula"
require "tab"
require "cli/parser"
module Homebrew
@ -23,9 +22,6 @@ module Homebrew
def leaves
leaves_args.parse
installed = Formula.installed.sort
deps_of_installed = installed.flat_map(&:runtime_formula_dependencies)
leaves = installed.map(&:full_name) - deps_of_installed.map(&:full_name)
leaves.each(&method(:puts))
Formula.installed_formulae_with_no_dependents.map(&:full_name).sort.each(&method(:puts))
end
end

View File

@ -8,6 +8,7 @@ require "migrator"
require "cli/parser"
require "cask/cmd"
require "cask/cask_loader"
require "uninstall"
module Homebrew
module_function
@ -54,76 +55,10 @@ module Homebrew
kegs_by_rack = all_kegs.group_by(&:rack)
end
handle_unsatisfied_dependents(kegs_by_rack,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named)
return if Homebrew.failed?
kegs_by_rack.each do |rack, kegs|
if args.force?
name = rack.basename
if rack.directory?
puts "Uninstalling #{name}... (#{rack.abv})"
kegs.each do |keg|
keg.unlink
keg.uninstall
end
end
rm_pin rack
else
kegs.each do |keg|
begin
f = Formulary.from_rack(rack)
if f.pinned?
onoe "#{f.full_name} is pinned. You must unpin it to uninstall."
next
end
rescue
nil
end
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)
puts "#{keg.name} #{versions.to_sentence} #{"is".pluralize(versions.count)} still installed."
puts "Run `brew uninstall --force #{keg.name}` to remove all versions."
end
next unless f
paths = f.pkgetc.find.map(&:to_s) if f.pkgetc.exist?
if paths.present?
puts
opoo <<~EOS
The following #{f.name} configuration files have not been removed!
If desired, remove them manually with `rm -rf`:
#{paths.sort.uniq.join("\n ")}
EOS
end
unversioned_name = f.name.gsub(/@.+$/, "")
maybe_paths = Dir.glob("#{f.etc}/*#{unversioned_name}*")
maybe_paths -= paths if paths.present?
if maybe_paths.present?
puts
opoo <<~EOS
The following may be #{f.name} configuration files and have not been removed!
If desired, remove them manually with `rm -rf`:
#{maybe_paths.sort.uniq.join("\n ")}
EOS
end
end
end
end
end
Uninstall.uninstall_kegs(kegs_by_rack,
force: args.force?,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named)
return if casks.blank?
@ -144,74 +79,4 @@ module Homebrew
end
end
end
def handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: false, named_args: [])
return if ignore_dependencies
all_kegs = kegs_by_rack.values.flatten(1)
check_for_dependents(all_kegs, named_args: named_args)
rescue MethodDeprecatedError
# Silently ignore deprecations when uninstalling.
nil
end
def check_for_dependents(kegs, named_args: [])
return false unless result = Keg.find_some_installed_dependents(kegs)
if Homebrew::EnvConfig.developer?
DeveloperDependentsMessage.new(*result, named_args: named_args).output
else
NondeveloperDependentsMessage.new(*result, named_args: named_args).output
end
true
end
class DependentsMessage
attr_reader :reqs, :deps, :named_args
def initialize(requireds, dependents, named_args: [])
@reqs = requireds
@deps = dependents
@named_args = named_args
end
protected
def sample_command
"brew uninstall --ignore-dependencies #{named_args.join(" ")}"
end
def are_required_by_deps
"#{"is".pluralize(reqs.count)} required by #{deps.to_sentence}, " \
"which #{"is".pluralize(deps.count)} currently installed"
end
end
class DeveloperDependentsMessage < DependentsMessage
def output
opoo <<~EOS
#{reqs.to_sentence} #{are_required_by_deps}.
You can silence this warning with:
#{sample_command}
EOS
end
end
class NondeveloperDependentsMessage < DependentsMessage
def output
ofail <<~EOS
Refusing to uninstall #{reqs.to_sentence}
because #{"it".pluralize(reqs.count)} #{are_required_by_deps}.
You can override this and force removal with:
#{sample_command}
EOS
end
end
def rm_pin(rack)
Formulary.from_rack(rack).unpin
rescue
nil
end
end

View File

@ -1518,6 +1518,14 @@ class Formula
end.uniq(&:name)
end
# An array of all installed {Formula} without dependents
# @private
def self.installed_formulae_with_no_dependents(formulae = installed)
return [] if formulae.blank?
formulae - formulae.flat_map(&:runtime_formula_dependencies)
end
def self.installed_with_alias_path(alias_path)
return [] if alias_path.nil?

View File

@ -33,6 +33,7 @@ RSpec/MultipleDescribes:
- 'cmd/--repository_spec.rb'
- 'cmd/--version_spec.rb'
- 'cmd/analytics_spec.rb'
- 'cmd/autoremove_spec.rb'
- 'cmd/cleanup_spec.rb'
- 'cmd/commands_spec.rb'
- 'cmd/config_spec.rb'

View File

@ -0,0 +1,8 @@
# typed: false
# frozen_string_literal: true
require "cmd/shared_examples/args_parse"
describe "Homebrew.autoremove_args" do
it_behaves_like "parseable arguments"
end

View File

@ -8,14 +8,42 @@ describe "Homebrew.leaves_args" do
end
describe "brew leaves", :integration_test do
it "prints all Formulae that are not dependencies of other Formulae" do
setup_test_formula "foo"
setup_test_formula "bar"
(HOMEBREW_CELLAR/"foo/0.1/somedir").mkpath
context "when there are no installed Formulae" do
it "prints nothing" do
setup_test_formula "foo"
setup_test_formula "bar"
expect { brew "leaves" }
.to output("foo\n").to_stdout
.and not_to_output.to_stderr
.and be_a_success
expect { brew "leaves" }
.to not_to_output.to_stdout
.and not_to_output.to_stderr
.and be_a_success
end
end
context "when there are only installed Formulae without dependencies" do
it "prints all installed Formulae" do
setup_test_formula "foo"
setup_test_formula "bar"
(HOMEBREW_CELLAR/"foo/0.1/somedir").mkpath
expect { brew "leaves" }
.to output("foo\n").to_stdout
.and not_to_output.to_stderr
.and be_a_success
end
end
context "when there are installed Formulae" do
it "prints all installed Formulae that are not dependencies of another installed Formula" do
setup_test_formula "foo"
setup_test_formula "bar"
(HOMEBREW_CELLAR/"foo/0.1/somedir").mkpath
(HOMEBREW_CELLAR/"bar/0.1/somedir").mkpath
expect { brew "leaves" }
.to output("bar\n").to_stdout
.and not_to_output.to_stderr
.and be_a_success
end
end
end

View File

@ -19,61 +19,3 @@ describe "brew uninstall", :integration_test do
.and be_a_success
end
end
describe Homebrew do
let(:dependency) { formula("dependency") { url "f-1" } }
let(:dependent) do
formula("dependent") do
url "f-1"
depends_on "dependency"
end
end
let(:kegs_by_rack) { { dependency.rack => [Keg.new(dependency.latest_installed_prefix)] } }
before do
[dependency, dependent].each do |f|
f.latest_installed_prefix.mkpath
Keg.new(f.latest_installed_prefix).optlink
end
tab = Tab.empty
tab.homebrew_version = "1.1.6"
tab.tabfile = dependent.latest_installed_prefix/Tab::FILENAME
tab.runtime_dependencies = [
{ "full_name" => "dependency", "version" => "1" },
]
tab.write
stub_formula_loader dependency
stub_formula_loader dependent
end
describe "::handle_unsatisfied_dependents" do
specify "when developer" do
ENV["HOMEBREW_DEVELOPER"] = "1"
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack)
}.to output(/Warning/).to_stderr
expect(described_class).not_to have_failed
end
specify "when not developer" do
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack)
}.to output(/Error/).to_stderr
expect(described_class).to have_failed
end
specify "when not developer and `ignore_dependencies` is true" do
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: true)
}.not_to output.to_stderr
expect(described_class).not_to have_failed
end
end
end

View File

@ -440,6 +440,43 @@ describe Formula do
end
end
describe "::installed_formulae_with_no_dependents" do
let(:formula_is_dep) do
formula "foo" do
url "foo-1.1"
end
end
let(:formula_with_deps) do
formula "bar" do
url "bar-1.0"
end
end
let(:formulae) do
[
formula_with_deps,
formula_is_dep,
]
end
before do
allow(formula_with_deps).to receive(:runtime_formula_dependencies).and_return([formula_is_dep])
end
specify "without formulae parameter" do
allow(described_class).to receive(:installed).and_return(formulae)
expect(described_class.installed_formulae_with_no_dependents)
.to eq([formula_with_deps])
end
specify "with formulae parameter" do
expect(described_class.installed_formulae_with_no_dependents(formulae))
.to eq([formula_with_deps])
end
end
describe "::installed_with_alias_path" do
specify "with alias path with nil" do
expect(described_class.installed_with_alias_path(nil)).to be_empty

View File

@ -0,0 +1,62 @@
# typed: false
# frozen_string_literal: true
require "uninstall"
describe Homebrew::Uninstall do
let(:dependency) { formula("dependency") { url "f-1" } }
let(:dependent) do
formula("dependent") do
url "f-1"
depends_on "dependency"
end
end
let(:kegs_by_rack) { { dependency.rack => [Keg.new(dependency.latest_installed_prefix)] } }
before do
[dependency, dependent].each do |f|
f.latest_installed_prefix.mkpath
Keg.new(f.latest_installed_prefix).optlink
end
tab = Tab.empty
tab.homebrew_version = "1.1.6"
tab.tabfile = dependent.latest_installed_prefix/Tab::FILENAME
tab.runtime_dependencies = [
{ "full_name" => "dependency", "version" => "1" },
]
tab.write
stub_formula_loader dependency
stub_formula_loader dependent
end
describe "::handle_unsatisfied_dependents" do
specify "when developer" do
ENV["HOMEBREW_DEVELOPER"] = "1"
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack)
}.to output(/Warning/).to_stderr
expect(Homebrew).not_to have_failed
end
specify "when not developer" do
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack)
}.to output(/Error/).to_stderr
expect(Homebrew).to have_failed
end
specify "when not developer and `ignore_dependencies` is true" do
expect {
described_class.handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: true)
}.not_to output.to_stderr
expect(Homebrew).not_to have_failed
end
end
end

View File

@ -0,0 +1,160 @@
# typed: true
# frozen_string_literal: true
require "keg"
require "formula"
module Homebrew
# Helper module for uninstalling kegs.
#
# @api private
module Uninstall
module_function
def uninstall_kegs(kegs_by_rack, force: false, ignore_dependencies: false, named_args: [])
handle_unsatisfied_dependents(kegs_by_rack,
ignore_dependencies: ignore_dependencies,
named_args: named_args)
return if Homebrew.failed?
kegs_by_rack.each do |rack, kegs|
if force
name = rack.basename
if rack.directory?
puts "Uninstalling #{name}... (#{rack.abv})"
kegs.each do |keg|
keg.unlink
keg.uninstall
end
end
rm_pin rack
else
kegs.each do |keg|
begin
f = Formulary.from_rack(rack)
if f.pinned?
onoe "#{f.full_name} is pinned. You must unpin it to uninstall."
next
end
rescue
nil
end
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)
puts "#{keg.name} #{versions.to_sentence} #{"is".pluralize(versions.count)} still installed."
puts "Run `brew uninstall --force #{keg.name}` to remove all versions."
end
next unless f
paths = f.pkgetc.find.map(&:to_s) if f.pkgetc.exist?
if paths.present?
puts
opoo <<~EOS
The following #{f.name} configuration files have not been removed!
If desired, remove them manually with `rm -rf`:
#{paths.sort.uniq.join("\n ")}
EOS
end
unversioned_name = f.name.gsub(/@.+$/, "")
maybe_paths = Dir.glob("#{f.etc}/*#{unversioned_name}*")
maybe_paths -= paths if paths.present?
if maybe_paths.present?
puts
opoo <<~EOS
The following may be #{f.name} configuration files and have not been removed!
If desired, remove them manually with `rm -rf`:
#{maybe_paths.sort.uniq.join("\n ")}
EOS
end
end
end
end
end
end
def handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: false, named_args: [])
return if ignore_dependencies
all_kegs = kegs_by_rack.values.flatten(1)
check_for_dependents(all_kegs, named_args: named_args)
rescue MethodDeprecatedError
# Silently ignore deprecations when uninstalling.
nil
end
def check_for_dependents(kegs, named_args: [])
return false unless result = Keg.find_some_installed_dependents(kegs)
if Homebrew::EnvConfig.developer?
DeveloperDependentsMessage.new(*result, named_args: named_args).output
else
NondeveloperDependentsMessage.new(*result, named_args: named_args).output
end
true
end
# @api private
class DependentsMessage
attr_reader :reqs, :deps, :named_args
def initialize(requireds, dependents, named_args: [])
@reqs = requireds
@deps = dependents
@named_args = named_args
end
protected
def sample_command
"brew uninstall --ignore-dependencies #{named_args.join(" ")}"
end
def are_required_by_deps
"#{"is".pluralize(reqs.count)} required by #{deps.to_sentence}, " \
"which #{"is".pluralize(deps.count)} currently installed"
end
end
# @api private
class DeveloperDependentsMessage < DependentsMessage
def output
opoo <<~EOS
#{reqs.to_sentence} #{are_required_by_deps}.
You can silence this warning with:
#{sample_command}
EOS
end
end
# @api private
class NondeveloperDependentsMessage < DependentsMessage
def output
ofail <<~EOS
Refusing to uninstall #{reqs.to_sentence}
because #{"it".pluralize(reqs.count)} #{are_required_by_deps}.
You can override this and force removal with:
#{sample_command}
EOS
end
end
def rm_pin(rack)
Formulary.from_rack(rack).unpin
rescue
nil
end
end
end

View File

@ -0,0 +1,7 @@
# typed: strict
module Homebrew
module Uninstall
include Kernel
end
end

View File

@ -12,6 +12,7 @@
abv
analytics
audit
autoremove
bottle
bump
bump-cask-pr

View File

@ -56,6 +56,13 @@ Turn Homebrew's analytics on or off respectively.
`brew analytics regenerate-uuid`:
Regenerate the UUID used for Homebrew's analytics.
### `autoremove` [*`options`*]
Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed.
* `-n`, `--dry-run`:
List what would be uninstalled, but do not actually uninstall anything.
### `cask` *`command`* [*`options`*] [*`cask`*]
Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries.

View File

@ -53,6 +53,13 @@ Control Homebrew\'s anonymous aggregate user behaviour analytics\. Read more at
\fBbrew analytics regenerate\-uuid\fR
Regenerate the UUID used for Homebrew\'s analytics\.
.
.SS "\fBautoremove\fR [\fIoptions\fR]"
Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed\.
.
.TP
\fB\-n\fR, \fB\-\-dry\-run\fR
List what would be uninstalled, but do not actually uninstall anything\.
.
.SS "\fBcask\fR \fIcommand\fR [\fIoptions\fR] [\fIcask\fR]"
Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries\.
.