Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
14fcabb
Fix runtime dependencies handling in autoremove
wu21-web Feb 14, 2026
e03ca51
Fix indentation and syntax in autoremove.rb
wu21-web Feb 14, 2026
7a8d246
Fix tests by mocking runtime_dependencies
wu21-web Feb 14, 2026
aa0396c
Update Library/Homebrew/test/utils/autoremove_spec.rb
wu21-web Feb 14, 2026
a496661
Update Library/Homebrew/utils/autoremove.rb
wu21-web Feb 14, 2026
7310719
Update Library/Homebrew/utils/autoremove.rb
wu21-web Feb 14, 2026
46674a9
Update Library/Homebrew/utils/autoremove.rb
wu21-web Feb 14, 2026
0ca6f7e
Refactor Utils::Autoremove
wu21-web Feb 14, 2026
2b5b2fe
Update autoremove::utils module
wu21-web Feb 14, 2026
cd7357b
Update autoremove.rb
wu21-web Feb 14, 2026
828b5a0
Refactor runtime dependency handling in autoremove.rb
wu21-web Feb 15, 2026
7ed0c83
Refactor autoremove.rb
wu21-web Feb 15, 2026
2021732
Update autoremove.rb
wu21-web Feb 15, 2026
52523c4
Update autoremove.rb
wu21-web Feb 15, 2026
15b5a11
Update actions/cache version in tests workflow
wu21-web Feb 15, 2026
fd2d11b
Merge branch 'Homebrew:main' into main
wu21-web Feb 16, 2026
83b5743
Update tests.yml to remove uninstall option
wu21-web Feb 16, 2026
6c96ca8
Add uninstall option to Homebrew setup in tests.yml
wu21-web Feb 16, 2026
44c3654
Enable uninstall for shellcheck and shfmt
wu21-web Feb 16, 2026
ffccd42
Refactor autoremove_spec.rb for clarity and correctness
wu21-web Feb 16, 2026
f5c8b44
restore workflow tests file
wu21-web Feb 16, 2026
5051864
restore autoremove_spec.rb
wu21-web Feb 16, 2026
e80a385
restore autoremove docs and aligh dependency checks
wu21-web Feb 16, 2026
87c8ad2
autoremove: use installed dependents in cleanup
wu21-web Feb 16, 2026
e80fe05
Refactor autoremove.rb to clean up code and logic
wu21-web Feb 16, 2026
c451d2d
Mock runtime dependencies in autoremove_spec.rb
wu21-web Feb 16, 2026
1ca0154
Add comments to autoremove.rb
wu21-web Feb 16, 2026
47f1e76
Update logic in autoremove.rb
wu21-web Feb 17, 2026
8e6843c
Update keg instances to include optlinked? attribute
wu21-web Feb 17, 2026
d747024
Simplify conditional logic in formulae_to_keep assignment
wu21-web Feb 17, 2026
e311a2f
Merge branch 'Homebrew:main' into main
wu21-web Feb 20, 2026
377b39c
fix(autoremove): improve handling of runtime dependencies and non-bot…
wu21-web Feb 21, 2026
8dfe595
Merge branch 'Homebrew:main' into main
wu21-web Feb 21, 2026
d50cfe3
Update autoremove.rb to remove 'installed dependents'
wu21-web Feb 21, 2026
fd87727
Update autoremove_spec.rb to remove unused dependencies
wu21-web Feb 21, 2026
fc6dd31
include comments to clarify autoremove logic
wu21-web Feb 21, 2026
5047c54
Update Library/Homebrew/utils/autoremove.rb
wu21-web Feb 21, 2026
edd4a4f
Update autoremove_spec.rb and autoremove.rb to enhance dependency han…
wu21-web Feb 21, 2026
5f469ff
Update Library/Homebrew/test/utils/autoremove_spec.rb
wu21-web Feb 21, 2026
9e6888b
Update Library/Homebrew/utils/autoremove.rb
wu21-web Feb 21, 2026
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
60 changes: 56 additions & 4 deletions Library/Homebrew/test/utils/autoremove_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,31 @@
end

let(:tab_from_keg) { instance_double(Tab) }
let(:keg_for_formula_with_deps) { instance_double(Keg, tab: tab_from_keg, optlinked?: true) }
let(:keg_for_first_dep) { instance_double(Keg, tab: tab_from_keg, optlinked?: true) }
let(:keg_for_second_dep) { instance_double(Keg, tab: tab_from_keg, optlinked?: true) }
let(:keg_for_build_dep) { instance_double(Keg, tab: tab_from_keg, optlinked?: true) }

before do
allow(formula_with_deps).to receive_messages(
installed_runtime_formula_dependencies: [first_formula_dep, second_formula_dep],
any_installed_keg: instance_double(Keg, tab: tab_from_keg),
runtime_installed_formula_dependents: [],
any_installed_keg: keg_for_formula_with_deps,
)
allow(first_formula_dep).to receive_messages(
installed_runtime_formula_dependencies: [second_formula_dep],
any_installed_keg: instance_double(Keg, tab: tab_from_keg),
runtime_installed_formula_dependents: [],
any_installed_keg: keg_for_first_dep,
)
allow(second_formula_dep).to receive_messages(
installed_runtime_formula_dependencies: [],
any_installed_keg: instance_double(Keg, tab: tab_from_keg),
runtime_installed_formula_dependents: [],
any_installed_keg: keg_for_second_dep,
)
allow(formula_is_build_dep).to receive_messages(
installed_runtime_formula_dependencies: [],
any_installed_keg: instance_double(Keg, tab: tab_from_keg),
runtime_installed_formula_dependents: [],
any_installed_keg: keg_for_build_dep,
)
end
end
Expand All @@ -67,6 +75,21 @@
before do
allow(Formulary).to receive(:factory).with("three", { warn: false })
.and_return(formula_is_build_dep)
allow(formula_with_deps).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([
instance_double(Dependency, to_formula: first_formula_dep),
instance_double(Dependency, to_formula: second_formula_dep),
])
allow(first_formula_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
allow(second_formula_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
allow(formula_is_build_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
end

context "when formulae are bottles" do
Expand All @@ -78,6 +101,23 @@
end
end

context "when a formula has runtime dependencies according to current definition" do
it "keeps the dependency" do
allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true)
allow(formula_with_deps).to receive(:installed_runtime_formula_dependencies)
.and_return([])
# Set up pango-like scenario: formula_with_deps depends on second_formula_dep
dep_requirement = instance_double(Dependency)
allow(dep_requirement).to receive(:to_formula).and_return(second_formula_dep)
allow(formula_with_deps).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([dep_requirement])

expect(described_class.send(:bottled_formulae_with_no_formula_dependents, formulae))
.not_to include(second_formula_dep)
end
end

context "when formulae are built from source" do
it "filters out formulae" do
allow(tab_from_keg).to receive(:poured_from_bottle).and_return(false)
Expand All @@ -93,6 +133,18 @@

before do
allow(tab_from_keg).to receive(:poured_from_bottle).and_return(true)
allow(formula_with_deps).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
allow(first_formula_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
allow(second_formula_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
allow(formula_is_build_dep).to receive(:runtime_dependencies)
.with(read_from_tab: false, undeclared: false)
.and_return([])
end

specify "installed on request" do
Expand Down
50 changes: 25 additions & 25 deletions Library/Homebrew/utils/autoremove.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ module Utils
# @private
module Autoremove
class << self
# 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
sig { params(formulae: T::Array[Formula], casks: T::Array[Cask::Cask]).returns(T::Array[Formula]) }
def removable_formulae(formulae, casks)
unused_formulae = unused_formulae_with_no_formula_dependents(formulae)
Expand All @@ -19,8 +15,6 @@ def removable_formulae(formulae, casks)

private

# An array of all installed {Formula} with {Cask} dependents.
# @private
sig { params(casks: T::Array[Cask::Cask]).returns(T::Array[Formula]) }
def formulae_with_cask_dependents(casks)
casks.flat_map { |cask| cask.depends_on[:formula] }.compact.flat_map do |name|
Expand All @@ -35,45 +29,51 @@ def formulae_with_cask_dependents(casks)
end
end

# An array of all installed bottled {Formula} without runtime {Formula}
# dependents for bottles and without build {Formula} dependents
# for those built from source.
# @private
sig { params(formulae: T::Array[Formula]).returns(T::Array[Formula]) }
# Returns the subset of the given formulae that are bottled and have no
# other formulae depending on them, based on current formula definitions.
# Formulae that are runtime dependencies, or that were built from source
# (and their build dependencies), are filtered out and kept.
def bottled_formulae_with_no_formula_dependents(formulae)
formulae_to_keep = T.let([], T::Array[Formula])
formulae.each do |formula|
formulae_to_keep += formula.installed_runtime_formula_dependencies
keg = formula.any_installed_keg
# Include runtime deps from the current formula definition.
formulae_to_keep += formula.runtime_dependencies(read_from_tab: false,
undeclared: false).filter_map do |dep|
dep.to_formula
rescue FormulaUnavailableError
nil
end

if (tab = formula.any_installed_keg&.tab)
# Ignore build dependencies when the formula is a bottle
next if tab.poured_from_bottle
tab = keg&.tab
next unless tab
next if tab.poured_from_bottle

# Keep the formula if it was built from source
formulae_to_keep << formula
end
# Keep non-bottled formulae and their build dependencies
formulae_to_keep << formula

formula.deps.select(&:build?).each do |dep|
formulae_to_keep << dep.to_formula
formulae_to_keep += formula.deps.select(&:build?).filter_map do |dep|
dep.to_formula
rescue FormulaUnavailableError
# do nothing
nil
end
end
names_to_keep = formulae_to_keep.to_set(&:name)
formulae.reject { |f| names_to_keep.include?(f.name) }
end

# Recursive function that returns an array of {Formula} without
# {Formula} dependents that weren't installed on request.
# 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
sig { params(formulae: T::Array[Formula]).returns(T::Array[Formula]) }
def unused_formulae_with_no_formula_dependents(formulae)
unused_formulae = bottled_formulae_with_no_formula_dependents(formulae).select do |f|
tab = f.any_installed_keg&.tab
next unless tab
next unless tab.installed_on_request_present?
next false unless tab

tab.installed_on_request == false
tab.installed_on_request_present? && tab.installed_on_request == false
end

unless unused_formulae.empty?
Expand Down
Loading