From 7965ad616085bbecc75383dc0dd81646cebeac7a Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Tue, 9 Feb 2021 06:11:53 +0530 Subject: [PATCH 1/5] cmd/uninstall: check for dependent casks --- Library/Homebrew/cmd/uninstall.rb | 1 + Library/Homebrew/keg.rb | 51 +++++++++++++++++++++++++ Library/Homebrew/test/uninstall_spec.rb | 21 +++++++--- Library/Homebrew/uninstall.rb | 20 +++++++--- 4 files changed, 83 insertions(+), 10 deletions(-) diff --git a/Library/Homebrew/cmd/uninstall.rb b/Library/Homebrew/cmd/uninstall.rb index 4b1c4bead7..18cebd71fb 100644 --- a/Library/Homebrew/cmd/uninstall.rb +++ b/Library/Homebrew/cmd/uninstall.rb @@ -54,6 +54,7 @@ module Homebrew Uninstall.uninstall_kegs( kegs_by_rack, + casks: casks, force: args.force?, ignore_dependencies: args.ignore_dependencies?, named_args: args.named, diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 8b7089bb57..76de4524c9 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -190,6 +190,57 @@ class Keg [all_required_kegs.to_a, all_dependents.sort] end + # Given an array of kegs, this method finds casks dependent on them + # except those included in the `casks` array. If it does, it returns: + # + # - kegs in the passed array that have dependent casks + # - installed casks dependent on them. + # + # If it doesn't, it returns nil. + # + # If all dependent casks are included in the `casks` array, the value + # returned is nil. + def self.find_cask_dependents(kegs, casks) + return if kegs.blank? + + casks_to_check = Cask::Caskroom.casks - casks + return if casks_to_check.blank? + + kegs_by_source = kegs.group_by do |keg| + f = keg.to_formula + [f.name, f.tap] + rescue + [keg.name, keg.tab.tap] + end + + all_required_kegs = Set.new + all_dependents = [] + + casks_to_check.each do |dependent| + dependencies = CaskDependent.new(dependent).runtime_dependencies + next if dependencies.blank? + + required = dependencies.map(&:to_formula) + + required_kegs = required.map do |f| + f_kegs = kegs_by_source[[f.name, f.tap]] + next unless f_kegs + + f_kegs.max_by(&:version) + end.compact + + next if required_kegs.blank? + + all_required_kegs += required_kegs + all_dependents << dependent.to_s + end + + return if all_required_kegs.blank? + return if all_dependents.blank? + + [all_required_kegs.to_a, all_dependents.sort] + end + # @param path if this is a file in a keg, returns the containing {Keg} object. def self.for(path) original_path = path diff --git a/Library/Homebrew/test/uninstall_spec.rb b/Library/Homebrew/test/uninstall_spec.rb index d5f54077f2..c6b0a5723d 100644 --- a/Library/Homebrew/test/uninstall_spec.rb +++ b/Library/Homebrew/test/uninstall_spec.rb @@ -5,31 +5,42 @@ require "uninstall" describe Homebrew::Uninstall do let(:dependency) { formula("dependency") { url "f-1" } } - let(:dependent) do - formula("dependent") do + + let(:dependent_formula) do + formula("dependent_formula") do url "f-1" depends_on "dependency" end end + let(:dependent_cask) do + Cask::CaskLoader.load(+<<-RUBY) + cask "dependent_cask" do + url "c-1" + depends_on formula: "dependency" + end + RUBY + end + let(:kegs_by_rack) { { dependency.rack => [Keg.new(dependency.latest_installed_prefix)] } } before do - [dependency, dependent].each do |f| + [dependency, dependent_formula].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.tabfile = dependent_formula.latest_installed_prefix/Tab::FILENAME tab.runtime_dependencies = [ { "full_name" => "dependency", "version" => "1" }, ] tab.write stub_formula_loader dependency - stub_formula_loader dependent + stub_formula_loader dependent_formula + stub_cask_loader dependent_cask end describe "::handle_unsatisfied_dependents" do diff --git a/Library/Homebrew/uninstall.rb b/Library/Homebrew/uninstall.rb index cabb18f627..b3da4f5082 100644 --- a/Library/Homebrew/uninstall.rb +++ b/Library/Homebrew/uninstall.rb @@ -10,8 +10,9 @@ module Homebrew module Uninstall module_function - def uninstall_kegs(kegs_by_rack, force: false, ignore_dependencies: false, named_args: []) + def uninstall_kegs(kegs_by_rack, casks: [], force: false, ignore_dependencies: false, named_args: []) handle_unsatisfied_dependents(kegs_by_rack, + casks: casks, ignore_dependencies: ignore_dependencies, named_args: named_args) return if Homebrew.failed? @@ -96,18 +97,27 @@ module Homebrew end end - def handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: false, named_args: []) + def handle_unsatisfied_dependents(kegs_by_rack, casks: [], 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) + check_for_dependents(all_kegs, casks: casks, 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) + def check_for_dependents(kegs, casks: [], named_args: []) + result_kegs = Keg.find_some_installed_dependents(kegs) + result_casks = Keg.find_cask_dependents(kegs, casks) + + return false if result_kegs.blank? && result_casks.blank? + + result = if result_kegs.present? && result_casks.present? + [(result_kegs[0] + result_casks[0]).uniq, result_kegs[1] + result_casks[1]] + else + result_kegs || result_casks + end if Homebrew::EnvConfig.developer? DeveloperDependentsMessage.new(*result, named_args: named_args).output From 1d204a08934ce17cf767d3543357954b33df204b Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Tue, 9 Feb 2021 16:07:49 +0530 Subject: [PATCH 2/5] test/uninstall_spec: fix dependent cask handling --- Library/Homebrew/test/uninstall_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Library/Homebrew/test/uninstall_spec.rb b/Library/Homebrew/test/uninstall_spec.rb index c6b0a5723d..a7c3288d50 100644 --- a/Library/Homebrew/test/uninstall_spec.rb +++ b/Library/Homebrew/test/uninstall_spec.rb @@ -16,6 +16,8 @@ describe Homebrew::Uninstall do let(:dependent_cask) do Cask::CaskLoader.load(+<<-RUBY) cask "dependent_cask" do + version "1.0.0" + url "c-1" depends_on formula: "dependency" end @@ -38,6 +40,8 @@ describe Homebrew::Uninstall do ] tab.write + Cask::Caskroom.path.join("dependent_cask", dependent_cask.version).mkpath + stub_formula_loader dependency stub_formula_loader dependent_formula stub_cask_loader dependent_cask From 56ebffff7a0555d7fbe97b4a9b4267bce8781795 Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Wed, 10 Feb 2021 11:41:48 +0530 Subject: [PATCH 3/5] Refactor code to find dependents of kegs --- Library/Homebrew/keg.rb | 72 +++++++---------------------------- Library/Homebrew/uninstall.rb | 11 +----- 2 files changed, 15 insertions(+), 68 deletions(-) diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 76de4524c9..493550ee05 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -137,18 +137,19 @@ class Keg LIBTOOL_EXTENSIONS = %w[.la .lai].freeze # Given an array of kegs, this method will try to find some other kegs - # that depend on them. If it does, it returns: + # or casks that depend on them. If it does, it returns: # # - some kegs in the passed array that have installed dependents # - some installed dependents of those kegs. # # If it doesn't, it returns nil. # - # Note that nil will be returned if the only installed dependents - # in the passed kegs are other kegs in the array. + # Note that nil will be returned if the only installed dependents of the + # passed kegs are other kegs in the array or casks present in the casks + # parameter. # # For efficiency, we don't bother trying to get complete data. - def self.find_some_installed_dependents(kegs) + def self.find_some_installed_dependents(kegs, casks: []) keg_names = kegs.select(&:optlinked?).map(&:name) keg_formulae = [] kegs_by_source = kegs.group_by do |keg| @@ -167,10 +168,16 @@ class Keg all_dependents = [] # Don't include dependencies of kegs that were in the given array. - formulae_to_check = Formula.installed - keg_formulae + dependents_to_check = (Formula.installed - keg_formulae) + (Cask::Caskroom.casks - casks) + + dependents_to_check.each do |dependent| + required = case dependent + when Formula + dependent.missing_dependencies(hide: keg_names) + when Cask::Cask + CaskDependent.new(dependent).runtime_dependencies.map(&:to_formula) + end - formulae_to_check.each do |dependent| - required = dependent.missing_dependencies(hide: keg_names) required_kegs = required.map do |f| f_kegs = kegs_by_source[[f.name, f.tap]] next unless f_kegs @@ -190,57 +197,6 @@ class Keg [all_required_kegs.to_a, all_dependents.sort] end - # Given an array of kegs, this method finds casks dependent on them - # except those included in the `casks` array. If it does, it returns: - # - # - kegs in the passed array that have dependent casks - # - installed casks dependent on them. - # - # If it doesn't, it returns nil. - # - # If all dependent casks are included in the `casks` array, the value - # returned is nil. - def self.find_cask_dependents(kegs, casks) - return if kegs.blank? - - casks_to_check = Cask::Caskroom.casks - casks - return if casks_to_check.blank? - - kegs_by_source = kegs.group_by do |keg| - f = keg.to_formula - [f.name, f.tap] - rescue - [keg.name, keg.tab.tap] - end - - all_required_kegs = Set.new - all_dependents = [] - - casks_to_check.each do |dependent| - dependencies = CaskDependent.new(dependent).runtime_dependencies - next if dependencies.blank? - - required = dependencies.map(&:to_formula) - - required_kegs = required.map do |f| - f_kegs = kegs_by_source[[f.name, f.tap]] - next unless f_kegs - - f_kegs.max_by(&:version) - end.compact - - next if required_kegs.blank? - - all_required_kegs += required_kegs - all_dependents << dependent.to_s - end - - return if all_required_kegs.blank? - return if all_dependents.blank? - - [all_required_kegs.to_a, all_dependents.sort] - end - # @param path if this is a file in a keg, returns the containing {Keg} object. def self.for(path) original_path = path diff --git a/Library/Homebrew/uninstall.rb b/Library/Homebrew/uninstall.rb index b3da4f5082..42d750b647 100644 --- a/Library/Homebrew/uninstall.rb +++ b/Library/Homebrew/uninstall.rb @@ -108,16 +108,7 @@ module Homebrew end def check_for_dependents(kegs, casks: [], named_args: []) - result_kegs = Keg.find_some_installed_dependents(kegs) - result_casks = Keg.find_cask_dependents(kegs, casks) - - return false if result_kegs.blank? && result_casks.blank? - - result = if result_kegs.present? && result_casks.present? - [(result_kegs[0] + result_casks[0]).uniq, result_kegs[1] + result_casks[1]] - else - result_kegs || result_casks - end + return false unless result = Keg.find_some_installed_dependents(kegs, casks: casks) if Homebrew::EnvConfig.developer? DeveloperDependentsMessage.new(*result, named_args: named_args).output From fcb1aa8aef1fa09714166a60b887226d7a189d1f Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Wed, 10 Feb 2021 12:05:12 +0530 Subject: [PATCH 4/5] test/keg_spec: add test to check for dependent casks --- Library/Homebrew/test/keg_spec.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Library/Homebrew/test/keg_spec.rb b/Library/Homebrew/test/keg_spec.rb index c64d825987..f4e81ee99c 100644 --- a/Library/Homebrew/test/keg_spec.rb +++ b/Library/Homebrew/test/keg_spec.rb @@ -455,5 +455,31 @@ describe Keg do dependencies [{ "full_name" => "foo", "version" => "1.1" }] # different version expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) end + + def stub_cask_name(name, version, dependency) + c = Cask::CaskLoader.load(+<<-RUBY) + cask "#{name}" do + version "#{version}" + + url "c-1" + depends_on formula: "#{dependency}" + end + RUBY + + stub_cask_loader c + c + end + + def setup_test_cask(name, version, dependency) + c = stub_cask_name(name, version, dependency) + Cask::Caskroom.path.join(name, c.version).mkpath + c + end + + specify "identify dependent casks" do + setup_test_cask("qux", "1.0.0", "foo") + dependents = described_class.find_some_installed_dependents([keg]).last + expect(dependents.include?("qux")).to eq(true) + end end end From f7f94334431d5ece07ec4cf773c2f2ed94beb02f Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Fri, 12 Feb 2021 01:42:17 +0530 Subject: [PATCH 5/5] Keg.find_some_installed_dependents: move to installed_dependents.rb --- Library/Homebrew/installed_dependents.rb | 74 ++++++++ Library/Homebrew/keg.rb | 61 ------ .../test/installed_dependents_spec.rb | 179 ++++++++++++++++++ Library/Homebrew/test/keg_spec.rb | 160 ---------------- Library/Homebrew/uninstall.rb | 4 +- 5 files changed, 255 insertions(+), 223 deletions(-) create mode 100644 Library/Homebrew/installed_dependents.rb create mode 100644 Library/Homebrew/test/installed_dependents_spec.rb diff --git a/Library/Homebrew/installed_dependents.rb b/Library/Homebrew/installed_dependents.rb new file mode 100644 index 0000000000..20a7440e7d --- /dev/null +++ b/Library/Homebrew/installed_dependents.rb @@ -0,0 +1,74 @@ +# typed: false +# frozen_string_literal: true + +require "cask_dependent" + +# Helper functions for installed dependents. +# +# @api private +module InstalledDependents + extend T::Sig + + module_function + + # Given an array of kegs, this method will try to find some other kegs + # or casks that depend on them. If it does, it returns: + # + # - some kegs in the passed array that have installed dependents + # - some installed dependents of those kegs. + # + # If it doesn't, it returns nil. + # + # Note that nil will be returned if the only installed dependents of the + # passed kegs are other kegs in the array or casks present in the casks + # parameter. + # + # For efficiency, we don't bother trying to get complete data. + def find_some_installed_dependents(kegs, casks: []) + keg_names = kegs.select(&:optlinked?).map(&:name) + keg_formulae = [] + kegs_by_source = kegs.group_by do |keg| + # First, attempt to resolve the keg to a formula + # to get up-to-date name and tap information. + f = keg.to_formula + keg_formulae << f + [f.name, f.tap] + rescue + # If the formula for the keg can't be found, + # fall back to the information in the tab. + [keg.name, keg.tab.tap] + end + + all_required_kegs = Set.new + all_dependents = [] + + # Don't include dependencies of kegs that were in the given array. + dependents_to_check = (Formula.installed - keg_formulae) + (Cask::Caskroom.casks - casks) + + dependents_to_check.each do |dependent| + required = case dependent + when Formula + dependent.missing_dependencies(hide: keg_names) + when Cask::Cask + CaskDependent.new(dependent).runtime_dependencies.map(&:to_formula) + end + + required_kegs = required.map do |f| + f_kegs = kegs_by_source[[f.name, f.tap]] + next unless f_kegs + + f_kegs.max_by(&:version) + end.compact + + next if required_kegs.empty? + + all_required_kegs += required_kegs + all_dependents << dependent.to_s + end + + return if all_required_kegs.empty? + return if all_dependents.empty? + + [all_required_kegs.to_a, all_dependents.sort] + end +end diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 493550ee05..c5bad0889b 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -136,67 +136,6 @@ class Keg PYC_EXTENSIONS = %w[.pyc .pyo].freeze LIBTOOL_EXTENSIONS = %w[.la .lai].freeze - # Given an array of kegs, this method will try to find some other kegs - # or casks that depend on them. If it does, it returns: - # - # - some kegs in the passed array that have installed dependents - # - some installed dependents of those kegs. - # - # If it doesn't, it returns nil. - # - # Note that nil will be returned if the only installed dependents of the - # passed kegs are other kegs in the array or casks present in the casks - # parameter. - # - # For efficiency, we don't bother trying to get complete data. - def self.find_some_installed_dependents(kegs, casks: []) - keg_names = kegs.select(&:optlinked?).map(&:name) - keg_formulae = [] - kegs_by_source = kegs.group_by do |keg| - # First, attempt to resolve the keg to a formula - # to get up-to-date name and tap information. - f = keg.to_formula - keg_formulae << f - [f.name, f.tap] - rescue - # If the formula for the keg can't be found, - # fall back to the information in the tab. - [keg.name, keg.tab.tap] - end - - all_required_kegs = Set.new - all_dependents = [] - - # Don't include dependencies of kegs that were in the given array. - dependents_to_check = (Formula.installed - keg_formulae) + (Cask::Caskroom.casks - casks) - - dependents_to_check.each do |dependent| - required = case dependent - when Formula - dependent.missing_dependencies(hide: keg_names) - when Cask::Cask - CaskDependent.new(dependent).runtime_dependencies.map(&:to_formula) - end - - required_kegs = required.map do |f| - f_kegs = kegs_by_source[[f.name, f.tap]] - next unless f_kegs - - f_kegs.max_by(&:version) - end.compact - - next if required_kegs.empty? - - all_required_kegs += required_kegs - all_dependents << dependent.to_s - end - - return if all_required_kegs.empty? - return if all_dependents.empty? - - [all_required_kegs.to_a, all_dependents.sort] - end - # @param path if this is a file in a keg, returns the containing {Keg} object. def self.for(path) original_path = path diff --git a/Library/Homebrew/test/installed_dependents_spec.rb b/Library/Homebrew/test/installed_dependents_spec.rb new file mode 100644 index 0000000000..824c6f2d94 --- /dev/null +++ b/Library/Homebrew/test/installed_dependents_spec.rb @@ -0,0 +1,179 @@ +# typed: false +# frozen_string_literal: true + +require "installed_dependents" + +describe InstalledDependents do + include FileUtils + + def setup_test_keg(name, version) + path = HOMEBREW_CELLAR/name/version + (path/"bin").mkpath + + %w[hiworld helloworld goodbye_cruel_world].each do |file| + touch path/"bin"/file + end + + Keg.new(path) + end + + let!(:keg) { setup_test_keg("foo", "1.0") } + + describe "::find_some_installed_dependents" do + def stub_formula_name(name) + f = formula(name) { url "foo-1.0" } + stub_formula_loader f + stub_formula_loader f, "homebrew/core/#{f}" + f + end + + def setup_test_keg(name, version) + f = stub_formula_name(name) + keg = super + Tab.create(f, DevelopmentTools.default_compiler, :libcxx).write + keg + end + + before do + keg.link + end + + def alter_tab(keg = dependent) + tab = Tab.for_keg(keg) + yield tab + tab.write + end + + # 1.1.6 is the earliest version of Homebrew that generates correct runtime + # dependency lists in {Tab}s. + def dependencies(deps, homebrew_version: "1.1.6") + alter_tab do |tab| + tab.homebrew_version = homebrew_version + tab.tabfile = dependent/Tab::FILENAME + tab.runtime_dependencies = deps + end + end + + def unreliable_dependencies(deps) + # 1.1.5 is (hopefully!) the last version of Homebrew that generates + # incorrect runtime dependency lists in {Tab}s. + dependencies(deps, homebrew_version: "1.1.5") + end + + let(:dependent) { setup_test_keg("bar", "1.0") } + + specify "a dependency with no Tap in Tab" do + tap_dep = setup_test_keg("baz", "1.0") + + # allow tap_dep to be linked too + FileUtils.rm_r tap_dep/"bin" + tap_dep.link + + alter_tab(keg) { |t| t.source["tap"] = nil } + + dependencies nil + Formula["bar"].class.depends_on "foo" + Formula["bar"].class.depends_on "baz" + + result = described_class.find_some_installed_dependents([keg, tap_dep]) + expect(result).to eq([[keg, tap_dep], ["bar"]]) + end + + specify "no dependencies anywhere" do + dependencies nil + expect(described_class.find_some_installed_dependents([keg])).to be nil + end + + specify "missing Formula dependency" do + dependencies nil + Formula["bar"].class.depends_on "foo" + expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) + end + + specify "uninstalling dependent and dependency" do + dependencies nil + Formula["bar"].class.depends_on "foo" + expect(described_class.find_some_installed_dependents([keg, dependent])).to be nil + end + + specify "renamed dependency" do + dependencies nil + + stub_formula_loader Formula["foo"], "homebrew/core/foo-old" + renamed_path = HOMEBREW_CELLAR/"foo-old" + (HOMEBREW_CELLAR/"foo").rename(renamed_path) + renamed_keg = Keg.new(renamed_path/"1.0") + + Formula["bar"].class.depends_on "foo" + + result = described_class.find_some_installed_dependents([renamed_keg]) + expect(result).to eq([[renamed_keg], ["bar"]]) + end + + specify "empty dependencies in Tab" do + dependencies [] + expect(described_class.find_some_installed_dependents([keg])).to be nil + end + + specify "same name but different version in Tab" do + dependencies [{ "full_name" => "foo", "version" => "1.1" }] + expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) + end + + specify "different name and same version in Tab" do + stub_formula_name("baz") + dependencies [{ "full_name" => "baz", "version" => keg.version.to_s }] + expect(described_class.find_some_installed_dependents([keg])).to be nil + end + + specify "same name and version in Tab" do + dependencies [{ "full_name" => "foo", "version" => "1.0" }] + expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) + end + + specify "fallback for old versions" do + unreliable_dependencies [{ "full_name" => "baz", "version" => "1.0" }] + Formula["bar"].class.depends_on "foo" + expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) + end + + specify "non-opt-linked" do + keg.remove_opt_record + dependencies [{ "full_name" => "foo", "version" => "1.0" }] + expect(described_class.find_some_installed_dependents([keg])).to be nil + end + + specify "keg-only" do + keg.unlink + Formula["foo"].class.keg_only "a good reason" + dependencies [{ "full_name" => "foo", "version" => "1.1" }] # different version + expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) + end + + def stub_cask_name(name, version, dependency) + c = Cask::CaskLoader.load(+<<-RUBY) + cask "#{name}" do + version "#{version}" + + url "c-1" + depends_on formula: "#{dependency}" + end + RUBY + + stub_cask_loader c + c + end + + def setup_test_cask(name, version, dependency) + c = stub_cask_name(name, version, dependency) + Cask::Caskroom.path.join(name, c.version).mkpath + c + end + + specify "identify dependent casks" do + setup_test_cask("qux", "1.0.0", "foo") + dependents = described_class.find_some_installed_dependents([keg]).last + expect(dependents.include?("qux")).to eq(true) + end + end +end diff --git a/Library/Homebrew/test/keg_spec.rb b/Library/Homebrew/test/keg_spec.rb index f4e81ee99c..1b82ada38b 100644 --- a/Library/Homebrew/test/keg_spec.rb +++ b/Library/Homebrew/test/keg_spec.rb @@ -5,8 +5,6 @@ require "keg" require "stringio" describe Keg do - include FileUtils - def setup_test_keg(name, version) path = HOMEBREW_CELLAR/name/version (path/"bin").mkpath @@ -324,162 +322,4 @@ describe Keg do keg.unlink expect(keg).not_to be_linked end - - describe "::find_some_installed_dependents" do - def stub_formula_name(name) - f = formula(name) { url "foo-1.0" } - stub_formula_loader f - stub_formula_loader f, "homebrew/core/#{f}" - f - end - - def setup_test_keg(name, version) - f = stub_formula_name(name) - keg = super - Tab.create(f, DevelopmentTools.default_compiler, :libcxx).write - keg - end - - before do - keg.link - end - - def alter_tab(keg = dependent) - tab = Tab.for_keg(keg) - yield tab - tab.write - end - - # 1.1.6 is the earliest version of Homebrew that generates correct runtime - # dependency lists in {Tab}s. - def dependencies(deps, homebrew_version: "1.1.6") - alter_tab do |tab| - tab.homebrew_version = homebrew_version - tab.tabfile = dependent/Tab::FILENAME - tab.runtime_dependencies = deps - end - end - - def unreliable_dependencies(deps) - # 1.1.5 is (hopefully!) the last version of Homebrew that generates - # incorrect runtime dependency lists in {Tab}s. - dependencies(deps, homebrew_version: "1.1.5") - end - - let(:dependent) { setup_test_keg("bar", "1.0") } - - specify "a dependency with no Tap in Tab" do - tap_dep = setup_test_keg("baz", "1.0") - - # allow tap_dep to be linked too - FileUtils.rm_r tap_dep/"bin" - tap_dep.link - - alter_tab(keg) { |t| t.source["tap"] = nil } - - dependencies nil - Formula["bar"].class.depends_on "foo" - Formula["bar"].class.depends_on "baz" - - result = described_class.find_some_installed_dependents([keg, tap_dep]) - expect(result).to eq([[keg, tap_dep], ["bar"]]) - end - - specify "no dependencies anywhere" do - dependencies nil - expect(described_class.find_some_installed_dependents([keg])).to be nil - end - - specify "missing Formula dependency" do - dependencies nil - Formula["bar"].class.depends_on "foo" - expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) - end - - specify "uninstalling dependent and dependency" do - dependencies nil - Formula["bar"].class.depends_on "foo" - expect(described_class.find_some_installed_dependents([keg, dependent])).to be nil - end - - specify "renamed dependency" do - dependencies nil - - stub_formula_loader Formula["foo"], "homebrew/core/foo-old" - renamed_path = HOMEBREW_CELLAR/"foo-old" - (HOMEBREW_CELLAR/"foo").rename(renamed_path) - renamed_keg = described_class.new(renamed_path/"1.0") - - Formula["bar"].class.depends_on "foo" - - result = described_class.find_some_installed_dependents([renamed_keg]) - expect(result).to eq([[renamed_keg], ["bar"]]) - end - - specify "empty dependencies in Tab" do - dependencies [] - expect(described_class.find_some_installed_dependents([keg])).to be nil - end - - specify "same name but different version in Tab" do - dependencies [{ "full_name" => "foo", "version" => "1.1" }] - expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) - end - - specify "different name and same version in Tab" do - stub_formula_name("baz") - dependencies [{ "full_name" => "baz", "version" => keg.version.to_s }] - expect(described_class.find_some_installed_dependents([keg])).to be nil - end - - specify "same name and version in Tab" do - dependencies [{ "full_name" => "foo", "version" => "1.0" }] - expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) - end - - specify "fallback for old versions" do - unreliable_dependencies [{ "full_name" => "baz", "version" => "1.0" }] - Formula["bar"].class.depends_on "foo" - expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) - end - - specify "non-opt-linked" do - keg.remove_opt_record - dependencies [{ "full_name" => "foo", "version" => "1.0" }] - expect(described_class.find_some_installed_dependents([keg])).to be nil - end - - specify "keg-only" do - keg.unlink - Formula["foo"].class.keg_only "a good reason" - dependencies [{ "full_name" => "foo", "version" => "1.1" }] # different version - expect(described_class.find_some_installed_dependents([keg])).to eq([[keg], ["bar"]]) - end - - def stub_cask_name(name, version, dependency) - c = Cask::CaskLoader.load(+<<-RUBY) - cask "#{name}" do - version "#{version}" - - url "c-1" - depends_on formula: "#{dependency}" - end - RUBY - - stub_cask_loader c - c - end - - def setup_test_cask(name, version, dependency) - c = stub_cask_name(name, version, dependency) - Cask::Caskroom.path.join(name, c.version).mkpath - c - end - - specify "identify dependent casks" do - setup_test_cask("qux", "1.0.0", "foo") - dependents = described_class.find_some_installed_dependents([keg]).last - expect(dependents.include?("qux")).to eq(true) - end - end end diff --git a/Library/Homebrew/uninstall.rb b/Library/Homebrew/uninstall.rb index 42d750b647..aa1c5ed302 100644 --- a/Library/Homebrew/uninstall.rb +++ b/Library/Homebrew/uninstall.rb @@ -1,7 +1,7 @@ # typed: true # frozen_string_literal: true -require "keg" +require "installed_dependents" module Homebrew # Helper module for uninstalling kegs. @@ -108,7 +108,7 @@ module Homebrew end def check_for_dependents(kegs, casks: [], named_args: []) - return false unless result = Keg.find_some_installed_dependents(kegs, casks: casks) + return false unless result = InstalledDependents.find_some_installed_dependents(kegs, casks: casks) if Homebrew::EnvConfig.developer? DeveloperDependentsMessage.new(*result, named_args: named_args).output