diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index deadc6bd53..24d32b0cfa 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -579,6 +579,7 @@ module Homebrew end def self.autoremove(dry_run: false) + require "utils/autoremove" require "cask/caskroom" # If this runs after install, uninstall, reinstall or upgrade, @@ -593,7 +594,7 @@ module Homebrew end casks = Cask::Caskroom.casks - removable_formulae = Formula.unused_formulae_with_no_dependents(formulae, casks) + removable_formulae = Utils::Autoremove.removable_formulae(formulae, casks) return if removable_formulae.blank? diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 7279db522b..1aad266d51 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1844,55 +1844,6 @@ class Formula end.uniq(&:name) end - # An array of all installed {Formula} with {Cask} dependents. - # @private - 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 runtime {Formula} - # dependents for bottles and without build {Formula} dependents - # for those built from source. - # @private - def self.formulae_with_no_formula_dependents(formulae) - return [] if formulae.blank? - - formulae - formulae.each_with_object([]) do |formula, dependents| - dependents.concat(formula.runtime_formula_dependencies) - - # Include build dependencies when the formula is not a bottle - unless Tab.for_keg(formula.any_installed_keg).poured_from_bottle - dependents.concat(formula.deps.select(&:build?).map(&:to_formula)) - end - end - 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/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 71ad626a6f..403c972dbe 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -450,136 +450,6 @@ describe Formula do end end - shared_context "with formulae for dependency testing" do - let(:formula_with_deps) do - formula "zero" do - url "zero-1.0" - end - end - - 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_dep1, - formula_is_dep2, - ] - end - - before do - 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 - - describe "::formulae_with_no_formula_dependents" do - include_context "with formulae for dependency testing" - - it "filters out dependencies" do - expect(described_class.formulae_with_no_formula_dependents(formulae)) - .to eq([formula_with_deps]) - end - end - - 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 - describe "::inreplace" do specify "raises build error on failure" do f = formula do diff --git a/Library/Homebrew/test/utils/autoremove_spec.rb b/Library/Homebrew/test/utils/autoremove_spec.rb new file mode 100644 index 0000000000..d7fe8fc9d6 --- /dev/null +++ b/Library/Homebrew/test/utils/autoremove_spec.rb @@ -0,0 +1,162 @@ +# typed: false +# frozen_string_literal: true + +require "utils/autoremove" + +describe Utils::Autoremove do + shared_context "with formulae for dependency testing" do + let(:formula_with_deps) do + formula "zero" do + url "zero-1.0" + + depends_on "three" => :build + end + end + + 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(:formula_is_build_dep) do + formula "three" do + url "three-1.1" + end + end + + let(:formulae) do + [ + formula_with_deps, + formula_is_dep1, + formula_is_dep2, + formula_is_build_dep, + ] + end + + let(:tab_from_keg) { double } + + before do + 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]) + + allow(Tab).to receive(:for_keg).and_return(tab_from_keg) + end + end + + describe "::formulae_with_no_formula_dependents" do + include_context "with formulae for dependency testing" + + before do + allow(Formulary).to receive(:factory).with("three").and_return(formula_is_build_dep) + end + + context "when formulae are bottles" do + it "filters out runtime dependencies" do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true) + expect(described_class.send(:formulae_with_no_formula_dependents, formulae)) + .to eq([formula_with_deps, formula_is_build_dep]) + end + end + + context "when formulae are built from source" do + it "filters out runtime and build dependencies" do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(false) + expect(described_class.send(:formulae_with_no_formula_dependents, formulae)) + .to eq([formula_with_deps]) + end + end + end + + describe "::unused_formulae_with_no_formula_dependents" do + include_context "with formulae for dependency testing" + + before do + allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true) + end + + specify "installed on request" do + allow(tab_from_keg).to receive(:installed_on_request).and_return(true) + expect(described_class.send(: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.send(:unused_formulae_with_no_formula_dependents, formulae)) + .to match_array(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(Formula).to receive("[]").with("zero").and_return(formula_with_deps) + allow(Formula).to receive("[]").with("one").and_return(formula_is_dep1) + allow(Formula).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.send(:formulae_with_cask_dependents, casks_no_deps)) + .to eq([]) + end + + specify "one dependent" do + expect(described_class.send(:formulae_with_cask_dependents, casks_one_dep)) + .to eq([formula_is_dep2]) + end + + specify "multiple dependents" do + expect(described_class.send(:formulae_with_cask_dependents, casks_multiple_deps)) + .to match_array([formula_with_deps, formula_is_dep1, formula_is_dep2]) + end + end +end diff --git a/Library/Homebrew/utils/autoremove.rb b/Library/Homebrew/utils/autoremove.rb new file mode 100644 index 0000000000..cb34473286 --- /dev/null +++ b/Library/Homebrew/utils/autoremove.rb @@ -0,0 +1,68 @@ +# typed: false +# frozen_string_literal: true + +module Utils + # Helper function for finding autoremovable formulae. + # + # @private + module Autoremove + module_function + + # An array of all installed {Formula} with {Cask} dependents. + # @private + def 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 + private_class_method :formulae_with_cask_dependents + + # An array of all installed {Formula} without runtime {Formula} + # dependents for bottles and without build {Formula} dependents + # for those built from source. + # @private + def formulae_with_no_formula_dependents(formulae) + return [] if formulae.blank? + + dependents = [] + formulae.each do |formula| + dependents += formula.runtime_formula_dependencies + + # Ignore build dependencies when the formula is a bottle + next if Tab.for_keg(formula.any_installed_keg).poured_from_bottle + + formula.deps.select(&:build?).each do |dep| + suppress(FormulaUnavailableError) { dependents << dep.to_formula } + end + end + formulae - dependents + end + private_class_method :formulae_with_no_formula_dependents + + # Recursive function that returns an array of {Formula} without + # {Formula} dependents that weren't installed on request. + # @private + def 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 + private_class_method :unused_formulae_with_no_formula_dependents + + # An array of {Formula} without {Formula} or {Cask} + # dependents that weren't installed on request and without + # build dependencies for {Formula} installed from source. + # @private + def removable_formulae(formulae, casks) + unused_formulae = unused_formulae_with_no_formula_dependents(formulae) + unused_formulae - formulae_with_cask_dependents(casks) + end + end +end