Skip to content

Commit fe0a384

Browse files
authored
Merge pull request #21351 from AbdelrahmanHafez/fix/bundle-cleanup
fix(bundle): prevent autoremove from removing Brewfile packages during cleanup
2 parents ce39ce9 + 6cd9c69 commit fe0a384

6 files changed

Lines changed: 136 additions & 2 deletions

File tree

Library/Homebrew/bundle.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,32 @@ def reset!
173173
@formula_versions_from_env = T.let(nil, T.nilable(T::Hash[String, String]))
174174
@upgrade_formulae = T.let(nil, T.nilable(T::Array[String]))
175175
end
176+
177+
# Marks Brewfile formulae as installed_on_request to prevent autoremove
178+
# from removing them when their dependents are uninstalled.
179+
sig { params(entries: T::Array[Dsl::Entry]).void }
180+
def mark_as_installed_on_request!(entries)
181+
return if entries.empty?
182+
183+
require "tab"
184+
185+
installed_formulae = Formula.installed_formula_names
186+
return if installed_formulae.empty?
187+
188+
entries.each do |entry|
189+
next if entry.type != :brew
190+
191+
name = entry.name
192+
next if installed_formulae.exclude?(name)
193+
194+
tab = Tab.for_name(name)
195+
next if tab.tabfile.blank? || !tab.tabfile.exist?
196+
next if tab.installed_on_request
197+
198+
tab.installed_on_request = true
199+
tab.write
200+
end
201+
end
176202
end
177203
end
178204
end

Library/Homebrew/bundle/commands/cleanup.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
4444
end
4545

4646
if formulae.any?
47+
# Mark Brewfile formulae as installed_on_request to prevent autoremove
48+
# from removing them when their dependents are uninstalled
49+
require "bundle/brewfile"
50+
@dsl ||= Brewfile.read(global:, file:)
51+
Homebrew::Bundle.mark_as_installed_on_request!(@dsl.entries)
52+
4753
Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--formula", "--force", *formulae
4854
puts "Uninstalled #{formulae.size} formula#{"e" if formulae.size != 1}"
4955
end

Library/Homebrew/bundle/commands/install.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ module Install
2222
def self.run(global: false, file: nil, no_lock: false, no_upgrade: false, verbose: false, force: false,
2323
quiet: false)
2424
@dsl = Brewfile.read(global:, file:)
25-
Homebrew::Bundle::Installer.install!(
25+
result = Homebrew::Bundle::Installer.install!(
2626
@dsl.entries,
2727
global:, file:, no_lock:, no_upgrade:, verbose:, force:, quiet:,
28-
) || exit(1)
28+
)
29+
30+
# Mark Brewfile formulae as installed_on_request to prevent autoremove
31+
# from removing them when their dependents are uninstalled
32+
Homebrew::Bundle.mark_as_installed_on_request!(@dsl.entries)
33+
34+
result || exit(1)
2935
end
3036

3137
sig { returns(T.nilable(Dsl)) }

Library/Homebrew/test/bundle/bundle_spec.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "bundle"
4+
require "bundle/dsl"
45

56
RSpec.describe Homebrew::Bundle do
67
context "when the system call succeeds" do
@@ -46,4 +47,63 @@
4647
expect(described_class.mas_installed?).to be(true)
4748
end
4849
end
50+
51+
describe ".mark_as_installed_on_request!", :no_api do
52+
subject(:mark_installed!) { described_class.mark_as_installed_on_request!(entries) }
53+
54+
let(:entries) { dsl.entries }
55+
let(:dsl) { Homebrew::Bundle::Dsl.new(Pathname.new("/fake/Brewfile")) }
56+
let(:tabfile) { Pathname.new("/fake/INSTALL_RECEIPT.json") }
57+
58+
before do
59+
allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false)
60+
allow_any_instance_of(Pathname).to receive(:read).and_return(brewfile_content)
61+
allow(tabfile).to receive_messages(blank?: false, exist?: true)
62+
end
63+
64+
context "when formula is installed but not marked as installed_on_request" do
65+
let(:brewfile_content) { "brew 'myformula'" }
66+
let(:tab) { instance_double(Tab, installed_on_request: false, tabfile:) }
67+
68+
before do
69+
allow(Formula).to receive(:installed_formula_names).and_return(["myformula"])
70+
allow(Tab).to receive(:for_name).with("myformula").and_return(tab)
71+
end
72+
73+
it "sets installed_on_request=true and writes" do
74+
expect(tab).to receive(:installed_on_request=).with(true)
75+
expect(tab).to receive(:write)
76+
mark_installed!
77+
end
78+
end
79+
80+
context "when formula is not installed" do
81+
let(:brewfile_content) { "brew 'notinstalled'" }
82+
83+
before do
84+
allow(Formula).to receive(:installed_formula_names).and_return([])
85+
end
86+
87+
it "skips the formula" do
88+
expect(Tab).not_to receive(:for_name)
89+
mark_installed!
90+
end
91+
end
92+
93+
context "when formula is already marked as installed_on_request" do
94+
let(:brewfile_content) { "brew 'alreadymarked'" }
95+
let(:tab) { instance_double(Tab, installed_on_request: true, tabfile:) }
96+
97+
before do
98+
allow(Formula).to receive(:installed_formula_names).and_return(["alreadymarked"])
99+
allow(Tab).to receive(:for_name).with("alreadymarked").and_return(tab)
100+
end
101+
102+
it "skips writing" do
103+
expect(tab).not_to receive(:installed_on_request=)
104+
expect(tab).not_to receive(:write)
105+
mark_installed!
106+
end
107+
end
108+
end
49109
end

Library/Homebrew/test/bundle/commands/cleanup_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@
211211
taps_to_untap: [],
212212
vscode_extensions_to_uninstall: [],
213213
flatpaks_to_uninstall: [])
214+
allow(Homebrew::Bundle).to receive(:mark_as_installed_on_request!)
215+
allow_any_instance_of(Pathname).to receive(:read).and_return("")
214216
end
215217

216218
it "uninstalls formulae" do
@@ -357,4 +359,25 @@
357359
described_class.system_output_no_stderr("true")
358360
end
359361
end
362+
363+
context "when running with force" do
364+
before do
365+
described_class.reset!
366+
allow(described_class).to receive_messages(
367+
casks_to_uninstall: [],
368+
formulae_to_uninstall: %w[some_formula],
369+
taps_to_untap: [],
370+
vscode_extensions_to_uninstall: [],
371+
flatpaks_to_uninstall: [],
372+
)
373+
allow(Kernel).to receive(:system)
374+
allow(described_class).to receive(:system_output_no_stderr).and_return("")
375+
allow_any_instance_of(Pathname).to receive(:read).and_return("")
376+
end
377+
378+
it "marks Brewfile formulae as installed_on_request before uninstalling" do
379+
expect(Homebrew::Bundle).to receive(:mark_as_installed_on_request!)
380+
described_class.run(force: true)
381+
end
382+
end
360383
end

Library/Homebrew/test/bundle/commands/install_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,18 @@
9292

9393
expect { described_class.run }.to raise_error(SystemExit)
9494
end
95+
96+
it "marks Brewfile formulae as installed_on_request after installing" do
97+
allow(Homebrew::Bundle::TapInstaller).to receive(:preinstall!).and_return(false)
98+
allow(Homebrew::Bundle::VscodeExtensionInstaller).to receive(:preinstall!).and_return(false)
99+
allow(Homebrew::Bundle::FlatpakInstaller).to receive(:preinstall!).and_return(false)
100+
allow(Homebrew::Bundle::FormulaInstaller).to receive_messages(preinstall!: true, install!: true)
101+
allow(Homebrew::Bundle::CaskInstaller).to receive_messages(preinstall!: true, install!: true)
102+
allow(Homebrew::Bundle::MacAppStoreInstaller).to receive_messages(preinstall!: true, install!: true)
103+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'test_formula'")
104+
105+
expect(Homebrew::Bundle).to receive(:mark_as_installed_on_request!)
106+
described_class.run
107+
end
95108
end
96109
end

0 commit comments

Comments
 (0)