Skip to content

Commit 97a8831

Browse files
committed
Check bundle formula link status
- Ensure `brew bundle check` catches formulae with wrong links - Match implicit link expectations used by `brew bundle install` - Cover `check`, `check --verbose` and `check --install`
1 parent 654b084 commit 97a8831

2 files changed

Lines changed: 152 additions & 17 deletions

File tree

Library/Homebrew/bundle/brew.rb

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,40 @@ def formula_installed_and_up_to_date?(formula, no_upgrade: false)
7373
!formula_upgradable?(formula)
7474
end
7575

76+
sig { params(formula: String, options: Homebrew::Bundle::EntryOptions).returns(T.nilable(T::Boolean)) }
77+
def link_status_to_check(formula, options)
78+
unless options.key?(:link)
79+
return case formula_dump_link_status(formula)
80+
when true
81+
false
82+
when false
83+
true
84+
end
85+
end
86+
87+
expected_link_status?(options[:link], keg_only: Formula[formula].keg_only?)
88+
end
89+
90+
sig { params(link: Homebrew::Bundle::EntryOption, keg_only: T::Boolean).returns(T::Boolean) }
91+
def expected_link_status?(link, keg_only:)
92+
case link
93+
when :overwrite, true
94+
true
95+
when false
96+
false
97+
else
98+
!keg_only
99+
end
100+
end
101+
102+
sig { params(formula: String).returns(T.nilable(T::Boolean)) }
103+
def formula_dump_link_status(formula)
104+
(
105+
@formulae_by_full_name&.[](formula) ||
106+
@formulae_by_name&.[](Utils.name_from_full_name(formula))
107+
)&.fetch(:link?)
108+
end
109+
76110
sig { params(no_upgrade: T::Boolean, formula_name: String).returns(T::Boolean) }
77111
def no_upgrade_with_args?(no_upgrade, formula_name)
78112
no_upgrade && Bundle.upgrade_formulae.exclude?(formula_name)
@@ -406,9 +440,45 @@ def initialize(name = "", options = {})
406440

407441
sig { override.params(formula: Object, no_upgrade: T::Boolean).returns(T::Boolean) }
408442
def installed_and_up_to_date?(formula, no_upgrade: false)
409-
raise "formula must be a String, got #{formula.class}: #{formula}" unless formula.is_a?(String)
443+
name = formula_name(formula)
444+
return false unless self.class.formula_installed_and_up_to_date?(name, no_upgrade:)
445+
446+
options = formula_options(formula)
447+
link_status = self.class.link_status_to_check(name, options)
448+
return true if link_status.nil?
449+
450+
(options.key?(:link) ? Formula[name].linked? : self.class.formula_dump_link_status(name)) == link_status
451+
end
452+
453+
sig { override.params(name: Object, no_upgrade: T::Boolean).returns(String) }
454+
def failure_reason(name, no_upgrade:)
455+
formula = formula_name(name)
456+
options = formula_options(name)
457+
return super(formula, no_upgrade:) unless self.class.formula_installed_and_up_to_date?(formula, no_upgrade:)
410458

411-
self.class.formula_installed_and_up_to_date?(formula, no_upgrade:)
459+
link_status = self.class.link_status_to_check(formula, options)
460+
return super(formula, no_upgrade:) if link_status.nil?
461+
462+
link_status = link_status ? "linked" : "unlinked"
463+
"Formula #{formula} needs to be #{link_status}."
464+
end
465+
466+
sig {
467+
override.params(
468+
entries: T::Array[Object],
469+
exit_on_first_error: T::Boolean,
470+
no_upgrade: T::Boolean,
471+
verbose: T::Boolean,
472+
).returns(T::Array[Object])
473+
}
474+
def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verbose: false)
475+
requested = instance_of?(Homebrew::Bundle::Brew) ? checkable_entries(entries) : format_checkable(entries)
476+
477+
if exit_on_first_error
478+
exit_early_check(requested, no_upgrade:)
479+
else
480+
full_check(requested, no_upgrade:)
481+
end
412482
end
413483

414484
sig { params(no_upgrade: T::Boolean, verbose: T::Boolean).returns(T::Boolean) }
@@ -524,22 +594,13 @@ def service_change_state!(verbose:)
524594
sig { params(verbose: T::Boolean).returns(T::Boolean) }
525595
def link_change_state!(verbose: false)
526596
link_args = []
527-
link_args << "--force" if unlinked_and_keg_only?
528-
529-
cmd = case @link
530-
when :overwrite
531-
link_args << "--overwrite"
597+
link_status = self.class.expected_link_status?(@link, keg_only: keg_only?)
598+
cmd = if link_status
599+
link_args << "--force" if unlinked_and_keg_only?
600+
link_args << "--overwrite" if @link == :overwrite
532601
"link" unless linked?
533-
when true
534-
"link" unless linked?
535-
when false
536-
"unlink" if linked?
537-
when nil
538-
if keg_only?
539-
"unlink" if linked?
540-
else
541-
"link" unless linked?
542-
end
602+
elsif linked?
603+
"unlink"
543604
end
544605

545606
if cmd.present?
@@ -563,6 +624,21 @@ def postinstall_change_state!(verbose:)
563624

564625
private
565626

627+
sig { params(formula: Object).returns(String) }
628+
def formula_name(formula)
629+
return formula.name if formula.is_a?(Dsl::Entry)
630+
return formula if formula.is_a?(String)
631+
632+
raise "formula must be a String or Dsl::Entry, got #{formula.class}: #{formula}"
633+
end
634+
635+
sig { params(formula: Object).returns(Homebrew::Bundle::EntryOptions) }
636+
def formula_options(formula)
637+
return formula.options if formula.is_a?(Dsl::Entry)
638+
639+
{}
640+
end
641+
566642
sig { returns(T::Boolean) }
567643
def installed?
568644
self.class.formula_installed?(@name)

Library/Homebrew/test/cmd/bundle/check_subcommand_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,65 @@
8181
end
8282
end
8383

84+
context "when formulae have the wrong link status" do
85+
before do
86+
allow(Homebrew::Bundle::Cask).to receive(:casks).and_return([])
87+
allow(Homebrew::Bundle::Brew).to receive_messages(upgradable_formulae: [], installed_formulae: ["abc"])
88+
stub_formula_loader formula("abc") { url "abc-1.0" }
89+
end
90+
91+
it "raises an error" do
92+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc', link: true")
93+
allow(Formula["abc"]).to receive_messages(linked?: false, keg_only?: false)
94+
95+
expect { do_check }.to raise_error(SystemExit).and \
96+
output(/Run `brew bundle check --verbose` to list unmet dependencies\./).to_stdout
97+
end
98+
99+
it "raises an error for an implicitly unlinked non-keg-only formula" do
100+
Homebrew::Bundle::Brew.instance_variable_set(:@formulae_by_name, { "abc" => { link?: false } })
101+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc'")
102+
103+
expect { do_check }.to raise_error(SystemExit).and \
104+
output(/Run `brew bundle check --verbose` to list unmet dependencies\./).to_stdout
105+
end
106+
107+
context "with verbose mode enabled" do
108+
let(:verbose) { true }
109+
110+
it "outputs the link status error" do
111+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc', link: false")
112+
allow(Formula["abc"]).to receive_messages(linked?: true, keg_only?: false)
113+
114+
expect { do_check }.to raise_error(SystemExit).and \
115+
output(/Formula abc needs to be unlinked\./).to_stdout
116+
end
117+
118+
it "outputs the implicit link status error" do
119+
Homebrew::Bundle::Brew.instance_variable_set(:@formulae_by_name, { "abc" => { link?: true } })
120+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc'")
121+
122+
expect { do_check }.to raise_error(SystemExit).and \
123+
output(/Formula abc needs to be unlinked\./).to_stdout
124+
end
125+
end
126+
127+
context "with install mode enabled" do
128+
it "raises an error after install leaves a formula with the wrong link status" do
129+
args = args_for_subcommand(:check, install?: true, global?: false, verbose?: false, upgrade_formulae: nil,
130+
jobs: nil, file: nil)
131+
allow(Homebrew::Cmd::Bundle).to receive(:redirect_stdout).and_yield
132+
allow(Homebrew::Bundle::Brew).to receive(:install!).and_return(true)
133+
allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc', link: true")
134+
allow(Formula["abc"]).to receive_messages(linked?: false, keg_only?: false)
135+
136+
expect { Homebrew::Cmd::Bundle.dispatch(args, extensions: Homebrew::Bundle.extensions) }
137+
.to raise_error(SystemExit).and \
138+
output(/Run `brew bundle check --verbose` to list unmet dependencies\./).to_stdout
139+
end
140+
end
141+
end
142+
84143
context "when taps are not tapped" do
85144
it "raises an error" do
86145
allow(Homebrew::Bundle::Cask).to receive(:casks).and_return([])

0 commit comments

Comments
 (0)