From 8cb62b13987087469f2dbdbe622dac01f9ea095c Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Sat, 20 Jul 2024 21:41:34 -0400 Subject: [PATCH] Set correct tap when loading installed casks --- Library/Homebrew/cask/cask_loader.rb | 43 +++++++++++++- Library/Homebrew/cask/caskroom.rb | 2 +- Library/Homebrew/cli/named_args.rb | 23 +++++++- Library/Homebrew/exceptions.rb | 12 ++++ .../Homebrew/test/cask/cask_loader_spec.rb | 56 +++++++++++++++++++ Library/Homebrew/test/cli/named_args_spec.rb | 30 ++++++++++ Library/Homebrew/test/exceptions_spec.rb | 8 +++ 7 files changed, 170 insertions(+), 4 deletions(-) diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 481118b555..2f9598693b 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -503,13 +503,26 @@ module Cask .returns(T.nilable(T.attached_class)) } def self.try_new(ref, warn: false) - return unless ref.is_a?(String) + token = if ref.is_a?(String) + ref + elsif ref.is_a?(Pathname) + ref.basename(ref.extname).to_s + end + return unless token - possible_installed_cask = Cask.new(ref) + possible_installed_cask = Cask.new(token) return unless (installed_caskfile = possible_installed_cask.installed_caskfile) new(installed_caskfile) end + + sig { params(path: T.any(Pathname, String), token: String).void } + def initialize(path, token: T.unsafe(nil)) + super + + installed_tap = Cask.new(@token).tab.tap + @tap = installed_tap if installed_tap + end end # Pseudo-loader which raises an error when trying to load the corresponding cask. @@ -598,6 +611,32 @@ module Cask end end + sig { params(ref: String, config: T.nilable(Config), warn: T::Boolean).returns(Cask) } + def self.load_installed_cask(ref, config: nil, warn: true) + tap, token = Tap.with_cask_token(ref) + token ||= ref + tap ||= Cask.new(ref).tab.tap + + if tap.nil? + self.load(token, config:, warn:) + else + begin + self.load("#{tap}/#{token}", config:, warn:) + rescue CaskUnavailableError + # cask may be migrated to different tap. Try to search in all taps. + self.load(token, config:, warn:) + end + end + end + + sig { params(path: Pathname, config: T.nilable(Config), warn: T::Boolean).returns(Cask) } + def self.load_from_installed_caskfile(path, config: nil, warn: true) + loader = FromInstalledPathLoader.try_new(path, warn:) + loader ||= NullLoader.new(path) + + loader.load(config:) + end + def self.default_path(token) find_cask_in_tap(token.to_s.downcase, CoreCaskTap.instance) end diff --git a/Library/Homebrew/cask/caskroom.rb b/Library/Homebrew/cask/caskroom.rb index 5f086e86c2..b674c6a35d 100644 --- a/Library/Homebrew/cask/caskroom.rb +++ b/Library/Homebrew/cask/caskroom.rb @@ -56,7 +56,7 @@ module Cask sig { params(config: T.nilable(Config)).returns(T::Array[Cask]) } def self.casks(config: nil) tokens.sort.filter_map do |token| - CaskLoader.load(token, config:, warn: false) + CaskLoader.load_installed_cask(token, config:, warn: false) rescue TapCaskAmbiguityError => e T.must(e.loaders.first).load(config:) rescue diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 763c5dda1a..9eadc7f6ec 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -189,7 +189,18 @@ module Homebrew if want_keg_like_cask && (installed_caskfile = candidate_cask.installed_caskfile) && installed_caskfile.exist? - Cask::CaskLoader.load(installed_caskfile) + cask = Cask::CaskLoader.load_from_installed_caskfile(installed_caskfile) + + requested_tap, requested_token = Tap.with_cask_token(name) + if requested_tap && requested_token + installed_cask_tap = cask.tab.tap + + if installed_cask_tap && installed_cask_tap != requested_tap + raise Cask::TapCaskUnavailableError.new(requested_tap, requested_token) + end + end + + cask else candidate_cask end @@ -405,6 +416,16 @@ module Homebrew rack = Formulary.to_rack(name.downcase) kegs = rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : [] + + requested_tap, requested_formula = Tap.with_formula_name(name) + if requested_tap && requested_formula + kegs = kegs.select do |keg| + keg.tab.tap == requested_tap + end + + raise NoSuchKegFromTapError.new(requested_formula, requested_tap) if kegs.none? + end + raise NoSuchKegError, name if kegs.none? [rack, kegs] diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 83e0cccd2d..95e97298d5 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -61,6 +61,18 @@ class NoSuchKegError < RuntimeError end end +# Raised when a keg from a specific tap doesn't exist. +class NoSuchKegFromTapError < RuntimeError + attr_reader :name, :tap + + sig { params(name: String, tap: Tap).void } + def initialize(name, tap) + @name = name + @tap = tap + super "No such keg: #{HOMEBREW_CELLAR}/#{name} from tap #{tap}" + end +end + # Raised when an invalid attribute is used in a formula. class FormulaValidationError < StandardError attr_reader :attr, :formula diff --git a/Library/Homebrew/test/cask/cask_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader_spec.rb index c867e0b322..d6a6ec9363 100644 --- a/Library/Homebrew/test/cask/cask_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader_spec.rb @@ -184,4 +184,60 @@ RSpec.describe Cask::CaskLoader, :cask do end end end + + describe "::load_installed_cask" do + let(:foo_tap) { Tap.fetch("user", "foo") } + let(:bar_tap) { Tap.fetch("user", "bar") } + + let(:blank_tab) { instance_double(Cask::Tab, tap: nil) } + let(:installed_tab) { instance_double(Cask::Tab, tap: bar_tap) } + + let(:cask_with_foo_tap) { instance_double(Cask::Cask, token: "test-cask", tap: foo_tap) } + let(:cask_with_bar_tap) { instance_double(Cask::Cask, token: "test-cask", tap: bar_tap) } + + let(:load_args) { { config: nil, warn: true } } + + before do + allow(described_class).to receive(:load).with("test-cask", load_args).and_return(cask_with_foo_tap) + allow(described_class).to receive(:load).with("user/foo/test-cask", load_args).and_return(cask_with_foo_tap) + allow(described_class).to receive(:load).with("user/bar/test-cask", load_args).and_return(cask_with_bar_tap) + end + + it "returns the correct cask when no tap is specified and no tab exists" do + allow_any_instance_of(Cask::Cask).to receive(:tab).and_return(blank_tab) + expect(described_class).to receive(:load).with("test-cask", load_args) + + expect(described_class.load_installed_cask("test-cask").tap).to eq(foo_tap) + end + + it "returns the correct cask when no tap is specified but a tab exists" do + allow_any_instance_of(Cask::Cask).to receive(:tab).and_return(installed_tab) + expect(described_class).to receive(:load).with("user/bar/test-cask", load_args) + + expect(described_class.load_installed_cask("test-cask").tap).to eq(bar_tap) + end + + it "returns the correct cask when a tap is specified and no tab exists" do + allow_any_instance_of(Cask::Cask).to receive(:tab).and_return(blank_tab) + expect(described_class).to receive(:load).with("user/bar/test-cask", load_args) + + expect(described_class.load_installed_cask("user/bar/test-cask").tap).to eq(bar_tap) + end + + it "returns the correct cask when no tap is specified and a tab exists" do + allow_any_instance_of(Cask::Cask).to receive(:tab).and_return(installed_tab) + expect(described_class).to receive(:load).with("user/foo/test-cask", load_args) + + expect(described_class.load_installed_cask("user/foo/test-cask").tap).to eq(foo_tap) + end + + it "returns the correct cask when no tap is specified and the tab lists an tap that isn't installed" do + allow_any_instance_of(Cask::Cask).to receive(:tab).and_return(installed_tab) + expect(described_class).to receive(:load).with("user/bar/test-cask", load_args) + .and_raise(Cask::CaskUnavailableError.new("test-cask", bar_tap)) + expect(described_class).to receive(:load).with("test-cask", load_args) + + expect(described_class.load_installed_cask("test-cask").tap).to eq(foo_tap) + end + end end diff --git a/Library/Homebrew/test/cli/named_args_spec.rb b/Library/Homebrew/test/cli/named_args_spec.rb index 0da0436384..fd9e9c135f 100644 --- a/Library/Homebrew/test/cli/named_args_spec.rb +++ b/Library/Homebrew/test/cli/named_args_spec.rb @@ -217,6 +217,36 @@ RSpec.describe Homebrew::CLI::NamedArgs do it "when there are no matching kegs returns an empty array" do expect(described_class.new.to_kegs).to be_empty end + + it "raises an error when a Keg is unavailable" do + expect { described_class.new("baz").to_kegs }.to raise_error NoSuchKegError + end + + context "when a keg specifies a tap" do + let(:tab) { instance_double(Tab, tap: Tap.fetch("user", "repo")) } + + before do + allow_any_instance_of(Keg).to receive(:tab).and_return(tab) + end + + it "returns kegs if no tap is specified" do + stub_formula_loader bar, "user/repo/bar" + + expect(described_class.new("bar").to_kegs.map(&:name)).to eq ["bar"] + end + + it "returns kegs if the tap is specified" do + stub_formula_loader bar, "user/repo/bar" + + expect(described_class.new("user/repo/bar").to_kegs.map(&:name)).to eq ["bar"] + end + + it "raises an error if there is no tap match" do + stub_formula_loader bar, "other/tap/bar" + + expect { described_class.new("other/tap/bar").to_kegs }.to raise_error(NoSuchKegFromTapError) + end + end end describe "#to_default_kegs" do diff --git a/Library/Homebrew/test/exceptions_spec.rb b/Library/Homebrew/test/exceptions_spec.rb index 60591c3831..86a67332d6 100644 --- a/Library/Homebrew/test/exceptions_spec.rb +++ b/Library/Homebrew/test/exceptions_spec.rb @@ -19,6 +19,14 @@ RSpec.describe "Exception" do end end + describe NoSuchKegFromTapError do + subject(:error) { described_class.new("foo", tap) } + + let(:tap) { instance_double(Tap, to_s: "u/r") } + + it(:to_s) { expect(error.to_s).to eq("No such keg: #{HOMEBREW_CELLAR}/foo from tap u/r") } + end + describe NoSuchKegError do subject(:error) { described_class.new("foo") }