Skip to content

Commit af18672

Browse files
authored
Merge pull request #21676 from Homebrew/link_versioned_keg_only
Link versioned keg-only formulae by default
2 parents 45a6f3a + f383ae2 commit af18672

13 files changed

Lines changed: 571 additions & 27 deletions

Library/Homebrew/caveats.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def completions_and_elisp
6666
sig { params(skip_reason: T::Boolean).returns(T.nilable(String)) }
6767
def keg_only_text(skip_reason: false)
6868
return unless formula.keg_only?
69+
return if formula.linked?
6970

7071
s = if skip_reason
7172
""

Library/Homebrew/cmd/link.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ def run
5454

5555
kegs.freeze.each do |keg|
5656
keg_only = Formulary.keg_only?(keg.rack)
57+
formula = begin
58+
keg.to_formula
59+
rescue FormulaUnavailableError
60+
# Not all kegs may belong to current formulae
61+
nil
62+
end
63+
versioned_keg_only_formula = formula.present? && formula.keg_only_reason&.versioned_formula?
5764

5865
if keg.linked?
5966
opoo "Already linked: #{keg}"
60-
name_and_flag = "#{"--HEAD " if args.HEAD?}#{"--force " if keg_only}#{keg.name}"
67+
name_and_flag = +""
68+
name_and_flag << "--HEAD " if args.HEAD?
69+
name_and_flag << "--force " if keg_only && !versioned_keg_only_formula
70+
name_and_flag << keg.name
6171
puts <<~EOS
6272
To relink, run:
6373
brew unlink #{keg.name} && brew link #{name_and_flag}
@@ -72,17 +82,10 @@ def run
7282
puts "Would link:"
7383
end
7484
keg.link(**options)
75-
puts_keg_only_path_message(keg) if keg_only
85+
puts_keg_only_path_message(keg) if keg_only && !versioned_keg_only_formula
7686
next
7787
end
7888

79-
formula = begin
80-
keg.to_formula
81-
rescue FormulaUnavailableError
82-
# Not all kegs may belong to formulae
83-
nil
84-
end
85-
8689
if keg_only
8790
if HOMEBREW_PREFIX.to_s == HOMEBREW_DEFAULT_PREFIX && formula.present? &&
8891
formula.keg_only_reason.by_macos?
@@ -94,14 +97,14 @@ def run
9497
next
9598
end
9699

97-
if !args.force? && (formula.blank? || !formula.keg_only_reason.versioned_formula?)
100+
if !args.force? && (formula.nil? || !formula.keg_only_reason.versioned_formula?)
98101
opoo "#{keg.name} is keg-only and must be linked with `--force`."
99102
puts_keg_only_path_message(keg)
100103
next
101104
end
102105
end
103106

104-
Unlink.unlink_versioned_formulae(formula, verbose: args.verbose?) if formula
107+
Unlink.unlink_link_overwrite_formulae(formula, verbose: args.verbose?) if formula
105108

106109
keg.lock do
107110
print "Linking #{keg}... "
@@ -116,7 +119,9 @@ def run
116119
puts "#{n} symlinks created."
117120
end
118121

119-
puts_keg_only_path_message(keg) if keg_only && !Homebrew::EnvConfig.developer?
122+
if keg_only && !versioned_keg_only_formula && !Homebrew::EnvConfig.developer?
123+
puts_keg_only_path_message(keg)
124+
end
120125
end
121126
end
122127
end

Library/Homebrew/formula.rb

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -626,11 +626,18 @@ def versioned_formula? = name.include?("@")
626626
# Returns any other `@`-versioned formulae names for any Formula (including versioned formulae).
627627
sig { returns(T::Array[String]) }
628628
def versioned_formulae_names
629-
versioned_names = if tap
630-
name_prefix = name.gsub(/(@[\d.]+)?$/, "")
631-
T.must(tap).prefix_to_versioned_formulae_names.fetch(name_prefix, [])
629+
name_prefix = unversioned_formula_name || name
630+
631+
versioned_names = if (formula_tap = tap)
632+
formula_tap.prefix_to_versioned_formulae_names.fetch(name_prefix, [])
632633
elsif path.exist?
633-
Pathname.glob(path.to_s.gsub(/(@[\d.]+)?\.rb$/, "@*.rb"))
634+
versioned_formula_glob = if name_prefix.end_with?("-full")
635+
"#{name_prefix.delete_suffix("-full")}@*-full.rb"
636+
else
637+
"#{name_prefix}@*.rb"
638+
end
639+
640+
Pathname.glob((path.dirname/versioned_formula_glob).to_s)
634641
.map { |path| path.basename(".rb").to_s }
635642
.sort
636643
else
@@ -652,6 +659,94 @@ def versioned_formulae
652659
end.sort_by(&:version).reverse
653660
end
654661

662+
sig { returns(T.nilable(String)) }
663+
def unversioned_formula_name
664+
return unless versioned_formula?
665+
666+
name.sub(/@[\d.]+(?=-full$|$)/, "")
667+
end
668+
669+
# Returns the sibling `-full` or non-`-full` formula names for any Formula.
670+
sig { returns(T::Array[String]) }
671+
def full_formulae_names
672+
[
673+
if name.end_with?("-full")
674+
name.delete_suffix("-full")
675+
else
676+
"#{name}-full"
677+
end,
678+
]
679+
end
680+
681+
# Returns sibling `-full` or non-`-full` Formula objects for any Formula.
682+
sig { returns(T::Array[Formula]) }
683+
def full_formulae
684+
full_formulae_names.filter_map do |formula_name|
685+
Formula[formula_name]
686+
rescue FormulaUnavailableError
687+
nil
688+
end.sort_by(&:version).reverse
689+
end
690+
691+
sig { returns(T.nilable(String)) }
692+
def link_overwrite_reason
693+
installed_overwrite_formulae = link_overwrite_formulae.select(&:any_version_installed?)
694+
return if installed_overwrite_formulae.empty?
695+
696+
reason_formulae = installed_overwrite_formulae.select(&:linked?)
697+
status = if reason_formulae.empty?
698+
reason_formulae = installed_overwrite_formulae
699+
"installed"
700+
else
701+
"linked"
702+
end
703+
704+
"#{reason_formulae.map(&:full_name).to_sentence} #{reason_formulae.one? ? "is" : "are"} already #{status}"
705+
end
706+
707+
sig { returns(T::Array[String]) }
708+
def link_overwrite_related_formula_names
709+
[*versioned_formulae_names, *full_formulae_names, unversioned_formula_name].compact
710+
end
711+
712+
# Returns sibling Formula names whose prefix links should be replaced when this Formula is linked.
713+
sig { returns(T::Array[String]) }
714+
def link_overwrite_formulae_names
715+
formula_names = T.let(Set.new, T::Set[String])
716+
pending_formula_names = T.let([name], T::Array[String])
717+
718+
pending_formula_names.each do |current_name|
719+
current_formula = begin
720+
if current_name == name
721+
self
722+
else
723+
Formula[current_name]
724+
end
725+
rescue FormulaUnavailableError
726+
next
727+
end
728+
729+
current_formula.link_overwrite_related_formula_names.each do |related_formula_name|
730+
next if related_formula_name == name
731+
next unless formula_names.add?(related_formula_name)
732+
733+
pending_formula_names << related_formula_name
734+
end
735+
end
736+
737+
formula_names.to_a.sort
738+
end
739+
740+
# Returns sibling Formulae whose prefix links should be replaced when this Formula is linked.
741+
sig { returns(T::Array[Formula]) }
742+
def link_overwrite_formulae
743+
link_overwrite_formulae_names.filter_map do |formula_name|
744+
Formula[formula_name]
745+
rescue FormulaUnavailableError
746+
nil
747+
end
748+
end
749+
655750
# Whether this {Formula} is version-synced with other formulae.
656751
sig { returns(T::Boolean) }
657752
def synced_with_other_formulae?

Library/Homebrew/formula_installer.rb

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class FormulaInstaller
5252
sig {
5353
params(
5454
formula: Formula,
55-
link_keg: T::Boolean,
55+
link_keg: T.nilable(T::Boolean),
5656
installed_as_dependency: T::Boolean,
5757
installed_on_request: T::Boolean,
5858
show_header: T::Boolean,
@@ -81,7 +81,7 @@ class FormulaInstaller
8181
}
8282
def initialize(
8383
formula,
84-
link_keg: false,
84+
link_keg: nil,
8585
installed_as_dependency: false,
8686
installed_on_request: false,
8787
show_header: false,
@@ -113,7 +113,10 @@ def initialize(
113113
@overwrite = overwrite
114114
@keep_tmp = keep_tmp
115115
@debug_symbols = debug_symbols
116-
@link_keg = T.let(!formula.keg_only? || link_keg, T::Boolean)
116+
@installed_as_dependency = installed_as_dependency
117+
@installed_on_request = installed_on_request
118+
link_keg = !formula.keg_only? || auto_link_versioned_keg_only? if link_keg.nil?
119+
@link_keg = T.let(link_keg, T::Boolean)
117120
@show_header = show_header
118121
@ignore_deps = ignore_deps
119122
@only_deps = only_deps
@@ -131,8 +134,6 @@ def initialize(
131134
@verbose = verbose
132135
@quiet = quiet
133136
@debug = debug
134-
@installed_as_dependency = installed_as_dependency
135-
@installed_on_request = installed_on_request
136137
@options = options
137138
@requirement_messages = T.let([], T::Array[String])
138139
@poured_bottle = T.let(false, T::Boolean)
@@ -937,6 +938,24 @@ def caveats
937938
Homebrew.messages.record_caveats(formula.name, caveats)
938939
end
939940

941+
sig { returns(T.nilable(String)) }
942+
def link_manual_command_warning
943+
return if installed_as_dependency?
944+
return unless formula.keg_only?
945+
return unless formula.keg_only_reason.versioned_formula?
946+
return if link_keg
947+
return if formula.linked?
948+
949+
reason = formula.link_overwrite_reason
950+
return if reason.blank?
951+
952+
<<~EOS
953+
#{formula.full_name} was installed but not linked because #{reason}.
954+
To link this version, run:
955+
brew link #{formula.full_name}
956+
EOS
957+
end
958+
940959
sig { void }
941960
def finish
942961
return if only_deps?
@@ -952,6 +971,8 @@ def finish
952971
end
953972
else
954973
link(keg)
974+
warning = link_manual_command_warning
975+
opoo warning if !quiet? && warning.present?
955976
end
956977

957978
install_service
@@ -1162,7 +1183,7 @@ def link(keg)
11621183
keg.remove_linked_keg_record
11631184
end
11641185

1165-
Homebrew::Unlink.unlink_versioned_formulae(formula, verbose: verbose?)
1186+
Homebrew::Unlink.unlink_link_overwrite_formulae(formula, verbose: verbose?)
11661187

11671188
link_overwrite_backup = {} # Hash: conflict file -> backup file
11681189
backup_dir = HOMEBREW_CACHE/"Backup"
@@ -1701,6 +1722,17 @@ def forbidden_formula_check
17011722

17021723
private
17031724

1725+
sig { returns(T::Boolean) }
1726+
def auto_link_versioned_keg_only?
1727+
return false if installed_as_dependency?
1728+
return false unless formula.keg_only?
1729+
return false unless formula.keg_only_reason.versioned_formula?
1730+
return false if formula.any_version_installed?
1731+
return false if formula.link_overwrite_formulae.any?(&:any_version_installed?)
1732+
1733+
true
1734+
end
1735+
17041736
sig { void }
17051737
def lock
17061738
return unless self.class.locked.empty?

Library/Homebrew/install.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def install_formula?(
181181
msg = <<~EOS
182182
#{msg}, it's just not linked.
183183
To link this version, run:
184-
brew link #{formula}
184+
brew link #{formula.full_name}
185185
EOS
186186
else
187187
msg = if quiet

Library/Homebrew/tap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ def formula_names
844844
def prefix_to_versioned_formulae_names
845845
@prefix_to_versioned_formulae_names ||= T.let(formula_names
846846
.select { |name| name.include?("@") }
847-
.group_by { |name| name.gsub(/(@[\d.]+)?$/, "") }
847+
.group_by { |name| name.sub(/@[\d.]+(?=-full$|$)/, "") }
848848
.transform_values(&:sort)
849849
.freeze, T.nilable(T::Hash[String, T::Array[String]]))
850850
end

Library/Homebrew/test/caveats_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ def caveats
176176
expect(caveats).to include("keg-only")
177177
end
178178

179+
it "omits keg-only caveats when the formula is linked" do
180+
allow(f).to receive(:linked?).and_return(true)
181+
182+
expect(caveats).to be_empty
183+
end
184+
179185
it "gives command to be run when f.bin is a directory" do
180186
Pathname.new(f.bin).mkpath
181187
expect(caveats).to include(f.opt_bin.to_s)

Library/Homebrew/test/cmd/link_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,36 @@
1818
.and be_a_success
1919
expect(HOMEBREW_PREFIX/"bin/testfile").to be_a_file
2020
end
21+
22+
{
23+
"@-versioned" => "testball-link-output@1.0",
24+
"-full" => "testball-link-output-full",
25+
}.each do |formula_type, formula_name|
26+
it "does not print keg-only output when linking a #{formula_type} formula", :integration_test do
27+
formula_content = <<~RUBY
28+
keg_only :versioned_formula
29+
30+
def caveats
31+
"unexpected caveat output"
32+
end
33+
34+
def post_install
35+
puts "unexpected post_install output"
36+
end
37+
RUBY
38+
39+
setup_test_formula formula_name, formula_content, tab_attributes: { installed_on_request: true }
40+
Formula[formula_name].bin.mkpath
41+
FileUtils.touch Formula[formula_name].bin/"link-output-test"
42+
Formula[formula_name].any_installed_keg.unlink
43+
unexpected_output = /unexpected caveat output|unexpected post_install output|
44+
If you need to have this software first in your PATH|keg-only/x
45+
46+
expect { brew "link", formula_name }
47+
.to not_to_output(unexpected_output).to_stdout
48+
.and not_to_output.to_stderr
49+
.and be_a_success
50+
expect(HOMEBREW_PREFIX/"bin/link-output-test").to be_a_file
51+
end
52+
end
2153
end

0 commit comments

Comments
 (0)