Skip to content

Commit

Permalink
Merge pull request #10575 from nandahkrishna/dependent-cask-check
Browse files Browse the repository at this point in the history
cmd/uninstall: check for dependent casks
  • Loading branch information
MikeMcQuaid authored Feb 12, 2021
2 parents d6cdc88 + f7f9433 commit f9b4b69
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 199 deletions.
1 change: 1 addition & 0 deletions Library/Homebrew/cmd/uninstall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def uninstall

Uninstall.uninstall_kegs(
kegs_by_rack,
casks: casks,
force: args.force?,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named,
Expand Down
74 changes: 74 additions & 0 deletions Library/Homebrew/installed_dependents.rb
Original file line number Diff line number Diff line change
@@ -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
54 changes: 0 additions & 54 deletions Library/Homebrew/keg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,60 +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
# 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.
#
# For efficiency, we don't bother trying to get complete data.
def self.find_some_installed_dependents(kegs)
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.
formulae_to_check = Formula.installed - keg_formulae

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

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
Expand Down
179 changes: 179 additions & 0 deletions Library/Homebrew/test/installed_dependents_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit f9b4b69

Please sign in to comment.