From f7f94334431d5ece07ec4cf773c2f2ed94beb02f Mon Sep 17 00:00:00 2001 From: nandahkrishna Date: Fri, 12 Feb 2021 01:42:17 +0530 Subject: [PATCH] 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 0000000000000..20a7440e7d421 --- /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 493550ee05177..c5bad0889ba1d 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -136,67 +136,6 @@ def to_s 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 0000000000000..824c6f2d947dc --- /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 f4e81ee99c39c..1b82ada38bce4 100644 --- a/Library/Homebrew/test/keg_spec.rb +++ b/Library/Homebrew/test/keg_spec.rb @@ -5,8 +5,6 @@ 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 @@ def setup_test_keg(name, version) 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 42d750b6470b9..aa1c5ed30200c 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 @@ def handle_unsatisfied_dependents(kegs_by_rack, casks: [], ignore_dependencies: 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