diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 59d78f2429908..56f689ee48136 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1120,14 +1120,16 @@ def linked_version sig { returns(Pathname) } def rack = HOMEBREW_CELLAR/name - # All currently installed prefix directories. - sig { returns(T::Array[Pathname]) } - def installed_prefixes = Utils::Path.formula_installed_prefixes(possible_names) + # All currently installed prefix directories for formula names. + sig { params(formula_names: T::Array[String]).returns(T::Array[Pathname]) } + def installed_prefixes(formula_names = possible_names) + Utils::Path.formula_installed_prefixes(formula_names) + end - # All currently installed kegs. - sig { returns(T::Array[Keg]) } - def installed_kegs - installed_prefixes.map { |dir| Keg.new(dir) } + # All currently installed kegs for formula names. + sig { params(formula_names: T::Array[String]).returns(T::Array[Keg]) } + def installed_kegs(formula_names = possible_names) + installed_prefixes(formula_names).map { |dir| Keg.new(dir) } end # The directory where the formula's binaries should be installed. diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 852aec4c23eb7..507b433e27d3c 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1242,7 +1242,24 @@ def self.to_rack(ref) end # Use canonical name to locate rack. - Utils::Path.resolved_path(HOMEBREW_CELLAR/canonical_name(ref)) + rack = Utils::Path.resolved_path(HOMEBREW_CELLAR/canonical_name(ref)) + return rack if rack.directory? + + possible_rack = begin + formula = factory(ref) + possible_names = formula.possible_names.reject { |name| (HOMEBREW_CELLAR/name).symlink? } + installed_kegs = formula.installed_kegs(possible_names) + matching_racks = installed_kegs.group_by(&:rack).filter_map do |installed_rack, kegs| + next if installed_rack.symlink? || kegs.any? { |keg| keg.tab.tap != formula.tap } + + installed_rack + end + matching_racks.first if matching_racks.one? + rescue FormulaUnavailableError, TapFormulaAmbiguityError, Homebrew::UntrustedTapError, + JSON::ParserError, SystemCallError + nil + end + possible_rack || rack end sig { params(ref: String).returns(String) } diff --git a/Library/Homebrew/test/cmd/list_spec.rb b/Library/Homebrew/test/cmd/list_spec.rb index 9e39629351518..4e6742c3e16a5 100644 --- a/Library/Homebrew/test/cmd/list_spec.rb +++ b/Library/Homebrew/test/cmd/list_spec.rb @@ -337,6 +337,28 @@ def ruby_list_output(argv) cask.unpin end + it "continues listing a pinned cask when a formula receipt is invalid", :cask do + formula = formula("local-caffeine") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/local-caffeine-1.0" + end + allow(Formulary).to receive(:factory).with("local-caffeine").and_return(formula) + allow(Formulary).to receive(:canonical_name).with("local-caffeine").and_return(formula.name) + allow(formula).to receive(:possible_names).and_return([formula.name, "old-local-caffeine"]) + + keg = HOMEBREW_CELLAR/"old-local-caffeine/1.0" + keg.mkpath + (keg/AbstractTab::FILENAME).write "{" + + cask = Cask::CaskLoader.load("local-caffeine") + InstallHelper.stub_cask_installation(cask) + cask.pin + + expect { described_class.new(["--pinned", "local-caffeine"]).run } + .to output("local-caffeine\n").to_stdout + .and not_to_output.to_stderr + end + it "warns about broken Caskroom symlinks" do Cask::Caskroom.path.mkpath FileUtils.ln_s "missing-cask", Cask::Caskroom.path/"dangling-alias" diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index d77bafeabe78a..2361429a8d3e6 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -1176,6 +1176,65 @@ class #{described_class.class_s(formula_name)} < Formula end end + context "when the Formula has an alias", :no_api do + let(:alias_name) { "testball-alias" } + let(:alias_rack_path) { HOMEBREW_CELLAR/alias_name } + let(:later_alias_name) { "testball-alias-later" } + let(:later_alias_rack_path) { HOMEBREW_CELLAR/later_alias_name } + + def install_keg(rack, version: "0.1", tap: CoreTap.instance) + keg_path = rack/version + keg_path.mkpath + tab = Tab.empty + tab.tabfile = keg_path/AbstractTab::FILENAME + tab.tap = tap + tab.write + end + + before do + formula_path.dirname.mkpath + formula_path.write formula_content + CoreTap.instance.alias_dir.mkpath + FileUtils.ln_s formula_path, CoreTap.instance.alias_dir/alias_name + FileUtils.ln_s formula_path, CoreTap.instance.alias_dir/later_alias_name + end + + it "returns the Rack of the alias when only that Rack exists" do + install_keg(alias_rack_path) + + expect(described_class.to_rack(formula_name)).to eq(alias_rack_path) + end + + it "finds a non-symlink alias after an earlier symlinked alias" do + install_keg(later_alias_rack_path) + alias_rack_path.make_relative_symlink(later_alias_rack_path) + + expect(described_class.to_rack(formula_name)).to eq(later_alias_rack_path) + end + + it "does not choose between multiple same-tap alias racks" do + install_keg(alias_rack_path) + install_keg(later_alias_rack_path) + + expect(described_class.to_rack(formula_name)).to eq(rack_path) + end + + it "ignores a Rack of the alias with any keg from another tap" do + install_keg(alias_rack_path) + install_keg(alias_rack_path, version: "0.2", tap: Tap.fetch("homebrew", "other")) + + expect(described_class.to_rack(formula_name)).to eq(rack_path) + end + + it "ignores a Rack of the alias that links to another Formula's Rack" do + other_rack_path = HOMEBREW_CELLAR/"testball-renamed" + install_keg(other_rack_path) + alias_rack_path.make_relative_symlink(other_rack_path) + + expect(described_class.to_rack(formula_name)).to eq(rack_path) + end + end + it "raises an error if the Formula is not available" do expect do described_class.to_rack("a/b/#{formula_name}")