diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 751e1d3009..1f6a7a285c 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -438,7 +438,7 @@ module Cask end def self.default_path(token) - Tap.default_cask_tap.cask_dir/"#{token.to_s.downcase}.rb" + CoreCaskTap.instance.cask_dir/"#{token.to_s.downcase}.rb" end def self.tap_paths(token, warn: true) diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index 33d911d75d..5139671a40 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -197,7 +197,11 @@ module Homebrew formulae, casks = args.named.to_formulae_and_casks .partition { |formula_or_cask| formula_or_cask.is_a?(Formula) } rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError - retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?) + cask_tap = CoreCaskTap.instance + if !cask_tap.installed? && (args.cask? || Tap.untapped_official_taps.exclude?(cask_tap.name)) + cask_tap.ensure_installed! + retry if cask_tap.installed? + end raise end diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb index ef58ff4c58..f568f61c54 100644 --- a/Library/Homebrew/cmd/update-report.rb +++ b/Library/Homebrew/cmd/update-report.rb @@ -149,7 +149,7 @@ module Homebrew updated_taps = [] Tap.each do |tap| next if !tap.git? || tap.git_repo.origin_url.nil? - next if (tap.core_tap? || tap == "homebrew/cask") && !Homebrew::EnvConfig.no_install_from_api? + next if (tap.core_tap? || tap.core_cask_tap?) && !Homebrew::EnvConfig.no_install_from_api? if ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"].present? && tap.core_tap? && Settings.read("linuxbrewmigrated") != "true" @@ -193,7 +193,7 @@ module Homebrew unless Homebrew::EnvConfig.no_install_from_api? api_cache = Homebrew::API::HOMEBREW_CACHE_API core_tap = CoreTap.instance - cask_tap = Tap.fetch("homebrew/cask") + cask_tap = CoreCaskTap.instance [ [:formula, core_tap, core_tap.formula_dir], [:cask, cask_tap, cask_tap.cask_dir], @@ -402,7 +402,7 @@ module Homebrew return if (HOMEBREW_PREFIX/".homebrewdocker").exist? core_tap = CoreTap.instance - cask_tap = Tap.default_cask_tap + cask_tap = CoreCaskTap.instance return if !core_tap.installed? && !cask_tap.installed? puts "Installing from the API is now the default behaviour!" @@ -504,7 +504,7 @@ class Reporter new_name = tap.cask_renames[old_name] next unless new_name - new_full_name = if tap.name == "homebrew/cask" + new_full_name = if tap.core_cask_tap? new_name else "#{tap}/#{new_name}" @@ -518,7 +518,7 @@ class Reporter old_name = tap.cask_renames.key(new_name) next unless old_name - old_full_name = if tap.name == "homebrew/cask" + old_full_name = if tap.core_cask_tap? old_name else "#{tap}/#{old_name}" diff --git a/Library/Homebrew/dev-cmd/generate-cask-api.rb b/Library/Homebrew/dev-cmd/generate-cask-api.rb index 1bbd987fcf..24f0816fad 100644 --- a/Library/Homebrew/dev-cmd/generate-cask-api.rb +++ b/Library/Homebrew/dev-cmd/generate-cask-api.rb @@ -42,7 +42,7 @@ module Homebrew def generate_cask_api args = generate_cask_api_args.parse - tap = Tap.default_cask_tap + tap = CoreCaskTap.instance raise TapUnavailableError, tap.name unless tap.installed? unless args.dry_run? diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 4d888797b7..41de599b59 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -531,7 +531,7 @@ module Homebrew end def check_casktap_integrity - default_cask_tap = Tap.default_cask_tap + default_cask_tap = CoreCaskTap.instance return unless default_cask_tap.installed? broken_tap(default_cask_tap) || examine_git_origin(default_cask_tap.git_repo, default_cask_tap.remote) @@ -861,7 +861,7 @@ module Homebrew return if Homebrew::EnvConfig.no_install_from_api? return if Homebrew::Settings.read("devcmdrun") == "true" - cask_tap = Tap.fetch("homebrew", "cask") + cask_tap = CoreCaskTap.instance return unless cask_tap.installed? <<~EOS @@ -921,7 +921,7 @@ module Homebrew end def check_cask_taps - default_cask_tap = Tap.default_cask_tap + default_cask_tap = CoreCaskTap.instance alt_taps = Tap.select { |t| t.cask_dir.exist? && t != default_cask_tap } error_tap_paths = [] diff --git a/Library/Homebrew/extend/os/mac/tap.rb b/Library/Homebrew/extend/os/mac/tap.rb index 97abb1a3fe..8d9bace415 100644 --- a/Library/Homebrew/extend/os/mac/tap.rb +++ b/Library/Homebrew/extend/os/mac/tap.rb @@ -3,12 +3,15 @@ class Tap def self.install_default_cask_tap_if_necessary(force: false) - return false if default_cask_tap.installed? + odeprecated "Tap.install_default_cask_tap_if_necessary", "CoreCaskTap.ensure_installed!" + + cask_tap = CoreCaskTap.instance + return false if cask_tap.installed? return false unless Homebrew::EnvConfig.no_install_from_api? return false if Homebrew::EnvConfig.automatically_set_no_install_from_api? - return false if !force && Tap.untapped_official_taps.include?(default_cask_tap.name) + return false if !force && Tap.untapped_official_taps.include?(cask_tap.name) - default_cask_tap.install + cask_tap.install true end end diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 6988916b33..b0196c0176 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -52,6 +52,7 @@ class Tap repo = repo.sub(HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX, "") return CoreTap.instance if ["Homebrew", "Linuxbrew"].include?(user) && ["core", "homebrew"].include?(repo) + return CoreCaskTap.instance if user == "Homebrew" && repo == "cask" cache_key = "#{user}/#{repo}".downcase cache.fetch(cache_key) { |key| cache[key] = Tap.new(user, repo) } @@ -64,13 +65,17 @@ class Tap fetch(match[:user], match[:repo]) end - sig { returns(T.attached_class) } + sig { returns(CoreCaskTap) } def self.default_cask_tap - @default_cask_tap ||= fetch("Homebrew", "cask") + odeprecated "Tap.default_cask_tap", "CoreCaskTap.instance" + + CoreCaskTap.instance end sig { params(force: T::Boolean).returns(T::Boolean) } def self.install_default_cask_tap_if_necessary(force: false) + odeprecated "Tap.install_default_cask_tap_if_necessary", "CoreCaskTap.ensure_installed!" + false end @@ -122,6 +127,7 @@ class Tap @cask_dir = nil @command_dir = nil @formula_files = nil + @cask_files = nil @alias_dir = nil @alias_files = nil @aliases = nil @@ -138,6 +144,13 @@ class Tap remove_instance_variable(:@private) if instance_variable_defined?(:@private) end + sig { void } + def ensure_installed! + return if installed? + + install + end + # The remote path to this {Tap}. # e.g. `https://github.com/user/homebrew-repo` def remote @@ -245,6 +258,12 @@ class Tap false end + # @private + sig { returns(T::Boolean) } + def core_cask_tap? + false + end + # Install this {Tap}. # # @param clone_target [String] If passed, it will be used as the clone remote. @@ -299,7 +318,7 @@ class Tap args << "-q" if quiet path.cd { safe_system "git", *args } return - elsif (core_tap? || name == "homebrew/cask") && !Homebrew::EnvConfig.no_install_from_api? && !force + elsif (core_tap? || core_cask_tap?) && !Homebrew::EnvConfig.no_install_from_api? && !force # odeprecated: move to odie in the next minor release. This may break some CI so we should give notice. opoo "Tapping #{name} is no longer typically necessary.\n" \ "Add #{Formatter.option("--force")} if you are sure you need one." @@ -553,13 +572,18 @@ class Tap sig { params(tap: Tap).returns(T::Hash[String, Pathname]) } def self.cask_files_by_name(tap) cache_key = "cask_files_by_name_#{tap}" - cache.fetch(cache_key) do |key| - cache[key] = tap.cask_files.each_with_object({}) do |file, hash| - # If there's more than one file with the same basename: intentionally - # ignore the later ones here. - hash[file.basename.to_s] ||= file - end + cache[key] = tap.cask_files_by_name + end + end + + # @private + sig { returns(T::Hash[String, Pathname]) } + def cask_files_by_name + cask_files.each_with_object({}) do |file, hash| + # If there's more than one file with the same basename: intentionally + # ignore the later ones here. + hash[file.basename.to_s] ||= file end end @@ -696,9 +720,7 @@ class Tap # Hash with tap cask renames. sig { returns(T::Hash[String, String]) } def cask_renames - @cask_renames ||= if name == "homebrew/cask" && !Homebrew::EnvConfig.no_install_from_api? - Homebrew::API::Cask.all_renames - elsif (rename_file = path/HOMEBREW_TAP_CASK_RENAMES_FILE).file? + @cask_renames ||= if (rename_file = path/HOMEBREW_TAP_CASK_RENAMES_FILE).file? JSON.parse(rename_file.read) else {} @@ -718,11 +740,7 @@ class Tap # Hash with tap migrations. sig { returns(Hash) } def tap_migrations - @tap_migrations ||= if name == "homebrew/cask" && !Homebrew::EnvConfig.no_install_from_api? - migrations, = Homebrew::API.fetch_json_api_file "cask_tap_migrations.jws.json", - stale_seconds: TAP_MIGRATIONS_STALE_SECONDS - migrations - elsif (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file? + @tap_migrations ||= if (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file? JSON.parse(migration_file.read) else {} @@ -749,9 +767,7 @@ class Tap # @private sig { returns(T::Boolean) } def should_report_analytics? - return !Homebrew::EnvConfig.no_install_from_api? && official? unless installed? - - !private? + installed? && !private? end sig { params(other: T.nilable(T.any(String, Tap))).returns(T::Boolean) } @@ -867,29 +883,51 @@ class Tap end end +class AbstractCoreTap < Tap + extend T::Helpers + + abstract! + + sig { returns(T.attached_class) } + def self.instance + @instance ||= T.unsafe(self).new + end + + sig { override.void } + def ensure_installed! + return unless Homebrew::EnvConfig.no_install_from_api? + return if Homebrew::EnvConfig.automatically_set_no_install_from_api? + + super + end + + sig { void } + def self.ensure_installed! + instance.ensure_installed! + end + + # @private + sig { override.returns(T::Boolean) } + def should_report_analytics? + return super if Homebrew::EnvConfig.no_install_from_api? + + true + end +end + # A specialized {Tap} class for the core formulae. -class CoreTap < Tap +class CoreTap < AbstractCoreTap # @private sig { void } def initialize super "Homebrew", "core" end - sig { returns(CoreTap) } - def self.instance - @instance ||= new - end - - sig { void } - def self.ensure_installed! - return if instance.installed? - return unless Homebrew::EnvConfig.no_install_from_api? - return if Homebrew::EnvConfig.automatically_set_no_install_from_api? - - # Tests override homebrew-core locations and we don't want to auto-tap in them. + sig { override.void } + def ensure_installed! return if ENV["HOMEBREW_TESTS"] - instance.install + super end sig { returns(String) } @@ -1046,20 +1084,74 @@ class CoreTap < Tap def formula_files_by_name return super if Homebrew::EnvConfig.no_install_from_api? - formula_names.each_with_object({}) do |name, hash| + Homebrew::API::Formula.all_formulae.each_with_object({}) do |item, hash| + name, formula_hash = item # If there's more than one file with the same basename: intentionally # ignore the later ones here. - hash[name] ||= sharded_formula_path(name) + hash[name] ||= path/formula_hash["ruby_source_path"] + end + end +end + +# A specialized {Tap} class for homebrew-cask. +class CoreCaskTap < AbstractCoreTap + # @private + sig { void } + def initialize + super "Homebrew", "cask" + end + + # @private + sig { override.returns(T::Boolean) } + def core_cask_tap? + true + end + + sig { override.returns(T::Array[Pathname]) } + def cask_files + return super if Homebrew::EnvConfig.no_install_from_api? || installed? + + raise TapUnavailableError, name + end + + sig { override.returns(T::Array[String]) } + def cask_tokens + return super if Homebrew::EnvConfig.no_install_from_api? + + Homebrew::API::Cask.all_casks.keys + end + + # @private + sig { override.returns(T::Hash[String, Pathname]) } + def cask_files_by_name + return super if Homebrew::EnvConfig.no_install_from_api? + + Homebrew::API::Cask.all_casks.each_with_object({}) do |item, hash| + name, cask_hash = item + # If there's more than one file with the same basename: intentionally + # ignore the later ones here. + hash[name] ||= path/cask_hash["ruby_source_path"] end end - private + sig { override.returns(T::Hash[String, String]) } + def cask_renames + @cask_renames ||= if Homebrew::EnvConfig.no_install_from_api? + super + else + Homebrew::API::Cask.all_renames + end + end - # @private - sig { params(name: String).returns(Pathname) } - def sharded_formula_path(name) - # TODO: add sharding logic. - formula_dir/"#{name}.rb" + sig { override.returns(Hash) } + def tap_migrations + @tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api? + super + else + migrations, = Homebrew::API.fetch_json_api_file "cask_tap_migrations.jws.json", + stale_seconds: TAP_MIGRATIONS_STALE_SECONDS + migrations + end end end diff --git a/Library/Homebrew/test/cask/cask_loader/from_tap_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_tap_loader_spec.rb index 2f1ca8bf77..d352527261 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_tap_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_tap_loader_spec.rb @@ -3,10 +3,11 @@ describe Cask::CaskLoader::FromTapLoader do let(:cask_name) { "testball" } let(:cask_full_name) { "homebrew/cask/#{cask_name}" } - let(:cask_path) { Tap.default_cask_tap.cask_dir/"#{cask_name}.rb" } + let(:cask_path) { CoreCaskTap.instance.cask_dir/"#{cask_name}.rb" } describe "#load" do before do + CoreCaskTap.instance.clear_cache cask_path.parent.mkpath cask_path.write <<~RUBY cask '#{cask_name}' do @@ -24,7 +25,7 @@ describe Cask::CaskLoader::FromTapLoader do end context "with sharded Cask directory" do - let(:cask_path) { Tap.default_cask_tap.cask_dir/cask_name[0]/"#{cask_name}.rb" } + let(:cask_path) { CoreCaskTap.instance.cask_dir/cask_name[0]/"#{cask_name}.rb" } it "returns a Cask" do expect(described_class.new(cask_full_name).load(config: nil)).to be_a(Cask::Cask) diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index c8071cd408..10f69df402 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -24,7 +24,7 @@ describe Cask::Cask, :cask do end describe "load" do - let(:tap_path) { Tap.default_cask_tap.path } + let(:tap_path) { CoreCaskTap.instance.path } let(:file_dirname) { Pathname.new(__FILE__).dirname } let(:relative_tap_path) { tap_path.relative_path_from(file_dirname) } diff --git a/Library/Homebrew/test/diagnostic_checks_spec.rb b/Library/Homebrew/test/diagnostic_checks_spec.rb index 7d51a90d3b..d04c5d9a52 100644 --- a/Library/Homebrew/test/diagnostic_checks_spec.rb +++ b/Library/Homebrew/test/diagnostic_checks_spec.rb @@ -119,7 +119,7 @@ describe Homebrew::Diagnostic::Checks do ENV.delete("HOMEBREW_DEVELOPER") ENV.delete("HOMEBREW_NO_INSTALL_FROM_API") - allow(CoreTap).to receive(:installed?).and_return(true) + expect_any_instance_of(CoreTap).to receive(:installed?).and_return(true) expect(checks.check_for_unnecessary_core_tap).to match("You have an unnecessary local Core tap") end @@ -128,9 +128,7 @@ describe Homebrew::Diagnostic::Checks do ENV.delete("HOMEBREW_DEVELOPER") ENV.delete("HOMEBREW_NO_INSTALL_FROM_API") - cask_tap = Tap.new("homebrew", "cask") - allow(Tap).to receive(:fetch).with("homebrew", "cask").and_return(cask_tap) - allow(cask_tap).to receive(:installed?).and_return(true) + expect_any_instance_of(CoreCaskTap).to receive(:installed?).and_return(true) expect(checks.check_for_unnecessary_cask_tap).to match("unnecessary local Cask tap") end diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb index c0067277a2..003705110a 100644 --- a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb +++ b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb @@ -38,7 +38,7 @@ RSpec.shared_context "Homebrew Cask", :needs_macos do # rubocop:disable RSpec/Co begin Cask::Config::DEFAULT_DIRS_PATHNAMES.each_value(&:mkpath) - Tap.default_cask_tap.tap do |tap| + CoreCaskTap.instance.tap do |tap| FileUtils.mkdir_p tap.path.dirname FileUtils.ln_sf TEST_FIXTURE_DIR.join("cask"), tap.path end @@ -52,7 +52,7 @@ RSpec.shared_context "Homebrew Cask", :needs_macos do # rubocop:disable RSpec/Co ensure FileUtils.rm_rf Cask::Config::DEFAULT_DIRS_PATHNAMES.values FileUtils.rm_rf [Cask::Config.new.binarydir, Cask::Caskroom.path, Cask::Cache.path] - Tap.default_cask_tap.path.unlink + CoreCaskTap.instance.path.unlink third_party_tap.path.unlink FileUtils.rm_rf third_party_tap.path.parent end