Skip to content

Commit a2b77dc

Browse files
info: add installed versions section
1 parent 20bd107 commit a2b77dc

4 files changed

Lines changed: 437 additions & 13 deletions

File tree

Library/Homebrew/caveats.rb

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,38 @@ def keg_only_text(skip_reason: false)
127127

128128
sig { returns(T.nilable(String)) }
129129
def shadowed_path_text
130-
return if formula.keg_only? && !formula.linked?
131130
return if Homebrew::EnvConfig.no_path_shadow_check?
131+
return unless formula.any_version_installed?
132132

133133
shadowed = shadowed_executables
134+
shadowed = shadowed.select { |_, shadower| sibling_keg_name(shadower) } if formula.keg_only? && !formula.linked?
134135
return if shadowed.empty?
135136

136-
lines = shadowed.sort_by(&:first).map { |name, shadower| " #{name} (shadowed by #{shadower})" }
137-
s = <<~EOS
138-
The following #{formula.name} executables are shadowed by other commands earlier in your PATH:
139-
#{lines.join("\n")}
140-
Running these by name will not invoke the version provided by Homebrew.
141-
EOS
137+
sibling, external = shadowed.sort_by(&:first).partition { |_, shadower| sibling_keg_name(shadower) }
138+
blocks = []
139+
140+
if external.any?
141+
lines = external.map { |name, shadower| " #{name} (shadowed by #{shadower})" }
142+
blocks << <<~EOS
143+
The following #{formula.name} executables are shadowed by other commands earlier in your PATH:
144+
#{lines.join("\n")}
145+
Running these by name will not invoke the version provided by Homebrew.
146+
EOS
147+
end
148+
149+
if sibling.any?
150+
lines = sibling.map do |name, shadower|
151+
" #{name} (shadowed by #{shadower} from #{sibling_keg_name(shadower)})"
152+
end
153+
blocks << <<~EOS
154+
The following #{formula.name} executables are shadowed by other linked Homebrew commands:
155+
#{lines.join("\n")}
156+
Running these by name will not invoke the version provided by this formula.
157+
Run `brew link #{formula.name}` to switch the active version to this keg.
158+
EOS
159+
end
160+
161+
s = blocks.join("\n").dup
142162
unless Homebrew::EnvConfig.no_env_hints?
143163
s << "Disable this behaviour by setting `HOMEBREW_NO_PATH_SHADOW_CHECK=1`.\n"
144164
s << "Hide these hints with `HOMEBREW_NO_ENV_HINTS=1` (see `man brew`).\n"
@@ -148,6 +168,24 @@ def shadowed_path_text
148168

149169
private
150170

171+
sig { params(shadower: Pathname).returns(T.nilable(String)) }
172+
def sibling_keg_name(shadower)
173+
target = shadower.realpath
174+
return unless target.to_s.start_with?("#{HOMEBREW_CELLAR.realpath}/")
175+
176+
name = target.relative_path_from(HOMEBREW_CELLAR.realpath).each_filename.first
177+
return if name.nil? || name == formula.name
178+
179+
family = [
180+
formula.unversioned_formula_name,
181+
formula.name,
182+
*formula.versioned_formulae_names,
183+
].compact
184+
name if family.include?(name)
185+
rescue Errno::ENOENT
186+
nil
187+
end
188+
151189
sig { returns(T::Array[[String, Pathname]]) }
152190
def shadowed_executables
153191
[formula.opt_bin, formula.opt_sbin].flat_map do |dir|

Library/Homebrew/cmd/info.rb

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,8 @@ def info_formula(formula, shadowed_by: nil)
617617
end
618618
puts formula.desc if formula.desc
619619
puts Formatter.url(formula.homepage) if formula.homepage
620+
puts "Aliases: #{formula.aliases.join(", ")}" if formula.aliases.any?
621+
puts "Old Names: #{formula.oldnames.join(", ")}" if formula.oldnames.any?
620622

621623
deprecate_disable_info_string = DeprecateDisable.message(formula)
622624
if deprecate_disable_info_string.present?
@@ -655,11 +657,6 @@ def info_formula(formula, shadowed_by: nil)
655657
end
656658
else
657659
puts self.class.installation_status(Tab.for_formula(formula))
658-
kegs.each do |keg|
659-
puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}"
660-
tab = keg.tab.to_s
661-
puts " #{tab}" unless tab.empty?
662-
end
663660
end
664661

665662
puts "From: #{Formatter.url(github_info(formula))}"
@@ -668,6 +665,12 @@ def info_formula(formula, shadowed_by: nil)
668665
metadata = self.class.metadata_lines(formula)
669666
puts metadata if metadata.present?
670667

668+
installed_lines = installed_section_lines(formula, verbose: args.verbose?)
669+
unless installed_lines.empty?
670+
ohai "Installed Kegs and Versions"
671+
installed_lines.each { |line| puts line }
672+
end
673+
671674
tab_runtime_deps = kegs.last&.runtime_dependencies
672675
installed_dependents = if $stdout.tty? && kegs.any?
673676
self.class.installed_dependent_names(formula.full_name, formula.name)
@@ -751,6 +754,52 @@ def info_formula(formula, shadowed_by: nil)
751754
Utils::Analytics.formula_output(formula, args:)
752755
end
753756

757+
sig { params(formula: Formula, verbose: T::Boolean).returns(T::Array[String]) }
758+
def installed_section_lines(formula, verbose: false)
759+
siblings = formula.versioned_formulae
760+
parent = if (parent_name = formula.unversioned_formula_name)
761+
begin
762+
Formulary.factory(parent_name)
763+
rescue FormulaUnavailableError
764+
nil
765+
end
766+
end
767+
related = [formula, parent, *siblings].compact.uniq(&:full_name)
768+
installed = related.select { |f| f.installed_kegs.any? }
769+
return [] if installed.empty?
770+
771+
ordered = installed.sort_by do |other|
772+
newest_keg = other.installed_kegs.max_by(&:scheme_and_version)
773+
newest_keg ? newest_keg.scheme_and_version : other.pkg_version
774+
end.reverse
775+
with_kegs = ordered.flat_map do |other|
776+
heads, versioned = other.installed_kegs.partition { |keg| keg.version.head? }
777+
ordered_kegs = [
778+
*heads.sort_by { |keg| -keg.tab.time.to_i },
779+
*versioned.sort_by(&:scheme_and_version).reverse,
780+
]
781+
ordered_kegs.map { |keg| [other, keg] }
782+
end
783+
rows = with_kegs.map do |other, keg|
784+
name_status = pretty_install_status(other.full_name, installed: true, outdated: other.outdated?)
785+
linked_marker = keg.linked? ? "[Linked]" : ""
786+
[name_status, keg.version.to_s, "(#{keg.abv})", linked_marker, keg]
787+
end
788+
name_width = rows.map { |r| Tty.strip_ansi(r[0]).length }.max || 0
789+
version_width = rows.map { |r| r[1].length }.max || 0
790+
size_width = rows.map { |r| r[2].length }.max || 0
791+
rows.flat_map do |name_status, version, size, linked_marker, keg|
792+
padded_name = name_status + (" " * (name_width - Tty.strip_ansi(name_status).length))
793+
padded_size = linked_marker.empty? ? size : size.ljust(size_width)
794+
line = "#{padded_name} #{version.ljust(version_width)} #{padded_size}" \
795+
"#{" #{linked_marker}" unless linked_marker.empty?}"
796+
next [line] unless verbose
797+
798+
tab_string = keg.tab.to_s
799+
tab_string.empty? ? [line] : [line, " #{tab_string}"]
800+
end
801+
end
802+
754803
sig {
755804
params(dependencies: T::Array[Dependency],
756805
tab_runtime_deps: T.nilable(T::Array[T::Hash[String, T.untyped]])).returns(String)

Library/Homebrew/test/caveats_spec.rb

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def caveats
250250
Pathname.new(f.opt_bin).mkpath
251251
FileUtils.touch(f.opt_bin/"foo")
252252
FileUtils.chmod(0755, f.opt_bin/"foo")
253+
allow(f).to receive(:any_version_installed?).and_return(true)
253254
allow_any_instance_of(Object).to receive(:which).and_call_original
254255
end
255256

@@ -291,6 +292,54 @@ def caveats
291292
expect(described_class.new(keg_only_f).caveats).not_to include("shadowed")
292293
end
293294

295+
it "does not warn when the queried formula itself is not installed" do
296+
uninstalled_f = formula("foo@old") do
297+
url "foo-1.0"
298+
keg_only :versioned_formula
299+
end
300+
Pathname.new(uninstalled_f.opt_bin).mkpath
301+
FileUtils.touch(uninstalled_f.opt_bin/"foo")
302+
FileUtils.chmod(0755, uninstalled_f.opt_bin/"foo")
303+
304+
sibling_keg_bin = HOMEBREW_CELLAR/"foo@2.0/2.0/bin"
305+
sibling_keg_bin.mkpath
306+
sibling_shadower = sibling_keg_bin/"foo"
307+
sibling_shadower.write("#!/bin/sh\n")
308+
sibling_shadower.chmod(0755)
309+
310+
allow(uninstalled_f).to receive_messages(versioned_formulae_names: ["foo@2.0"],
311+
unversioned_formula_name: "foo",
312+
any_version_installed?: false)
313+
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)
314+
315+
expect(described_class.new(uninstalled_f).caveats).not_to include("shadowed")
316+
end
317+
318+
it "warns for a keg-only formula when a sibling keg is linked over it" do
319+
keg_only_f = formula("foo@1.0") do
320+
url "foo-1.0"
321+
keg_only :versioned_formula
322+
end
323+
Pathname.new(keg_only_f.opt_bin).mkpath
324+
FileUtils.touch(keg_only_f.opt_bin/"foo")
325+
FileUtils.chmod(0755, keg_only_f.opt_bin/"foo")
326+
327+
sibling_keg_bin = HOMEBREW_CELLAR/"foo@2.0/2.0/bin"
328+
sibling_keg_bin.mkpath
329+
sibling_shadower = sibling_keg_bin/"foo"
330+
sibling_shadower.write("#!/bin/sh\n")
331+
sibling_shadower.chmod(0755)
332+
333+
allow(keg_only_f).to receive_messages(versioned_formulae_names: ["foo@2.0"],
334+
unversioned_formula_name: "foo",
335+
any_version_installed?: true)
336+
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)
337+
338+
caveats = described_class.new(keg_only_f).caveats
339+
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from foo@2.0)")
340+
expect(caveats).to include("Run `brew link foo@1.0`")
341+
end
342+
294343
it "warns when a keg-only formula has been linked" do
295344
keg_only_f = formula do
296345
url "foo-1.0"
@@ -299,7 +348,7 @@ def caveats
299348
Pathname.new(keg_only_f.opt_bin).mkpath
300349
FileUtils.touch(keg_only_f.opt_bin/"foo")
301350
FileUtils.chmod(0755, keg_only_f.opt_bin/"foo")
302-
allow(keg_only_f).to receive(:linked?).and_return(true)
351+
allow(keg_only_f).to receive_messages(linked?: true, any_version_installed?: true)
303352
shadower = Pathname.new("/usr/local/bin/foo")
304353
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(shadower)
305354
allow(shadower).to receive(:realpath).and_return(shadower)
@@ -331,6 +380,47 @@ def caveats
331380

332381
expect(described_class.new(f).caveats).not_to include("HOMEBREW_NO_PATH_SHADOW_CHECK")
333382
end
383+
384+
it "annotates sibling-keg shadowers with the keg name and adds a `brew link` hint" do
385+
sibling_keg_bin = HOMEBREW_CELLAR/"#{f.name}@1.0/1.0/bin"
386+
sibling_keg_bin.mkpath
387+
sibling_shadower = sibling_keg_bin/"foo"
388+
sibling_shadower.write("#!/bin/sh\n")
389+
sibling_shadower.chmod(0755)
390+
391+
allow(f).to receive_messages(versioned_formulae_names: ["#{f.name}@1.0"], unversioned_formula_name: nil)
392+
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)
393+
394+
caveats = described_class.new(f).caveats
395+
expect(caveats).to include("shadowed by other linked Homebrew commands")
396+
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from #{f.name}@1.0)")
397+
expect(caveats).to include("Run `brew link #{f.name}`")
398+
expect(caveats).not_to include("earlier in your PATH")
399+
end
400+
401+
it "annotates only the sibling line when shadowers are mixed" do
402+
Pathname.new(f.opt_bin).mkpath
403+
FileUtils.touch(f.opt_bin/"bar")
404+
FileUtils.chmod(0755, f.opt_bin/"bar")
405+
406+
sibling_keg_bin = HOMEBREW_CELLAR/"#{f.name}@1.0/1.0/bin"
407+
sibling_keg_bin.mkpath
408+
sibling_shadower = sibling_keg_bin/"foo"
409+
sibling_shadower.write("#!/bin/sh\n")
410+
sibling_shadower.chmod(0755)
411+
412+
bar_shadower = Pathname.new("/usr/local/bin/bar")
413+
allow(bar_shadower).to receive(:realpath).and_return(bar_shadower)
414+
415+
allow(f).to receive_messages(versioned_formulae_names: ["#{f.name}@1.0"], unversioned_formula_name: nil)
416+
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)
417+
allow_any_instance_of(Object).to receive(:which).with("bar", ORIGINAL_PATHS).and_return(bar_shadower)
418+
419+
caveats = described_class.new(f).caveats
420+
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from #{f.name}@1.0)")
421+
expect(caveats).to include("bar (shadowed by #{bar_shadower})")
422+
expect(caveats).to include("Run `brew link #{f.name}`")
423+
end
334424
end
335425

336426
describe "shell completions" do

0 commit comments

Comments
 (0)