Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/uninstall: check for dependent casks #10575

Merged
merged 5 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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