From 46416906745c122068f962d69ecf47c1bca8ef30 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 12:14:26 +0100 Subject: [PATCH 01/19] Refactor `Formulary::loader_for`. --- Library/Homebrew/api/formula.rb | 4 +- Library/Homebrew/diagnostic.rb | 9 +- Library/Homebrew/exceptions.rb | 16 +- Library/Homebrew/formula.rb | 19 +- Library/Homebrew/formulary.rb | 385 ++++++++++++++++-------- Library/Homebrew/tap_constants.rb | 14 +- Library/Homebrew/test/formula_spec.rb | 30 +- Library/Homebrew/test/formulary_spec.rb | 23 +- 8 files changed, 321 insertions(+), 179 deletions(-) diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index a8d2215295f72..8704ec6ad4342 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -59,7 +59,7 @@ def download_and_cache_data! end private :download_and_cache_data! - sig { returns(Hash) } + sig { returns(T::Hash[String, Hash]) } def all_formulae unless cache.key?("formulae") json_updated = download_and_cache_data! @@ -69,7 +69,7 @@ def all_formulae cache["formulae"] end - sig { returns(Hash) } + sig { returns(T::Hash[String, String]) } def all_aliases unless cache.key?("aliases") json_updated = download_and_cache_data! diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index b039069da96d0..c14bbaeb744f8 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -835,14 +835,7 @@ def check_deleted_formula kegs = Keg.all deleted_formulae = kegs.map do |keg| - next if Formulary.tap_paths(keg.name).any? - - unless EnvConfig.no_install_from_api? - # Formulae installed from the API should not count as deleted formulae - # but may not have a tap listed in their tab - tap = Tab.for_keg(keg).tap - next if (tap.blank? || tap.core_tap?) && Homebrew::API::Formula.all_formulae.key?(keg.name) - end + next if Formulary::FromNameLoader.try_new(keg.name, warn: false).nil? keg.name end.compact.uniq diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index e0861796c5246..e402a3b8280a6 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -259,19 +259,19 @@ def initialize(tap, name, error) # Raised when a formula with the same name is found in multiple taps. class TapFormulaAmbiguityError < RuntimeError - attr_reader :name, :paths, :formulae + attr_reader :name, :taps - def initialize(name, paths) + def initialize(name, taps) @name = name - @paths = paths - @formulae = paths.map do |path| - "#{Tap.from_path(path).name}/#{path.basename(".rb")}" - end + @taps = taps + + formulae = taps.map { |tap| "#{tap}/#{name}" } + formula_list = formulae.map { |f| "\n * #{f}" }.join super <<~EOS - Formulae found in multiple taps: #{formulae.map { |f| "\n * #{f}" }.join} + Formulae found in multiple taps:#{formula_list} - Please use the fully-qualified name (e.g. #{formulae.first}) to refer to the formula. + Please use the fully-qualified name (e.g. #{formulae.first}) to refer to a specific formula. EOS end end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 17c7f2ee77f5e..3b65c6248be60 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -77,7 +77,7 @@ class Formula # The path to the alias that was used to identify this {Formula}. # e.g. `/usr/local/Library/Taps/homebrew/homebrew-core/Aliases/another-name-for-this-formula` - sig { returns(T.any(NilClass, Pathname, String)) } + sig { returns(T.nilable(Pathname)) } attr_reader :alias_path # The name of the alias that was used to identify this {Formula}. @@ -199,7 +199,7 @@ class Formula # @private sig { - params(name: String, path: Pathname, spec: Symbol, alias_path: T.any(NilClass, Pathname, String), + params(name: String, path: Pathname, spec: Symbol, alias_path: T.nilable(Pathname), tap: T.nilable(Tap), force_bottle: T::Boolean).void } def initialize(name, path, spec, alias_path: nil, tap: nil, force_bottle: false) @@ -326,18 +326,22 @@ def validate_attributes! # The alias path that was used to install this formula, if it exists. # Can differ from {#alias_path}, which is the alias used to find the formula, # and is specified to this instance. + sig { returns(T.nilable(Pathname)) } def installed_alias_path build_tab = build path = build_tab.source["path"] if build_tab.is_a?(Tab) + return unless path&.match?(%r{#{HOMEBREW_TAP_DIR_REGEX}/Aliases}o) - return unless File.symlink?(path) + + path = Pathname(path) + return unless path.symlink? path end sig { returns(T.nilable(String)) } def installed_alias_name - File.basename(installed_alias_path) if installed_alias_path + installed_alias_path&.basename&.to_s end def full_installed_alias_name @@ -346,14 +350,13 @@ def full_installed_alias_name # The path that was specified to find this formula. def specified_path - alias_pathname = Pathname(T.must(alias_path)) if alias_path.present? - return alias_pathname if alias_pathname&.exist? + return alias_path if alias_path&.exist? return @unresolved_path if @unresolved_path.exist? return local_bottle_path if local_bottle_path.presence&.exist? - alias_pathname || @unresolved_path + alias_path || @unresolved_path end # The name specified to find this formula. @@ -2362,7 +2365,7 @@ def to_hash_with_variations(hash_method: :to_hash) # Take from API, merging in local install status. if loaded_from_api? && !Homebrew::EnvConfig.no_install_from_api? - json_formula = Homebrew::API::Formula.all_formulae[name].dup + json_formula = Homebrew::API::Formula.all_formulae.fetch(name).dup return json_formula.merge( hash.slice("name", "installed", "linked_keg", "pinned", "outdated"), ) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 978f06fde8228..1f1f09ce27f23 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -16,6 +16,7 @@ # # @api private module Formulary + extend Context extend Cachable URL_START_REGEX = %r{(https?|ftp|file)://} @@ -480,17 +481,26 @@ class FormulaLoader include Context # The formula's name + sig { returns(String) } attr_reader :name + # The formula's ruby file's path or filename + sig { returns(Pathname) } attr_reader :path + # The name used to install the formula + sig { returns(T.nilable(Pathname)) } attr_reader :alias_path + # The formula's tap (nil if it should be implicitly determined) + sig { returns(T.nilable(Tap)) } attr_reader :tap - def initialize(name, path, tap: nil) + sig { params(name: String, path: Pathname, alias_path: Pathname, tap: Tap).void } + def initialize(name, path, alias_path: T.unsafe(nil), tap: T.unsafe(nil)) @name = name @path = path + @alias_path = alias_path if alias_path @tap = tap end @@ -519,7 +529,17 @@ def load_file(flags:, ignore_errors:) end # Loads a formula from a bottle. - class BottleLoader < FormulaLoader + class FromBottleLoader < FormulaLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + ref = ref.to_s + + new(ref) if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(ref) + end + def initialize(bottle_name) case bottle_name when URL_START_REGEX @@ -562,27 +582,68 @@ def get_formula(spec, force_bottle: false, flags: [], ignore_errors: false, **) end end - # Loads a formula from a path to an alias. - class AliasLoader < FormulaLoader - def initialize(alias_path) - path = alias_path.resolved_path - name = path.basename(".rb").to_s - super name, path - @alias_path = alias_path.to_s - end - end - # Loads formulae from disk using a path. class FromPathLoader < FormulaLoader - def initialize(path) - path = Pathname.new(path).expand_path + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + path = case ref + when String + Pathname(ref) + when Pathname + ref + else + return + end + + return unless path.expand_path.exist? + + options = if path.symlink? + alias_path = path + path = alias_path.resolved_path + { alias_path: alias_path } + else + {} + end + + return if path.extname != ".rb" + + new(path, **options) + end + + sig { params(path: T.any(Pathname, String), alias_path: Pathname).void } + def initialize(path, alias_path: T.unsafe(nil)) + path = Pathname(path).expand_path name = path.basename(".rb").to_s - super name, path, tap: Homebrew::API.tap_from_source_download(path) + alias_path = alias_path&.expand_path + alias_dir = alias_path&.dirname + + tap = Tap.from_path(path) || Homebrew::API.tap_from_source_download(path) + + options = { + alias_path: (alias_path if alias_dir == tap&.alias_dir), + tap: tap, + + }.compact + + super(name, path, **options) end end - # Loads formulae from URLs. - class FromUrlLoader < FormulaLoader + # Loads formula from a URI. + class FromURILoader < FormulaLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + ref = ref.to_s + + new(ref, from: from) if URL_START_REGEX.match?(ref) + end + attr_reader :url sig { params(url: T.any(URI::Generic, String), from: T.nilable(Symbol)).void } @@ -621,7 +682,45 @@ def load_file(flags:, ignore_errors:) end # Loads tapped formulae. - class TapLoader < FormulaLoader + class FromTapLoader < FormulaLoader + sig { returns(Tap) } + attr_reader :tap + + sig { returns(Pathname) } + attr_reader :path + + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + ref = ref.to_s + return unless (match = ref.match(HOMEBREW_TAP_FORMULA_REGEX)) + + alias_name = T.cast(match[:name], String) + + name, tap, type = Formulary.tap_formula_name_type(ref, warn: warn) + path = Formulary.find_formula_in_tap(name, tap) + + options = if type == :alias + { alias_name: alias_name.downcase } + else + {} + end + + new(name, path, tap: tap, **options) + end + + sig { params(name: String, path: Pathname, tap: Tap, alias_name: String).void } + def initialize(name, path, tap:, alias_name: T.unsafe(nil)) + options = { + alias_path: (tap.alias_dir/alias_name if alias_name), + tap: tap, + }.compact + + super(name, path, **options) + end + def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false) super rescue FormulaUnreadableError => e @@ -638,17 +737,90 @@ def load_file(flags:, ignore_errors:) e.issues_url = tap.issues_url || tap.to_s raise end + end + + # Loads a formula from a name, as long as it exists only in a single tap. + module FromNameLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(FromTapLoader)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + return unless ref.is_a?(String) + return if ref.include?("/") + + name = ref + + loaders = Tap.map { |tap| FromTapLoader.try_new("#{tap}/#{name}") } + .compact + .select { _1.path.exist? } + + case loaders.count + when 1 + loaders.first + when 2..Float::INFINITY + default_tap_loaders, other_loaders = *loaders.partition { _1.tap.core_tap? } + default_tap_loader = default_tap_loaders.first if default_tap_loaders.count + + # Put default tap last so that the error message always recommends + # using the fully-qualified name for non-default taps. + taps = other_loaders.map(&:tap) + default_tap_loaders.map(&:tap) + + error = TapFormulaAmbiguityError.new(name, taps) + + raise error unless default_tap_loader + + opoo error if warn + default_tap_loader + end + end + end - private + # Loads a formula from a formula file in a keg. + class FromKegLoader < FormulaLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + ref = ref.to_s + + return unless (keg_formula = HOMEBREW_PREFIX/"opt/#{ref}/.brew/#{ref}.rb").file? - def find_formula_from_name(name, tap) - Formulary.find_formula_in_tap(name, tap) + new(ref, keg_formula) + end + end + + # Loads a formula from a cached formula file. + class FromCacheLoader < FormulaLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + ref = ref.to_s + + return unless (cached_formula = HOMEBREW_CACHE_FORMULA/"#{ref}.rb").file? + + new(ref, cached_formula) end end # Pseudo-loader which will raise a {FormulaUnavailableError} when trying to load the corresponding formula. class NullLoader < FormulaLoader - def initialize(name) + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + return if ref.is_a?(URI::Generic) + + new(ref) + end + + sig { params(ref: T.any(String, Pathname)).void } + def initialize(ref) + name = File.basename(ref, ".rb") super name, Formulary.core_path(name) end @@ -674,10 +846,44 @@ def klass(flags:, ignore_errors:) end end - # Load formulae from the API. - class FormulaAPILoader < FormulaLoader - def initialize(name) - super name, Formulary.core_path(name) + # Load a formula from the API. + class FromAPILoader < FormulaLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(T.attached_class)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + return if Homebrew::EnvConfig.no_install_from_api? + return unless ref.is_a?(String) + + return unless (match = ref.match(HOMEBREW_DEFAULT_TAP_FORMULA_REGEX)) + + name = alias_name = T.cast(match[:name], String) + unless Homebrew::API::Formula.all_formulae.key?(name) || Homebrew::API::Formula.all_aliases.key?(name) || Homebrew::API::Formula.all_renames.key?(name) + return + end + + ref = "#{CoreTap.instance}/#{name}" + + name, tap, type = Formulary.tap_formula_name_type(ref, warn: warn) + + options = if type == :alias + { alias_name: alias_name.downcase } + else + {} + end + + new(name, tap: tap, **options) + end + + sig { params(name: String, tap: Tap, alias_name: String).void } + def initialize(name, tap: T.unsafe(nil), alias_name: T.unsafe(nil)) + options = { + alias_path: (CoreTap.instance.alias_dir/alias_name if alias_name), + tap: tap, + }.compact + + super(name, Formulary.core_path(name), **options) end def klass(flags:, ignore_errors:) @@ -694,14 +900,6 @@ def load_from_api(flags:) end end - # Load aliases from the API. - class AliasAPILoader < FormulaAPILoader - def initialize(alias_name) - super Homebrew::API::Formula.all_aliases[alias_name] - @alias_path = Formulary.core_alias_path(alias_name).to_s - end - end - # Return a {Formula} instance for the given reference. # `ref` is a string containing: # @@ -741,6 +939,7 @@ def self.factory( force_bottle: force_bottle, flags: flags, ignore_errors: ignore_errors }.compact + formula = loader_for(ref, **loader_options) .get_formula(spec, **formula_options) @@ -906,9 +1105,17 @@ def self.tap_formula_name_type(tapped_name, warn:) user, repo, name = tapped_name.split("/", 3).map(&:downcase) tap = Tap.fetch(user, repo) type = nil - alias_name = tap.core_tap? ? name : "#{tap}/#{name}" - if (possible_alias = tap.alias_table[alias_name].presence) + # FIXME: Remove the need to do this here. + alias_table_key = if tap.core_tap? + name + else + "#{tap}/#{name}" + end + + if (possible_alias = tap.alias_table[alias_table_key].presence) + # FIXME: Remove the need to split the name and instead make + # the alias table only contain short names. name = possible_alias.split("/").last type = :alias elsif (new_name = tap.formula_renames[name].presence) @@ -938,103 +1145,31 @@ def self.tap_formula_name_type(tapped_name, warn:) [name, tap, type] end - def self.tap_loader_for(tapped_name, warn:) - name, tap, type = Formulary.tap_formula_name_type(tapped_name, warn: warn) - - if tap.core_tap? && !Homebrew::EnvConfig.no_install_from_api? - if type == :alias - return AliasAPILoader.new(name) - elsif Homebrew::API::Formula.all_formulae.key?(name) - return FormulaAPILoader.new(name) + def self.loader_for(ref, from: T.unsafe(nil), warn: true) + options = { from: from, warn: warn }.compact + + [ + FromBottleLoader, + FromURILoader, + FromAPILoader, + FromTapLoader, + FromPathLoader, + FromNameLoader, + FromKegLoader, + FromCacheLoader, + NullLoader, + ].each do |loader_class| + if (loader = loader_class.try_new(ref, **options)) + $stderr.puts "#{$PROGRAM_NAME} (#{loader_class}): loading #{ref}" if debug? + return loader end end - - path = find_formula_in_tap(name, tap) - TapLoader.new(name, path, tap: tap) - end - - def self.loader_for(ref, from: nil, warn: true) - case ref - when HOMEBREW_BOTTLES_EXTNAME_REGEX - return BottleLoader.new(ref) - when URL_START_REGEX - return FromUrlLoader.new(ref, from: from) - when HOMEBREW_TAP_FORMULA_REGEX - return Formulary.tap_loader_for(ref, warn: warn) - end - - pathname_ref = Pathname.new(ref) - return FromPathLoader.new(ref) if File.extname(ref) == ".rb" && pathname_ref.expand_path.exist? - - unless Homebrew::EnvConfig.no_install_from_api? - return FormulaAPILoader.new(ref) if Homebrew::API::Formula.all_formulae.key?(ref) - return AliasAPILoader.new(ref) if Homebrew::API::Formula.all_aliases.key?(ref) - end - - formula_with_that_name = core_path(ref) - return FormulaLoader.new(ref, formula_with_that_name) if formula_with_that_name.file? - - possible_alias = if pathname_ref.absolute? - pathname_ref - else - core_alias_path(ref) - end - return AliasLoader.new(possible_alias) if possible_alias.symlink? - - case (possible_tap_formulae = tap_paths(ref)).count - when 1 - path = possible_tap_formulae.first.resolved_path - name = path.basename(".rb").to_s - return FormulaLoader.new(name, path) - when 2..Float::INFINITY - raise TapFormulaAmbiguityError.new(ref, possible_tap_formulae) - end - - if CoreTap.instance.formula_renames.key?(ref) - return Formulary.tap_loader_for("#{CoreTap.instance}/#{ref}", warn: warn) - end - - possible_taps = Tap.select { |tap| tap.formula_renames.key?(ref) } - - case possible_taps.count - when 1 - return Formulary.tap_loader_for("#{possible_taps.first}/#{ref}", warn: warn) - when 2..Float::INFINITY - possible_tap_newname_formulae = possible_taps.map { |tap| "#{tap}/#{tap.formula_renames[ref]}" } - raise TapFormulaWithOldnameAmbiguityError.new(ref, possible_tap_newname_formulae) - end - - if (keg_formula = HOMEBREW_PREFIX/"opt/#{ref}/.brew/#{ref}.rb").file? - return FormulaLoader.new(ref, keg_formula) - end - - if (cached_formula = HOMEBREW_CACHE_FORMULA/"#{ref}.rb").file? - return FormulaLoader.new(ref, cached_formula) - end - - NullLoader.new(ref) end def self.core_path(name) find_formula_in_tap(name.to_s.downcase, CoreTap.instance) end - def self.core_alias_path(name) - CoreTap.instance.alias_dir/name.to_s.downcase - end - - def self.tap_paths(name) - name = name.to_s.downcase - Tap.map do |tap| - formula_path = find_formula_in_tap(name, tap) - - alias_path = tap.alias_dir/name - next alias_path if !formula_path.exist? && alias_path.exist? - - formula_path - end.select(&:file?) - end - sig { params(name: String, tap: Tap).returns(Pathname) } def self.find_formula_in_tap(name, tap) filename = if name.end_with?(".rb") diff --git a/Library/Homebrew/tap_constants.rb b/Library/Homebrew/tap_constants.rb index 6f102bfcbf4bb..bb66528f9fd20 100644 --- a/Library/Homebrew/tap_constants.rb +++ b/Library/Homebrew/tap_constants.rb @@ -2,7 +2,9 @@ # frozen_string_literal: true # Match taps' formulae, e.g. `someuser/sometap/someformula` -HOMEBREW_TAP_FORMULA_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([\w+-.@]+)$}, Regexp) +HOMEBREW_TAP_FORMULA_REGEX = T.let(%r{^(?[\w-]+)/(?[\w-]+)/(?[\w+-.@]+)$}, Regexp) +# Match default formula taps' formulae, e.g. `homebrew/core/someformula` or `someformula` +HOMEBREW_DEFAULT_TAP_FORMULA_REGEX = T.let(%r{^(?:[Hh]omebrew/(?:homebrew-)?core/)?(?[a-z0-9\-_]+)$}, Regexp) # Match taps' casks, e.g. `someuser/sometap/somecask` HOMEBREW_TAP_CASK_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([a-z0-9\-_]+)$}, Regexp) # Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask` @@ -11,12 +13,14 @@ ) # Match taps' directory paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap` HOMEBREW_TAP_DIR_REGEX = T.let( - %r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?[\w-]+)/(?[\w-]+)}, Regexp + %r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?[\w-]+)/(?[\w-]+)}, + Regexp, ) # Match taps' formula paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap/someformula` HOMEBREW_TAP_PATH_REGEX = T.let(Regexp.new(HOMEBREW_TAP_DIR_REGEX.source + %r{(?:/.*)?$}.source).freeze, Regexp) # Match official taps' casks, e.g. `homebrew/cask/somecask or homebrew/cask-versions/somecask` -HOMEBREW_CASK_TAP_CASK_REGEX = - T.let(%r{^(?:([Cc]askroom)/(cask|versions)|([Hh]omebrew)/(?:homebrew-)?(cask|cask-[\w-]+))/([\w+-.]+)$}, - Regexp) +HOMEBREW_CASK_TAP_CASK_REGEX = T.let( + %r{^(?:([Cc]askroom)/(cask|versions)|([Hh]omebrew)/(?:homebrew-)?(cask|cask-[\w-]+))/([\w+-.]+)$}, + Regexp, +) HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = T.let(/^(home|linux)brew-/, Regexp) diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 414b6004dd738..de9eba1357796 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -28,7 +28,7 @@ let(:path) { Formulary.core_path(name) } let(:spec) { :stable } let(:alias_name) { "baz@1" } - let(:alias_path) { (CoreTap.instance.alias_dir/alias_name).to_s } + let(:alias_path) { CoreTap.instance.alias_dir/alias_name } let(:f) { klass.new(name, path, spec) } let(:f_alias) { klass.new(name, path, spec, alias_path: alias_path) } @@ -190,11 +190,11 @@ end alias_name = "bar" - alias_path = "#{CoreTap.instance.alias_dir}/#{alias_name}" + alias_path = CoreTap.instance.alias_dir/alias_name CoreTap.instance.alias_dir.mkpath FileUtils.ln_sf f.path, alias_path - f.build = Tab.new(source: { "path" => alias_path }) + f.build = Tab.new(source: { "path" => alias_path.to_s }) expect(f.installed_alias_path).to eq(alias_path) expect(f.installed_alias_name).to eq(alias_name) @@ -225,12 +225,12 @@ end alias_name = "bar" + alias_path = tap.alias_dir/alias_name full_alias_name = "#{tap.user}/#{tap.repo}/#{alias_name}" - alias_path = "#{tap.alias_dir}/#{alias_name}" tap.alias_dir.mkpath FileUtils.ln_sf f.path, alias_path - f.build = Tab.new(source: { "path" => alias_path }) + f.build = Tab.new(source: { "path" => alias_path.to_s }) expect(f.installed_alias_path).to eq(alias_path) expect(f.installed_alias_name).to eq(alias_name) @@ -451,7 +451,7 @@ FileUtils.ln_sf f.path, source_path expect(f.alias_path).to eq(alias_path) - expect(f.installed_alias_path).to eq(source_path.to_s) + expect(f.installed_alias_path).to eq(source_path) end end @@ -491,14 +491,14 @@ end specify "with alias path with a path" do - alias_path = "#{CoreTap.instance.alias_dir}/alias" - different_alias_path = "#{CoreTap.instance.alias_dir}/another_alias" + alias_path = CoreTap.instance.alias_dir/"alias" + different_alias_path = CoreTap.instance.alias_dir/"another_alias" formula_with_alias = formula "foo" do url "foo-1.0" end formula_with_alias.build = Tab.empty - formula_with_alias.build.source["path"] = alias_path + formula_with_alias.build.source["path"] = alias_path.to_s formula_without_alias = formula "bar" do url "bar-1.0" @@ -510,7 +510,7 @@ url "baz-1.0" end formula_with_different_alias.build = Tab.empty - formula_with_different_alias.build.source["path"] = different_alias_path + formula_with_different_alias.build.source["path"] = different_alias_path.to_s formulae = [ formula_with_alias, @@ -1239,8 +1239,8 @@ def pour_bottle? end let(:tab) { Tab.empty } - let(:alias_path) { "#{CoreTap.instance.alias_dir}/bar" } let(:alias_name) { "bar" } + let(:alias_path) { CoreTap.instance.alias_dir/alias_name } before do allow(described_class).to receive(:installed).and_return([f]) @@ -1261,7 +1261,7 @@ def pour_bottle? end specify "alias changes when not changed" do - tab.source["path"] = alias_path + tab.source["path"] = alias_path.to_s stub_formula_loader(f, alias_name) CoreTap.instance.alias_dir.mkpath @@ -1276,7 +1276,7 @@ def pour_bottle? end specify "alias changes when new alias target" do - tab.source["path"] = alias_path + tab.source["path"] = alias_path.to_s stub_formula_loader(new_formula, alias_name) CoreTap.instance.alias_dir.mkpath @@ -1291,7 +1291,7 @@ def pour_bottle? end specify "alias changes when old formulae installed" do - tab.source["path"] = alias_path + tab.source["path"] = alias_path.to_s stub_formula_loader(new_formula, alias_name) CoreTap.instance.alias_dir.mkpath @@ -1332,8 +1332,8 @@ def pour_bottle? end end - let(:alias_path) { "#{f.tap.alias_dir}/bar" } let(:alias_name) { "bar" } + let(:alias_path) { f.tap.alias_dir/alias_name } def setup_tab_for_prefix(prefix, options = {}) prefix.mkpath diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 411f17574f281..498295d1822f3 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -111,7 +111,7 @@ class Wrong#{described_class.class_s(formula_name)} < Formula it "raises an error" do expect do described_class.factory(formula_name) - end.to raise_error(FormulaClassUnavailableError) + end.to raise_error(TapFormulaClassUnavailableError) end end @@ -139,12 +139,13 @@ class Wrong#{described_class.class_s(formula_name)} < Formula context "when given an alias" do subject(:formula) { described_class.factory("foo") } - let(:alias_dir) { CoreTap.instance.alias_dir.tap(&:mkpath) } + let(:alias_dir) { CoreTap.instance.alias_dir } let(:alias_path) { alias_dir/"foo" } before do alias_dir.mkpath FileUtils.ln_s formula_path, alias_path + CoreTap.instance.clear_cache end it "returns a Formula" do @@ -152,7 +153,7 @@ class Wrong#{described_class.class_s(formula_name)} < Formula end it "calling #alias_path on the returned Formula returns the alias path" do - expect(formula.alias_path).to eq(alias_path.to_s) + expect(formula.alias_path).to eq(alias_path) end end @@ -231,16 +232,22 @@ class Wrong#{described_class.class_s(formula_name)} < Formula let(:tap) { Tap.new("homebrew", "foo") } let(:another_tap) { Tap.new("homebrew", "bar") } let(:formula_path) { tap.path/"Formula/#{formula_name}.rb" } + let(:alias_name) { "bar" } + let(:alias_dir) { tap.alias_dir } + let(:alias_path) { alias_dir/alias_name } + + before do + alias_dir.mkpath + FileUtils.ln_s formula_path, alias_path + tap.clear_cache + end it "returns a Formula when given a name" do expect(described_class.factory(formula_name)).to be_a(Formula) end it "returns a Formula from an Alias path" do - alias_dir = tap.path/"Aliases" - alias_dir.mkpath - FileUtils.ln_s formula_path, alias_dir/"bar" - expect(described_class.factory("bar")).to be_a(Formula) + expect(described_class.factory(alias_name)).to be_a(Formula) end it "returns a Formula from a fully qualified Alias path" do @@ -378,7 +385,7 @@ def formula_json_contents(extra_items = {}) end before do - allow(described_class).to receive(:loader_for).and_return(described_class::FormulaAPILoader.new(formula_name)) + allow(described_class).to receive(:loader_for).and_return(described_class::FromAPILoader.new(formula_name)) # don't try to load/fetch gcc/glibc allow(DevelopmentTools).to receive_messages(needs_libc_formula?: false, needs_compiler_formula?: false) From d70f719411a5e40d68e7847e98963fb32af3f554 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 20:49:09 +0100 Subject: [PATCH 02/19] Fix code style. --- Library/Homebrew/formulary.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 1f1f09ce27f23..49822652b9d33 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -859,7 +859,9 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) return unless (match = ref.match(HOMEBREW_DEFAULT_TAP_FORMULA_REGEX)) name = alias_name = T.cast(match[:name], String) - unless Homebrew::API::Formula.all_formulae.key?(name) || Homebrew::API::Formula.all_aliases.key?(name) || Homebrew::API::Formula.all_renames.key?(name) + if !Homebrew::API::Formula.all_formulae.key?(name) && + !Homebrew::API::Formula.all_aliases.key?(name) && + !Homebrew::API::Formula.all_renames.key?(name) return end From ecee4aed35db07330c9a64986d944442932303e9 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 21:13:49 +0100 Subject: [PATCH 03/19] Revert diagnostic change. --- Library/Homebrew/diagnostic.rb | 9 ++++++++- Library/Homebrew/formulary.rb | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index c14bbaeb744f8..b039069da96d0 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -835,7 +835,14 @@ def check_deleted_formula kegs = Keg.all deleted_formulae = kegs.map do |keg| - next if Formulary::FromNameLoader.try_new(keg.name, warn: false).nil? + next if Formulary.tap_paths(keg.name).any? + + unless EnvConfig.no_install_from_api? + # Formulae installed from the API should not count as deleted formulae + # but may not have a tap listed in their tab + tap = Tab.for_keg(keg).tap + next if (tap.blank? || tap.core_tap?) && Homebrew::API::Formula.all_formulae.key?(keg.name) + end keg.name end.compact.uniq diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 49822652b9d33..015e94d22f934 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1172,6 +1172,18 @@ def self.core_path(name) find_formula_in_tap(name.to_s.downcase, CoreTap.instance) end + def self.tap_paths(name) + name = name.to_s.downcase + Tap.map do |tap| + formula_path = find_formula_in_tap(name, tap) + + alias_path = tap.alias_dir/name + next alias_path if !formula_path.exist? && alias_path.exist? + + formula_path + end.select(&:file?) + end + sig { params(name: String, tap: Tap).returns(Pathname) } def self.find_formula_in_tap(name, tap) filename = if name.end_with?(".rb") From 0f4d912de595ce5df50537af50c3b960aa4394af Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 21:29:26 +0100 Subject: [PATCH 04/19] Remove `Formulary::tap_paths` again. --- Library/Homebrew/diagnostic.rb | 21 ++++++++++++++------- Library/Homebrew/formulary.rb | 12 ------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index b039069da96d0..b869809a0612e 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -835,16 +835,23 @@ def check_deleted_formula kegs = Keg.all deleted_formulae = kegs.map do |keg| - next if Formulary.tap_paths(keg.name).any? + tap = Tab.for_keg(keg).tap - unless EnvConfig.no_install_from_api? - # Formulae installed from the API should not count as deleted formulae - # but may not have a tap listed in their tab - tap = Tab.for_keg(keg).tap - next if (tap.blank? || tap.core_tap?) && Homebrew::API::Formula.all_formulae.key?(keg.name) + loadable = [ + Formulary::FromAPILoader, + Formulary::FromNameLoader, + ].any? do |loader_class| + if (loader = loader_class.try_new(keg.name, warn: false)) + # If we know the tap, ignore all other taps. + next false if tap && loader.tap != tap + + next true + end + + false end - keg.name + keg.name unless loadable end.compact.uniq return if deleted_formulae.blank? diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 015e94d22f934..49822652b9d33 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1172,18 +1172,6 @@ def self.core_path(name) find_formula_in_tap(name.to_s.downcase, CoreTap.instance) end - def self.tap_paths(name) - name = name.to_s.downcase - Tap.map do |tap| - formula_path = find_formula_in_tap(name, tap) - - alias_path = tap.alias_dir/name - next alias_path if !formula_path.exist? && alias_path.exist? - - formula_path - end.select(&:file?) - end - sig { params(name: String, tap: Tap).returns(Pathname) } def self.find_formula_in_tap(name, tap) filename = if name.end_with?(".rb") From 0626f62077d716bd70658cf6d06b9a59ea6c07ea Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 21:42:22 +0100 Subject: [PATCH 05/19] Remove redundant debug output. --- Library/Homebrew/formulary.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 49822652b9d33..49c4fc2cf37fb 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -521,7 +521,6 @@ def klass(flags:, ignore_errors:) private def load_file(flags:, ignore_errors:) - $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? raise FormulaUnavailableError, name unless path.file? Formulary.load_formula_from_path(name, path, flags: flags, ignore_errors: ignore_errors) @@ -840,7 +839,6 @@ def initialize(name, path, contents) end def klass(flags:, ignore_errors:) - $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug? namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}" Formulary.load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors) end @@ -896,8 +894,6 @@ def klass(flags:, ignore_errors:) private def load_from_api(flags:) - $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{name} from API" if debug? - Formulary.load_formula_from_api(name, flags: flags) end end From 61a283ee45382c0c0e16b04ffb6f2356beac3303 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 8 Feb 2024 21:52:32 +0100 Subject: [PATCH 06/19] Fix loading versioned casks/formulae from API. --- Library/Homebrew/tap_constants.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/tap_constants.rb b/Library/Homebrew/tap_constants.rb index bb66528f9fd20..39aac9218f245 100644 --- a/Library/Homebrew/tap_constants.rb +++ b/Library/Homebrew/tap_constants.rb @@ -4,12 +4,16 @@ # Match taps' formulae, e.g. `someuser/sometap/someformula` HOMEBREW_TAP_FORMULA_REGEX = T.let(%r{^(?[\w-]+)/(?[\w-]+)/(?[\w+-.@]+)$}, Regexp) # Match default formula taps' formulae, e.g. `homebrew/core/someformula` or `someformula` -HOMEBREW_DEFAULT_TAP_FORMULA_REGEX = T.let(%r{^(?:[Hh]omebrew/(?:homebrew-)?core/)?(?[a-z0-9\-_]+)$}, Regexp) +HOMEBREW_DEFAULT_TAP_FORMULA_REGEX = T.let( + %r{^(?:[Hh]omebrew/(?:homebrew-)?core/)?(?[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)$}, + Regexp, +) # Match taps' casks, e.g. `someuser/sometap/somecask` HOMEBREW_TAP_CASK_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([a-z0-9\-_]+)$}, Regexp) # Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask` HOMEBREW_DEFAULT_TAP_CASK_REGEX = T.let( - %r{^(?:[Hh]omebrew/(?:homebrew-)?cask/)?(?[a-z0-9\-_]+)$}, Regexp + %r{^(?:[Hh]omebrew/(?:homebrew-)?cask/)?(?[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)$}, + Regexp, ) # Match taps' directory paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap` HOMEBREW_TAP_DIR_REGEX = T.let( From ba29328b597131923809d7429b42a082b4e079f3 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Fri, 9 Feb 2024 17:55:25 +0100 Subject: [PATCH 07/19] Avoid `T.cast`. --- Library/Homebrew/formulary.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 49c4fc2cf37fb..1cd98789319d2 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -694,9 +694,9 @@ class FromTapLoader < FormulaLoader } def self.try_new(ref, from: T.unsafe(nil), warn: false) ref = ref.to_s - return unless (match = ref.match(HOMEBREW_TAP_FORMULA_REGEX)) + return unless (name = ref[HOMEBREW_TAP_FORMULA_REGEX, :name]) - alias_name = T.cast(match[:name], String) + alias_name = name name, tap, type = Formulary.tap_formula_name_type(ref, warn: warn) path = Formulary.find_formula_in_tap(name, tap) @@ -853,16 +853,15 @@ class FromAPILoader < FormulaLoader def self.try_new(ref, from: T.unsafe(nil), warn: false) return if Homebrew::EnvConfig.no_install_from_api? return unless ref.is_a?(String) - - return unless (match = ref.match(HOMEBREW_DEFAULT_TAP_FORMULA_REGEX)) - - name = alias_name = T.cast(match[:name], String) + return unless (name = ref[HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, :name]) if !Homebrew::API::Formula.all_formulae.key?(name) && !Homebrew::API::Formula.all_aliases.key?(name) && !Homebrew::API::Formula.all_renames.key?(name) return end + alias_name = name + ref = "#{CoreTap.instance}/#{name}" name, tap, type = Formulary.tap_formula_name_type(ref, warn: warn) From 22bb15748b3c182a7862febe04f2830177a5ecda Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 12 Feb 2024 07:29:10 +0100 Subject: [PATCH 08/19] Simplify conditional. --- Library/Homebrew/formulary.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 1cd98789319d2..264acde229dc9 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1104,11 +1104,7 @@ def self.tap_formula_name_type(tapped_name, warn:) type = nil # FIXME: Remove the need to do this here. - alias_table_key = if tap.core_tap? - name - else - "#{tap}/#{name}" - end + alias_table_key = tap.core_tap? ? name : "#{tap}/#{name}" if (possible_alias = tap.alias_table[alias_table_key].presence) # FIXME: Remove the need to split the name and instead make From c5a877d7dbd762b03ca56badf0dda3181b32e918 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 12 Feb 2024 07:32:18 +0100 Subject: [PATCH 09/19] Simplify test. --- Library/Homebrew/test/formulary_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 498295d1822f3..86988f0533dc5 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -251,10 +251,7 @@ class Wrong#{described_class.class_s(formula_name)} < Formula end it "returns a Formula from a fully qualified Alias path" do - alias_dir = tap.path/"Aliases" - alias_dir.mkpath - FileUtils.ln_s formula_path, alias_dir/"bar" - expect(described_class.factory("#{tap}/bar")).to be_a(Formula) + expect(described_class.factory("#{tap.name}/#{alias_name}")).to be_a(Formula) end it "raises an error when the Formula cannot be found" do From 562d5295f4eea72554ac7608174fabfdd7e4cc58 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 12 Feb 2024 08:08:36 +0100 Subject: [PATCH 10/19] Remove unnecessary `if`. --- Library/Homebrew/formulary.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 264acde229dc9..8e1815506bdcf 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -500,7 +500,7 @@ class FormulaLoader def initialize(name, path, alias_path: T.unsafe(nil), tap: T.unsafe(nil)) @name = name @path = path - @alias_path = alias_path if alias_path + @alias_path = alias_path @tap = tap end From eb48d6be95b1e4e185d9f1466eef5fe66fd50040 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 12 Feb 2024 21:52:51 +0100 Subject: [PATCH 11/19] Always prefer the default tap. --- Library/Homebrew/formulary.rb | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 8e1815506bdcf..b2908fc0cbf2e 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -758,19 +758,12 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) when 1 loaders.first when 2..Float::INFINITY - default_tap_loaders, other_loaders = *loaders.partition { _1.tap.core_tap? } - default_tap_loader = default_tap_loaders.first if default_tap_loaders.count - - # Put default tap last so that the error message always recommends - # using the fully-qualified name for non-default taps. - taps = other_loaders.map(&:tap) + default_tap_loaders.map(&:tap) - - error = TapFormulaAmbiguityError.new(name, taps) - - raise error unless default_tap_loader + # Always prefer the default tap, i.e. behave the same as if loading from the API. + if (default_tap_loader = loaders.find { _1.tap.core_tap? }) + return default_tap_loader + end - opoo error if warn - default_tap_loader + raise TapFormulaAmbiguityError.new(name, loaders.map(&:tap)) end end end From 726aeca2452bf5fabb55643206f697b6a6f64b4f Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Mon, 12 Feb 2024 22:15:05 +0100 Subject: [PATCH 12/19] Fix tap constants. --- Library/Homebrew/tap_constants.rb | 40 ++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Library/Homebrew/tap_constants.rb b/Library/Homebrew/tap_constants.rb index 39aac9218f245..e3af9328ae331 100644 --- a/Library/Homebrew/tap_constants.rb +++ b/Library/Homebrew/tap_constants.rb @@ -1,30 +1,42 @@ # typed: strict # frozen_string_literal: true -# Match taps' formulae, e.g. `someuser/sometap/someformula` -HOMEBREW_TAP_FORMULA_REGEX = T.let(%r{^(?[\w-]+)/(?[\w-]+)/(?[\w+-.@]+)$}, Regexp) -# Match default formula taps' formulae, e.g. `homebrew/core/someformula` or `someformula` +# Match a formula name. +HOMEBREW_TAP_FORMULA_NAME_REGEX = T.let(/(?[\w+\-.@]+)/, Regexp) +# Match taps' formulae, e.g. `someuser/sometap/someformula`. +HOMEBREW_TAP_FORMULA_REGEX = T.let( + %r{\A(?[\w-]+)/(?[\w-]+)/#{HOMEBREW_TAP_FORMULA_NAME_REGEX.source}\Z}, + Regexp, +) +# Match default formula taps' formulae, e.g. `homebrew/core/someformula` or `someformula`. HOMEBREW_DEFAULT_TAP_FORMULA_REGEX = T.let( - %r{^(?:[Hh]omebrew/(?:homebrew-)?core/)?(?[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)$}, + %r{\A(?:[Hh]omebrew/(?:homebrew-)?core/)?(?#{HOMEBREW_TAP_FORMULA_NAME_REGEX.source})\Z}, + Regexp, +) + +# Match a cask token. +HOMEBREW_TAP_CASK_TOKEN_REGEX = T.let(/(?[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)/, Regexp) +# Match taps' casks, e.g. `someuser/sometap/somecask`. +HOMEBREW_TAP_CASK_REGEX = T.let( + %r{\A(?[\w-]+)/(?[\w-]+)/#{HOMEBREW_TAP_CASK_TOKEN_REGEX.source}\Z}, Regexp, ) -# Match taps' casks, e.g. `someuser/sometap/somecask` -HOMEBREW_TAP_CASK_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([a-z0-9\-_]+)$}, Regexp) -# Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask` +# Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask`. HOMEBREW_DEFAULT_TAP_CASK_REGEX = T.let( - %r{^(?:[Hh]omebrew/(?:homebrew-)?cask/)?(?[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)$}, + %r{\A(?:[Hh]omebrew/(?:homebrew-)?cask/)?#{HOMEBREW_TAP_CASK_TOKEN_REGEX.source}\Z}, Regexp, ) -# Match taps' directory paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap` + +# Match taps' directory paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap`. HOMEBREW_TAP_DIR_REGEX = T.let( %r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?[\w-]+)/(?[\w-]+)}, Regexp, ) -# Match taps' formula paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap/someformula` -HOMEBREW_TAP_PATH_REGEX = T.let(Regexp.new(HOMEBREW_TAP_DIR_REGEX.source + %r{(?:/.*)?$}.source).freeze, Regexp) -# Match official taps' casks, e.g. `homebrew/cask/somecask or homebrew/cask-versions/somecask` +# Match taps' formula paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap/someformula`. +HOMEBREW_TAP_PATH_REGEX = T.let(Regexp.new(HOMEBREW_TAP_DIR_REGEX.source + %r{(?:/.*)?\Z}.source).freeze, Regexp) +# Match official taps' casks, e.g. `homebrew/cask/somecask or homebrew/cask-versions/somecask`. HOMEBREW_CASK_TAP_CASK_REGEX = T.let( - %r{^(?:([Cc]askroom)/(cask|versions)|([Hh]omebrew)/(?:homebrew-)?(cask|cask-[\w-]+))/([\w+-.]+)$}, + %r{\A(?:([Cc]askroom)/(cask|versions)|([Hh]omebrew)/(?:homebrew-)?(cask|cask-[\w-]+))/([\w+-.]+)\Z}, Regexp, ) -HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = T.let(/^(home|linux)brew-/, Regexp) +HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = T.let(/\A(home|linux)brew-/, Regexp) From 9056613181425844b76d99b548f1a5836c854aac Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 20:25:07 +0100 Subject: [PATCH 13/19] Add separate loader for default tap. --- Library/Homebrew/formulary.rb | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index b2908fc0cbf2e..87a399b4e6e56 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -738,8 +738,22 @@ def load_file(flags:, ignore_errors:) end end + class FromDefaultNameLoader < FromTapLoader + sig { + params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) + .returns(T.nilable(FromTapLoader)) + } + def self.try_new(ref, from: T.unsafe(nil), warn: false) + return unless ref.is_a?(String) + return unless (name = ref[HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, :name]) + return unless (tap = CoreTap.instance).installed? + + super("#{tap}/#{name}") + end + end + # Loads a formula from a name, as long as it exists only in a single tap. - module FromNameLoader + class FromNameLoader < FromTapLoader sig { params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) .returns(T.nilable(FromTapLoader)) @@ -750,7 +764,7 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) name = ref - loaders = Tap.map { |tap| FromTapLoader.try_new("#{tap}/#{name}") } + loaders = Tap.map { |tap| super("#{tap}/#{name}") } .compact .select { _1.path.exist? } @@ -758,11 +772,6 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) when 1 loaders.first when 2..Float::INFINITY - # Always prefer the default tap, i.e. behave the same as if loading from the API. - if (default_tap_loader = loaders.find { _1.tap.core_tap? }) - return default_tap_loader - end - raise TapFormulaAmbiguityError.new(name, loaders.map(&:tap)) end end @@ -1140,6 +1149,7 @@ def self.loader_for(ref, from: T.unsafe(nil), warn: true) FromAPILoader, FromTapLoader, FromPathLoader, + FromDefaultNameLoader, FromNameLoader, FromKegLoader, FromCacheLoader, From ce60048192d7f2a5b04cd44d4ca28325a5c1e390 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 20:27:53 +0100 Subject: [PATCH 14/19] Handle `TapFormulaAmbiguityError` in diagnostics. --- Library/Homebrew/diagnostic.rb | 8 +++++++- Library/Homebrew/exceptions.rb | 7 ++++--- Library/Homebrew/formulary.rb | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index b869809a0612e..87d58992598f5 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -841,7 +841,13 @@ def check_deleted_formula Formulary::FromAPILoader, Formulary::FromNameLoader, ].any? do |loader_class| - if (loader = loader_class.try_new(keg.name, warn: false)) + loader = begin + loader_class.try_new(keg.name, warn: false) + rescue TapFormulaAmbiguityError => e + e.loaders.first + end + + if loader # If we know the tap, ignore all other taps. next false if tap && loader.tap != tap diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index e402a3b8280a6..3fdda640b25c1 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -259,11 +259,12 @@ def initialize(tap, name, error) # Raised when a formula with the same name is found in multiple taps. class TapFormulaAmbiguityError < RuntimeError - attr_reader :name, :taps + attr_reader :name, :taps, :loaders - def initialize(name, taps) + def initialize(name, loaders) @name = name - @taps = taps + @loaders = loaders + @taps = loaders.map(&:tap) formulae = taps.map { |tap| "#{tap}/#{name}" } formula_list = formulae.map { |f| "\n * #{f}" }.join diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 87a399b4e6e56..4e7f151c8b3a8 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -772,7 +772,7 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) when 1 loaders.first when 2..Float::INFINITY - raise TapFormulaAmbiguityError.new(name, loaders.map(&:tap)) + raise TapFormulaAmbiguityError.new(name, loaders) end end end From f4ac2996a7ea46c5f581ed27725843d13d98a932 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 22:11:51 +0100 Subject: [PATCH 15/19] Fix type signature. --- Library/Homebrew/formulary.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 4e7f151c8b3a8..1d402ba0c3fee 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -741,7 +741,7 @@ def load_file(flags:, ignore_errors:) class FromDefaultNameLoader < FromTapLoader sig { params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) - .returns(T.nilable(FromTapLoader)) + .returns(T.nilable(T.attached_class)) } def self.try_new(ref, from: T.unsafe(nil), warn: false) return unless ref.is_a?(String) @@ -756,7 +756,7 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) class FromNameLoader < FromTapLoader sig { params(ref: T.any(String, Pathname, URI::Generic), from: Symbol, warn: T::Boolean) - .returns(T.nilable(FromTapLoader)) + .returns(T.nilable(T.attached_class)) } def self.try_new(ref, from: T.unsafe(nil), warn: false) return unless ref.is_a?(String) From 177e9163c20c8d27f68fad95893eaf624780471f Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 22:47:29 +0100 Subject: [PATCH 16/19] Fix code style. --- Library/Homebrew/formulary.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 1d402ba0c3fee..61019d9cfcca3 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -624,7 +624,6 @@ def initialize(path, alias_path: T.unsafe(nil)) options = { alias_path: (alias_path if alias_dir == tap&.alias_dir), tap: tap, - }.compact super(name, path, **options) From b717f5d9547319c2b2754d52a1fbc31f024bf2b9 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 22:56:25 +0100 Subject: [PATCH 17/19] Remove unused `TapFormulaWithOldnameAmbiguityError`. --- Library/Homebrew/cleanup.rb | 4 ++-- Library/Homebrew/diagnostic.rb | 5 ++--- Library/Homebrew/exceptions.rb | 21 --------------------- Library/Homebrew/formula.rb | 2 +- Library/Homebrew/formula_auditor.rb | 8 ++------ Library/Homebrew/formulary.rb | 2 +- 6 files changed, 8 insertions(+), 34 deletions(-) diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index 19348c6a4cd60..66937919c49b5 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -130,7 +130,7 @@ def stale_formula?(pathname, scrub) formula = begin Formulary.from_rack(HOMEBREW_CELLAR/formula_name) - rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError nil end @@ -300,7 +300,7 @@ def clean!(quiet: false, periodic: false) args.each do |arg| formula = begin Formulary.resolve(arg) - rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError nil end diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 87d58992598f5..50ac367efa992 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -733,8 +733,7 @@ def check_for_unreadable_installed_formula rescue FormulaUnreadableError, FormulaClassUnavailableError, TapFormulaUnreadableError, TapFormulaClassUnavailableError => e formula_unavailable_exceptions << e - rescue FormulaUnavailableError, - TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError nil end return if formula_unavailable_exceptions.empty? @@ -752,7 +751,7 @@ def check_for_unlinked_but_not_keg_only else begin Formulary.from_rack(rack).keg_only? - rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError false end end diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 3fdda640b25c1..1094c7e517bf6 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -277,27 +277,6 @@ def initialize(name, loaders) end end -# Raised when a formula's old name in a specific tap is found in multiple taps. -class TapFormulaWithOldnameAmbiguityError < RuntimeError - attr_reader :name, :possible_tap_newname_formulae, :taps - - def initialize(name, possible_tap_newname_formulae) - @name = name - @possible_tap_newname_formulae = possible_tap_newname_formulae - - @taps = possible_tap_newname_formulae.map do |newname| - newname =~ HOMEBREW_TAP_FORMULA_REGEX - "#{Regexp.last_match(1)}/#{Regexp.last_match(2)}" - end - - super <<~EOS - Formulae with '#{name}' old name found in multiple taps: #{taps.map { |t| "\n * #{t}" }.join} - - Please use the fully-qualified name (e.g. #{taps.first}/#{name}) to refer to the formula or use its new name. - EOS - end -end - # Raised when a tap is unavailable. class TapUnavailableError < RuntimeError attr_reader :name diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 3b65c6248be60..9dd88c03650b0 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1318,7 +1318,7 @@ def link_overwrite?(path) f = Formulary.factory(keg.name) rescue FormulaUnavailableError # formula for this keg is deleted, so defer to allowlist - rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue TapFormulaAmbiguityError return false # this keg belongs to another formula else # this keg belongs to another unrelated formula diff --git a/Library/Homebrew/formula_auditor.rb b/Library/Homebrew/formula_auditor.rb index 2285785cc177e..93fdc1f0f386b 100644 --- a/Library/Homebrew/formula_auditor.rb +++ b/Library/Homebrew/formula_auditor.rb @@ -64,8 +64,7 @@ def audit_file unversioned_formula = begin Formulary.factory(full_name).path - rescue FormulaUnavailableError, TapFormulaAmbiguityError, - TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError Pathname.new formula.path.to_s.gsub(/@.*\.rb$/, ".rb") end unless unversioned_formula.exist? @@ -285,9 +284,6 @@ def audit_deps rescue TapFormulaAmbiguityError problem "Ambiguous dependency '#{dep.name.inspect}'." next - rescue TapFormulaWithOldnameAmbiguityError - problem "Ambiguous oldname dependency '#{dep.name.inspect}'." - next end if dep_f.oldnames.include?(dep.name.split("/").last) @@ -461,7 +457,7 @@ def audit_conflicts next rescue FormulaUnavailableError problem "Can't find conflicting formula #{conflict.name.inspect}." - rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue TapFormulaAmbiguityError problem "Ambiguous conflicting formula #{conflict.name.inspect}." end end diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 61019d9cfcca3..622f9ffb4e11f 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -990,7 +990,7 @@ def self.from_rack( # Return whether given rack is keg-only. def self.keg_only?(rack) Formulary.from_rack(rack).keg_only? - rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError + rescue FormulaUnavailableError, TapFormulaAmbiguityError false end From fe1f6c95f5e3a00dddeb546dd0eaa668c94ed0cf Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Wed, 14 Feb 2024 23:08:48 +0100 Subject: [PATCH 18/19] Fix `FromDefaultNameLoader`. --- Library/Homebrew/formulary.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 622f9ffb4e11f..ea73296aa8fa9 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -747,7 +747,9 @@ def self.try_new(ref, from: T.unsafe(nil), warn: false) return unless (name = ref[HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, :name]) return unless (tap = CoreTap.instance).installed? - super("#{tap}/#{name}") + return unless (loader = super("#{tap}/#{name}", warn: warn)) + + loader if loader.path.exist? end end From efa6130a6621b5312d82f23f116bfe3fb633246c Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Thu, 15 Feb 2024 01:37:43 +0100 Subject: [PATCH 19/19] Use `FromDefaultNameLoader` for diagnostics. --- Library/Homebrew/diagnostic.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 50ac367efa992..7debb69b38aba 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -838,6 +838,7 @@ def check_deleted_formula loadable = [ Formulary::FromAPILoader, + Formulary::FromDefaultNameLoader, Formulary::FromNameLoader, ].any? do |loader_class| loader = begin