Skip to content
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
52 changes: 45 additions & 7 deletions Library/Homebrew/caveats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,38 @@ def keg_only_text(skip_reason: false)

sig { returns(T.nilable(String)) }
def shadowed_path_text
return if formula.keg_only? && !formula.linked?
return if Homebrew::EnvConfig.no_path_shadow_check?
return unless formula.any_version_installed?

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

lines = shadowed.sort_by(&:first).map { |name, shadower| " #{name} (shadowed by #{shadower})" }
s = <<~EOS
The following #{formula.name} executables are shadowed by other commands earlier in your PATH:
#{lines.join("\n")}
Running these by name will not invoke the version provided by Homebrew.
EOS
sibling, external = shadowed.sort_by(&:first).partition { |_, shadower| sibling_keg_name(shadower) }
blocks = []

if external.any?
lines = external.map { |name, shadower| " #{name} (shadowed by #{shadower})" }
blocks << <<~EOS
The following #{formula.name} executables are shadowed by other commands earlier in your PATH:
#{lines.join("\n")}
Running these by name will not invoke the version provided by Homebrew.
EOS
end

if sibling.any?
lines = sibling.map do |name, shadower|
" #{name} (shadowed by #{shadower} from #{sibling_keg_name(shadower)})"
end
blocks << <<~EOS
The following #{formula.name} executables are shadowed by other linked Homebrew commands:
#{lines.join("\n")}
Running these by name will not invoke the version provided by this formula.
Run `brew link #{formula.name}` to switch the active version to this keg.
EOS
end

s = blocks.join("\n").dup
unless Homebrew::EnvConfig.no_env_hints?
s << "Disable this behaviour by setting `HOMEBREW_NO_PATH_SHADOW_CHECK=1`.\n"
s << "Hide these hints with `HOMEBREW_NO_ENV_HINTS=1` (see `man brew`).\n"
Expand All @@ -148,6 +168,24 @@ def shadowed_path_text

private

sig { params(shadower: Pathname).returns(T.nilable(String)) }
def sibling_keg_name(shadower)
Comment thread
MikeMcQuaid marked this conversation as resolved.
target = shadower.realpath
return unless target.to_s.start_with?("#{HOMEBREW_CELLAR.realpath}/")

name = target.relative_path_from(HOMEBREW_CELLAR.realpath).each_filename.first
return if name.nil? || name == formula.name

family = [
formula.unversioned_formula_name,
formula.name,
*formula.versioned_formulae_names,
].compact
name if family.include?(name)
rescue Errno::ENOENT
nil
end

sig { returns(T::Array[[String, Pathname]]) }
def shadowed_executables
[formula.opt_bin, formula.opt_sbin].flat_map do |dir|
Expand Down
59 changes: 54 additions & 5 deletions Library/Homebrew/cmd/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,8 @@ def info_formula(formula, shadowed_by: nil)
end
puts formula.desc if formula.desc
puts Formatter.url(formula.homepage) if formula.homepage
puts "Aliases: #{formula.aliases.join(", ")}" if formula.aliases.any?
puts "Old Names: #{formula.oldnames.join(", ")}" if formula.oldnames.any?

deprecate_disable_info_string = DeprecateDisable.message(formula)
if deprecate_disable_info_string.present?
Expand Down Expand Up @@ -655,11 +657,6 @@ def info_formula(formula, shadowed_by: nil)
end
else
puts self.class.installation_status(Tab.for_formula(formula))
kegs.each do |keg|
puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}"
tab = keg.tab.to_s
puts " #{tab}" unless tab.empty?
end
end

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

installed_lines = installed_section_lines(formula, verbose: args.verbose?)
unless installed_lines.empty?
ohai "Installed Kegs and Versions"
installed_lines.each { |line| puts line }
end

tab_runtime_deps = kegs.last&.runtime_dependencies
installed_dependents = if $stdout.tty? && kegs.any?
self.class.installed_dependent_names(formula.full_name, formula.name)
Expand Down Expand Up @@ -751,6 +754,52 @@ def info_formula(formula, shadowed_by: nil)
Utils::Analytics.formula_output(formula, args:)
end

sig { params(formula: Formula, verbose: T::Boolean).returns(T::Array[String]) }
def installed_section_lines(formula, verbose: false)
siblings = formula.versioned_formulae
parent = if (parent_name = formula.unversioned_formula_name)
begin
Formulary.factory(parent_name)
rescue FormulaUnavailableError
nil
end
end
related = [formula, parent, *siblings].compact.uniq(&:full_name)
installed = related.select { |f| f.installed_kegs.any? }
return [] if installed.empty?

ordered = installed.sort_by do |other|
newest_keg = other.installed_kegs.max_by(&:scheme_and_version)
newest_keg ? newest_keg.scheme_and_version : other.pkg_version
end.reverse
with_kegs = ordered.flat_map do |other|
heads, versioned = other.installed_kegs.partition { |keg| keg.version.head? }
ordered_kegs = [
*heads.sort_by { |keg| -keg.tab.time.to_i },
*versioned.sort_by(&:scheme_and_version).reverse,
]
ordered_kegs.map { |keg| [other, keg] }
end
rows = with_kegs.map do |other, keg|
name_status = pretty_install_status(other.full_name, installed: true, outdated: other.outdated?)
linked_marker = keg.linked? ? "[Linked]" : ""
[name_status, keg.version.to_s, "(#{keg.abv})", linked_marker, keg]
end
name_width = rows.map { |r| Tty.strip_ansi(r[0]).length }.max || 0
version_width = rows.map { |r| r[1].length }.max || 0
size_width = rows.map { |r| r[2].length }.max || 0
rows.flat_map do |name_status, version, size, linked_marker, keg|
padded_name = name_status + (" " * (name_width - Tty.strip_ansi(name_status).length))
padded_size = linked_marker.empty? ? size : size.ljust(size_width)
line = "#{padded_name} #{version.ljust(version_width)} #{padded_size}" \
"#{" #{linked_marker}" unless linked_marker.empty?}"
next [line] unless verbose

tab_string = keg.tab.to_s
tab_string.empty? ? [line] : [line, " #{tab_string}"]
end
end

sig {
params(dependencies: T::Array[Dependency],
tab_runtime_deps: T.nilable(T::Array[T::Hash[String, T.untyped]])).returns(String)
Expand Down
92 changes: 91 additions & 1 deletion Library/Homebrew/test/caveats_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def caveats
Pathname.new(f.opt_bin).mkpath
FileUtils.touch(f.opt_bin/"foo")
FileUtils.chmod(0755, f.opt_bin/"foo")
allow(f).to receive(:any_version_installed?).and_return(true)
allow_any_instance_of(Object).to receive(:which).and_call_original
end

Expand Down Expand Up @@ -291,6 +292,54 @@ def caveats
expect(described_class.new(keg_only_f).caveats).not_to include("shadowed")
end

it "does not warn when the queried formula itself is not installed" do
uninstalled_f = formula("foo@old") do
url "foo-1.0"
keg_only :versioned_formula
end
Pathname.new(uninstalled_f.opt_bin).mkpath
FileUtils.touch(uninstalled_f.opt_bin/"foo")
FileUtils.chmod(0755, uninstalled_f.opt_bin/"foo")

sibling_keg_bin = HOMEBREW_CELLAR/"foo@2.0/2.0/bin"
sibling_keg_bin.mkpath
sibling_shadower = sibling_keg_bin/"foo"
sibling_shadower.write("#!/bin/sh\n")
sibling_shadower.chmod(0755)

allow(uninstalled_f).to receive_messages(versioned_formulae_names: ["foo@2.0"],
unversioned_formula_name: "foo",
any_version_installed?: false)
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)

expect(described_class.new(uninstalled_f).caveats).not_to include("shadowed")
end

it "warns for a keg-only formula when a sibling keg is linked over it" do
keg_only_f = formula("foo@1.0") do
url "foo-1.0"
keg_only :versioned_formula
end
Pathname.new(keg_only_f.opt_bin).mkpath
FileUtils.touch(keg_only_f.opt_bin/"foo")
FileUtils.chmod(0755, keg_only_f.opt_bin/"foo")

sibling_keg_bin = HOMEBREW_CELLAR/"foo@2.0/2.0/bin"
sibling_keg_bin.mkpath
sibling_shadower = sibling_keg_bin/"foo"
sibling_shadower.write("#!/bin/sh\n")
sibling_shadower.chmod(0755)

allow(keg_only_f).to receive_messages(versioned_formulae_names: ["foo@2.0"],
unversioned_formula_name: "foo",
any_version_installed?: true)
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)

caveats = described_class.new(keg_only_f).caveats
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from foo@2.0)")
expect(caveats).to include("Run `brew link foo@1.0`")
end

it "warns when a keg-only formula has been linked" do
keg_only_f = formula do
url "foo-1.0"
Expand All @@ -299,7 +348,7 @@ def caveats
Pathname.new(keg_only_f.opt_bin).mkpath
FileUtils.touch(keg_only_f.opt_bin/"foo")
FileUtils.chmod(0755, keg_only_f.opt_bin/"foo")
allow(keg_only_f).to receive(:linked?).and_return(true)
allow(keg_only_f).to receive_messages(linked?: true, any_version_installed?: true)
shadower = Pathname.new("/usr/local/bin/foo")
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(shadower)
allow(shadower).to receive(:realpath).and_return(shadower)
Expand Down Expand Up @@ -331,6 +380,47 @@ def caveats

expect(described_class.new(f).caveats).not_to include("HOMEBREW_NO_PATH_SHADOW_CHECK")
end

it "annotates sibling-keg shadowers with the keg name and adds a `brew link` hint" do
sibling_keg_bin = HOMEBREW_CELLAR/"#{f.name}@1.0/1.0/bin"
sibling_keg_bin.mkpath
sibling_shadower = sibling_keg_bin/"foo"
sibling_shadower.write("#!/bin/sh\n")
sibling_shadower.chmod(0755)

allow(f).to receive_messages(versioned_formulae_names: ["#{f.name}@1.0"], unversioned_formula_name: nil)
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)

caveats = described_class.new(f).caveats
expect(caveats).to include("shadowed by other linked Homebrew commands")
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from #{f.name}@1.0)")
expect(caveats).to include("Run `brew link #{f.name}`")
expect(caveats).not_to include("earlier in your PATH")
end

it "annotates only the sibling line when shadowers are mixed" do
Pathname.new(f.opt_bin).mkpath
FileUtils.touch(f.opt_bin/"bar")
FileUtils.chmod(0755, f.opt_bin/"bar")

sibling_keg_bin = HOMEBREW_CELLAR/"#{f.name}@1.0/1.0/bin"
sibling_keg_bin.mkpath
sibling_shadower = sibling_keg_bin/"foo"
sibling_shadower.write("#!/bin/sh\n")
sibling_shadower.chmod(0755)

bar_shadower = Pathname.new("/usr/local/bin/bar")
allow(bar_shadower).to receive(:realpath).and_return(bar_shadower)

allow(f).to receive_messages(versioned_formulae_names: ["#{f.name}@1.0"], unversioned_formula_name: nil)
allow_any_instance_of(Object).to receive(:which).with("foo", ORIGINAL_PATHS).and_return(sibling_shadower)
allow_any_instance_of(Object).to receive(:which).with("bar", ORIGINAL_PATHS).and_return(bar_shadower)

caveats = described_class.new(f).caveats
expect(caveats).to include("foo (shadowed by #{sibling_shadower} from #{f.name}@1.0)")
expect(caveats).to include("bar (shadowed by #{bar_shadower})")
expect(caveats).to include("Run `brew link #{f.name}`")
end
end

describe "shell completions" do
Expand Down
Loading
Loading