Reapply "Refactor Formulary::loader_for."

This reverts commit 24683525cb5abf3cc79a9e0e268fa6efd0af558b.
This commit is contained in:
Markus Reiter 2024-02-16 21:27:02 +01:00
parent deb048874a
commit e0743a1436
No known key found for this signature in database
GPG Key ID: 245293B51702655B
10 changed files with 375 additions and 228 deletions

View File

@ -59,7 +59,7 @@ module Homebrew
end end
private :download_and_cache_data! private :download_and_cache_data!
sig { returns(Hash) } sig { returns(T::Hash[String, Hash]) }
def all_formulae def all_formulae
unless cache.key?("formulae") unless cache.key?("formulae")
json_updated = download_and_cache_data! json_updated = download_and_cache_data!
@ -69,7 +69,7 @@ module Homebrew
cache["formulae"] cache["formulae"]
end end
sig { returns(Hash) } sig { returns(T::Hash[String, String]) }
def all_aliases def all_aliases
unless cache.key?("aliases") unless cache.key?("aliases")
json_updated = download_and_cache_data! json_updated = download_and_cache_data!

View File

@ -130,7 +130,7 @@ module Homebrew
formula = begin formula = begin
Formulary.from_rack(HOMEBREW_CELLAR/formula_name) Formulary.from_rack(HOMEBREW_CELLAR/formula_name)
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue FormulaUnavailableError, TapFormulaAmbiguityError
nil nil
end end
@ -300,7 +300,7 @@ module Homebrew
args.each do |arg| args.each do |arg|
formula = begin formula = begin
Formulary.resolve(arg) Formulary.resolve(arg)
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue FormulaUnavailableError, TapFormulaAmbiguityError
nil nil
end end

View File

@ -733,8 +733,7 @@ module Homebrew
rescue FormulaUnreadableError, FormulaClassUnavailableError, rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError => e TapFormulaUnreadableError, TapFormulaClassUnavailableError => e
formula_unavailable_exceptions << e formula_unavailable_exceptions << e
rescue FormulaUnavailableError, rescue FormulaUnavailableError, TapFormulaAmbiguityError
TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
nil nil
end end
return if formula_unavailable_exceptions.empty? return if formula_unavailable_exceptions.empty?
@ -752,7 +751,7 @@ module Homebrew
else else
begin begin
Formulary.from_rack(rack).keg_only? Formulary.from_rack(rack).keg_only?
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue FormulaUnavailableError, TapFormulaAmbiguityError
false false
end end
end end
@ -835,16 +834,30 @@ module Homebrew
kegs = Keg.all kegs = Keg.all
deleted_formulae = kegs.map do |keg| 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? loadable = [
# Formulae installed from the API should not count as deleted formulae Formulary::FromAPILoader,
# but may not have a tap listed in their tab Formulary::FromDefaultNameLoader,
tap = Tab.for_keg(keg).tap Formulary::FromNameLoader,
next if (tap.blank? || tap.core_tap?) && Homebrew::API::Formula.all_formulae.key?(keg.name) ].any? do |loader_class|
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
next true
end
false
end end
keg.name keg.name unless loadable
end.compact.uniq end.compact.uniq
return if deleted_formulae.blank? return if deleted_formulae.blank?

View File

@ -259,40 +259,20 @@ end
# Raised when a formula with the same name is found in multiple taps. # Raised when a formula with the same name is found in multiple taps.
class TapFormulaAmbiguityError < RuntimeError class TapFormulaAmbiguityError < RuntimeError
attr_reader :name, :paths, :formulae attr_reader :name, :taps, :loaders
def initialize(name, paths) def initialize(name, loaders)
@name = name @name = name
@paths = paths @loaders = loaders
@formulae = paths.map do |path| @taps = loaders.map(&:tap)
"#{Tap.from_path(path).name}/#{path.basename(".rb")}"
end formulae = taps.map { |tap| "#{tap}/#{name}" }
formula_list = formulae.map { |f| "\n * #{f}" }.join
super <<~EOS 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
# 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 EOS
end end
end end

View File

@ -77,7 +77,7 @@ class Formula
# The path to the alias that was used to identify this {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` # 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 attr_reader :alias_path
# The name of the alias that was used to identify this {Formula}. # The name of the alias that was used to identify this {Formula}.
@ -199,7 +199,7 @@ class Formula
# @private # @private
sig { 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 tap: T.nilable(Tap), force_bottle: T::Boolean).void
} }
def initialize(name, path, spec, alias_path: nil, tap: nil, force_bottle: false) def initialize(name, path, spec, alias_path: nil, tap: nil, force_bottle: false)
@ -326,18 +326,22 @@ class Formula
# The alias path that was used to install this formula, if it exists. # 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, # Can differ from {#alias_path}, which is the alias used to find the formula,
# and is specified to this instance. # and is specified to this instance.
sig { returns(T.nilable(Pathname)) }
def installed_alias_path def installed_alias_path
build_tab = build build_tab = build
path = build_tab.source["path"] if build_tab.is_a?(Tab) path = build_tab.source["path"] if build_tab.is_a?(Tab)
return unless path&.match?(%r{#{HOMEBREW_TAP_DIR_REGEX}/Aliases}o) return unless path&.match?(%r{#{HOMEBREW_TAP_DIR_REGEX}/Aliases}o)
return unless File.symlink?(path)
path = Pathname(path)
return unless path.symlink?
path path
end end
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def installed_alias_name def installed_alias_name
File.basename(installed_alias_path) if installed_alias_path installed_alias_path&.basename&.to_s
end end
def full_installed_alias_name def full_installed_alias_name
@ -346,14 +350,13 @@ class Formula
# The path that was specified to find this formula. # The path that was specified to find this formula.
def specified_path def specified_path
alias_pathname = Pathname(T.must(alias_path)) if alias_path.present? return alias_path if alias_path&.exist?
return alias_pathname if alias_pathname&.exist?
return @unresolved_path if @unresolved_path.exist? return @unresolved_path if @unresolved_path.exist?
return local_bottle_path if local_bottle_path.presence&.exist? return local_bottle_path if local_bottle_path.presence&.exist?
alias_pathname || @unresolved_path alias_path || @unresolved_path
end end
# The name specified to find this formula. # The name specified to find this formula.
@ -1315,7 +1318,7 @@ class Formula
f = Formulary.factory(keg.name) f = Formulary.factory(keg.name)
rescue FormulaUnavailableError rescue FormulaUnavailableError
# formula for this keg is deleted, so defer to allowlist # formula for this keg is deleted, so defer to allowlist
rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue TapFormulaAmbiguityError
return false # this keg belongs to another formula return false # this keg belongs to another formula
else else
# this keg belongs to another unrelated formula # this keg belongs to another unrelated formula
@ -2362,7 +2365,7 @@ class Formula
# Take from API, merging in local install status. # Take from API, merging in local install status.
if loaded_from_api? && !Homebrew::EnvConfig.no_install_from_api? 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( return json_formula.merge(
hash.slice("name", "installed", "linked_keg", "pinned", "outdated"), hash.slice("name", "installed", "linked_keg", "pinned", "outdated"),
) )

View File

@ -64,8 +64,7 @@ module Homebrew
unversioned_formula = begin unversioned_formula = begin
Formulary.factory(full_name).path Formulary.factory(full_name).path
rescue FormulaUnavailableError, TapFormulaAmbiguityError, rescue FormulaUnavailableError, TapFormulaAmbiguityError
TapFormulaWithOldnameAmbiguityError
Pathname.new formula.path.to_s.gsub(/@.*\.rb$/, ".rb") Pathname.new formula.path.to_s.gsub(/@.*\.rb$/, ".rb")
end end
unless unversioned_formula.exist? unless unversioned_formula.exist?
@ -285,9 +284,6 @@ module Homebrew
rescue TapFormulaAmbiguityError rescue TapFormulaAmbiguityError
problem "Ambiguous dependency '#{dep.name}'." problem "Ambiguous dependency '#{dep.name}'."
next next
rescue TapFormulaWithOldnameAmbiguityError
problem "Ambiguous oldname dependency '#{dep.name.inspect}'."
next
end end
if dep_f.oldnames.include?(dep.name.split("/").last) if dep_f.oldnames.include?(dep.name.split("/").last)
@ -461,7 +457,7 @@ module Homebrew
next next
rescue FormulaUnavailableError rescue FormulaUnavailableError
problem "Can't find conflicting formula #{conflict.name.inspect}." problem "Can't find conflicting formula #{conflict.name.inspect}."
rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue TapFormulaAmbiguityError
problem "Ambiguous conflicting formula #{conflict.name.inspect}." problem "Ambiguous conflicting formula #{conflict.name.inspect}."
end end
end end

View File

@ -16,6 +16,7 @@ require "extend/hash/keys"
# #
# @api private # @api private
module Formulary module Formulary
extend Context
extend Cachable extend Cachable
URL_START_REGEX = %r{(https?|ftp|file)://} URL_START_REGEX = %r{(https?|ftp|file)://}
@ -480,17 +481,26 @@ module Formulary
include Context include Context
# The formula's name # The formula's name
sig { returns(String) }
attr_reader :name attr_reader :name
# The formula's ruby file's path or filename # The formula's ruby file's path or filename
sig { returns(Pathname) }
attr_reader :path attr_reader :path
# The name used to install the formula # The name used to install the formula
sig { returns(T.nilable(Pathname)) }
attr_reader :alias_path attr_reader :alias_path
# The formula's tap (nil if it should be implicitly determined) # The formula's tap (nil if it should be implicitly determined)
sig { returns(T.nilable(Tap)) }
attr_reader :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 @name = name
@path = path @path = path
@alias_path = alias_path
@tap = tap @tap = tap
end end
@ -511,7 +521,6 @@ module Formulary
private private
def load_file(flags:, ignore_errors:) def load_file(flags:, ignore_errors:)
$stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug?
raise FormulaUnavailableError, name unless path.file? raise FormulaUnavailableError, name unless path.file?
Formulary.load_formula_from_path(name, path, flags: flags, ignore_errors: ignore_errors) Formulary.load_formula_from_path(name, path, flags: flags, ignore_errors: ignore_errors)
@ -519,7 +528,17 @@ module Formulary
end end
# Loads a formula from a bottle. # 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) def initialize(bottle_name)
case bottle_name case bottle_name
when URL_START_REGEX when URL_START_REGEX
@ -562,27 +581,67 @@ module Formulary
end end
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. # Loads formulae from disk using a path.
class FromPathLoader < FormulaLoader class FromPathLoader < FormulaLoader
def initialize(path) sig {
path = Pathname.new(path).expand_path 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 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
end end
# Loads formulae from URLs. # Loads formula from a URI.
class FromUrlLoader < FormulaLoader 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 attr_reader :url
sig { params(url: T.any(URI::Generic, String), from: T.nilable(Symbol)).void } sig { params(url: T.any(URI::Generic, String), from: T.nilable(Symbol)).void }
@ -621,7 +680,45 @@ module Formulary
end end
# Loads tapped formulae. # 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 (name = ref[HOMEBREW_TAP_FORMULA_REGEX, :name])
alias_name = name
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) def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
super super
rescue FormulaUnreadableError => e rescue FormulaUnreadableError => e
@ -638,17 +735,94 @@ module Formulary
e.issues_url = tap.issues_url || tap.to_s e.issues_url = tap.issues_url || tap.to_s
raise raise
end end
end
private class FromDefaultNameLoader < FromTapLoader
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 unless ref.is_a?(String)
return unless (name = ref[HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, :name])
return unless (tap = CoreTap.instance).installed?
def find_formula_from_name(name, tap) return unless (loader = super("#{tap}/#{name}", warn: warn))
Formulary.find_formula_in_tap(name, tap)
loader if loader.path.exist?
end
end
# Loads a formula from a name, as long as it exists only in a single tap.
class FromNameLoader < FromTapLoader
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 unless ref.is_a?(String)
return if ref.include?("/")
name = ref
loaders = Tap.map { |tap| super("#{tap}/#{name}") }
.compact
.select { _1.path.exist? }
case loaders.count
when 1
loaders.first
when 2..Float::INFINITY
raise TapFormulaAmbiguityError.new(name, loaders)
end
end
end
# 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?
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
end end
# Pseudo-loader which will raise a {FormulaUnavailableError} when trying to load the corresponding formula. # Pseudo-loader which will raise a {FormulaUnavailableError} when trying to load the corresponding formula.
class NullLoader < FormulaLoader 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) super name, Formulary.core_path(name)
end end
@ -668,16 +842,50 @@ module Formulary
end end
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
$stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{path}" if debug?
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}" namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}"
Formulary.load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors) Formulary.load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors)
end end
end end
# Load formulae from the API. # Load a formula from the API.
class FormulaAPILoader < FormulaLoader class FromAPILoader < FormulaLoader
def initialize(name) sig {
super name, Formulary.core_path(name) 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 (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)
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 end
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
@ -688,20 +896,10 @@ module Formulary
private private
def load_from_api(flags:) 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) Formulary.load_formula_from_api(name, flags: flags)
end end
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. # Return a {Formula} instance for the given reference.
# `ref` is a string containing: # `ref` is a string containing:
# #
@ -741,6 +939,7 @@ module Formulary
force_bottle: force_bottle, force_bottle: force_bottle,
flags: flags, flags: flags,
ignore_errors: ignore_errors }.compact ignore_errors: ignore_errors }.compact
formula = loader_for(ref, **loader_options) formula = loader_for(ref, **loader_options)
.get_formula(spec, **formula_options) .get_formula(spec, **formula_options)
@ -793,7 +992,7 @@ module Formulary
# Return whether given rack is keg-only. # Return whether given rack is keg-only.
def self.keg_only?(rack) def self.keg_only?(rack)
Formulary.from_rack(rack).keg_only? Formulary.from_rack(rack).keg_only?
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError rescue FormulaUnavailableError, TapFormulaAmbiguityError
false false
end end
@ -906,9 +1105,13 @@ module Formulary
user, repo, name = tapped_name.split("/", 3).map(&:downcase) user, repo, name = tapped_name.split("/", 3).map(&:downcase)
tap = Tap.fetch(user, repo) tap = Tap.fetch(user, repo)
type = nil 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 = 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
# the alias table only contain short names.
name = possible_alias.split("/").last name = possible_alias.split("/").last
type = :alias type = :alias
elsif (new_name = tap.formula_renames[name].presence) elsif (new_name = tap.formula_renames[name].presence)
@ -938,103 +1141,32 @@ module Formulary
[name, tap, type] [name, tap, type]
end end
def self.tap_loader_for(tapped_name, warn:) def self.loader_for(ref, from: T.unsafe(nil), warn: true)
name, tap, type = Formulary.tap_formula_name_type(tapped_name, warn: warn) options = { from: from, warn: warn }.compact
if tap.core_tap? && !Homebrew::EnvConfig.no_install_from_api? [
if type == :alias FromBottleLoader,
return AliasAPILoader.new(name) FromURILoader,
elsif Homebrew::API::Formula.all_formulae.key?(name) FromAPILoader,
return FormulaAPILoader.new(name) FromTapLoader,
FromPathLoader,
FromDefaultNameLoader,
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
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 end
def self.core_path(name) def self.core_path(name)
find_formula_in_tap(name.to_s.downcase, CoreTap.instance) find_formula_in_tap(name.to_s.downcase, CoreTap.instance)
end 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.select(&:installed?).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) } sig { params(name: String, tap: Tap).returns(Pathname) }
def self.find_formula_in_tap(name, tap) def self.find_formula_in_tap(name, tap)
filename = if name.end_with?(".rb") filename = if name.end_with?(".rb")

View File

@ -1,22 +1,42 @@
# typed: strict # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
# Match taps' formulae, e.g. `someuser/sometap/someformula` # Match a formula name.
HOMEBREW_TAP_FORMULA_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([\w+-.@]+)$}, Regexp) HOMEBREW_TAP_FORMULA_NAME_REGEX = T.let(/(?<name>[\w+\-.@]+)/, Regexp)
# Match taps' casks, e.g. `someuser/sometap/somecask` # Match taps' formulae, e.g. `someuser/sometap/someformula`.
HOMEBREW_TAP_CASK_REGEX = T.let(%r{^([\w-]+)/([\w-]+)/([a-z0-9\-_]+)$}, Regexp) HOMEBREW_TAP_FORMULA_REGEX = T.let(
# Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask` %r{\A(?<user>[\w-]+)/(?<repo>[\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{\A(?:[Hh]omebrew/(?:homebrew-)?core/)?(?<name>#{HOMEBREW_TAP_FORMULA_NAME_REGEX.source})\Z},
Regexp,
)
# Match a cask token.
HOMEBREW_TAP_CASK_TOKEN_REGEX = T.let(/(?<token>[a-z0-9\-_]+(?:@[a-z0-9\-_.]+)?)/, Regexp)
# Match taps' casks, e.g. `someuser/sometap/somecask`.
HOMEBREW_TAP_CASK_REGEX = T.let(
%r{\A(?<user>[\w-]+)/(?<repo>[\w-]+)/#{HOMEBREW_TAP_CASK_TOKEN_REGEX.source}\Z},
Regexp,
)
# Match default cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask`.
HOMEBREW_DEFAULT_TAP_CASK_REGEX = T.let( HOMEBREW_DEFAULT_TAP_CASK_REGEX = T.let(
%r{^(?:[Hh]omebrew/(?:homebrew-)?cask/)?(?<token>[a-z0-9\-_]+)$}, Regexp %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( HOMEBREW_TAP_DIR_REGEX = T.let(
%r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?<user>[\w-]+)/(?<repo>[\w-]+)}, Regexp %r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?<user>[\w-]+)/(?<repo>[\w-]+)},
Regexp,
) )
# Match taps' formula paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap/someformula` # 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) 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` # Match official taps' casks, e.g. `homebrew/cask/somecask or homebrew/cask-versions/somecask`.
HOMEBREW_CASK_TAP_CASK_REGEX = HOMEBREW_CASK_TAP_CASK_REGEX = T.let(
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) Regexp,
HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = T.let(/^(home|linux)brew-/, Regexp) )
HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = T.let(/\A(home|linux)brew-/, Regexp)

View File

@ -28,7 +28,7 @@ RSpec.describe Formula do
let(:path) { Formulary.core_path(name) } let(:path) { Formulary.core_path(name) }
let(:spec) { :stable } let(:spec) { :stable }
let(:alias_name) { "baz@1" } 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) { klass.new(name, path, spec) }
let(:f_alias) { klass.new(name, path, spec, alias_path: alias_path) } let(:f_alias) { klass.new(name, path, spec, alias_path: alias_path) }
@ -190,11 +190,11 @@ RSpec.describe Formula do
end end
alias_name = "bar" alias_name = "bar"
alias_path = "#{CoreTap.instance.alias_dir}/#{alias_name}" alias_path = CoreTap.instance.alias_dir/alias_name
CoreTap.instance.alias_dir.mkpath CoreTap.instance.alias_dir.mkpath
FileUtils.ln_sf f.path, alias_path 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_path).to eq(alias_path)
expect(f.installed_alias_name).to eq(alias_name) expect(f.installed_alias_name).to eq(alias_name)
@ -225,12 +225,12 @@ RSpec.describe Formula do
end end
alias_name = "bar" alias_name = "bar"
alias_path = tap.alias_dir/alias_name
full_alias_name = "#{tap.user}/#{tap.repo}/#{alias_name}" full_alias_name = "#{tap.user}/#{tap.repo}/#{alias_name}"
alias_path = "#{tap.alias_dir}/#{alias_name}"
tap.alias_dir.mkpath tap.alias_dir.mkpath
FileUtils.ln_sf f.path, alias_path 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_path).to eq(alias_path)
expect(f.installed_alias_name).to eq(alias_name) expect(f.installed_alias_name).to eq(alias_name)
@ -451,7 +451,7 @@ RSpec.describe Formula do
FileUtils.ln_sf f.path, source_path FileUtils.ln_sf f.path, source_path
expect(f.alias_path).to eq(alias_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
end end
@ -491,14 +491,14 @@ RSpec.describe Formula do
end end
specify "with alias path with a path" do specify "with alias path with a path" do
alias_path = "#{CoreTap.instance.alias_dir}/alias" alias_path = CoreTap.instance.alias_dir/"alias"
different_alias_path = "#{CoreTap.instance.alias_dir}/another_alias" different_alias_path = CoreTap.instance.alias_dir/"another_alias"
formula_with_alias = formula "foo" do formula_with_alias = formula "foo" do
url "foo-1.0" url "foo-1.0"
end end
formula_with_alias.build = Tab.empty 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 formula_without_alias = formula "bar" do
url "bar-1.0" url "bar-1.0"
@ -510,7 +510,7 @@ RSpec.describe Formula do
url "baz-1.0" url "baz-1.0"
end end
formula_with_different_alias.build = Tab.empty 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 = [ formulae = [
formula_with_alias, formula_with_alias,
@ -1239,8 +1239,8 @@ RSpec.describe Formula do
end end
let(:tab) { Tab.empty } let(:tab) { Tab.empty }
let(:alias_path) { "#{CoreTap.instance.alias_dir}/bar" }
let(:alias_name) { "bar" } let(:alias_name) { "bar" }
let(:alias_path) { CoreTap.instance.alias_dir/alias_name }
before do before do
allow(described_class).to receive(:installed).and_return([f]) allow(described_class).to receive(:installed).and_return([f])
@ -1261,7 +1261,7 @@ RSpec.describe Formula do
end end
specify "alias changes when not changed" do 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) stub_formula_loader(f, alias_name)
CoreTap.instance.alias_dir.mkpath CoreTap.instance.alias_dir.mkpath
@ -1276,7 +1276,7 @@ RSpec.describe Formula do
end end
specify "alias changes when new alias target" do 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) stub_formula_loader(new_formula, alias_name)
CoreTap.instance.alias_dir.mkpath CoreTap.instance.alias_dir.mkpath
@ -1291,7 +1291,7 @@ RSpec.describe Formula do
end end
specify "alias changes when old formulae installed" do 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) stub_formula_loader(new_formula, alias_name)
CoreTap.instance.alias_dir.mkpath CoreTap.instance.alias_dir.mkpath
@ -1332,8 +1332,8 @@ RSpec.describe Formula do
end end
end end
let(:alias_path) { "#{f.tap.alias_dir}/bar" }
let(:alias_name) { "bar" } let(:alias_name) { "bar" }
let(:alias_path) { f.tap.alias_dir/alias_name }
def setup_tab_for_prefix(prefix, options = {}) def setup_tab_for_prefix(prefix, options = {})
prefix.mkpath prefix.mkpath

View File

@ -111,7 +111,7 @@ RSpec.describe Formulary do
it "raises an error" do it "raises an error" do
expect do expect do
described_class.factory(formula_name) described_class.factory(formula_name)
end.to raise_error(FormulaClassUnavailableError) end.to raise_error(TapFormulaClassUnavailableError)
end end
end end
@ -139,7 +139,7 @@ RSpec.describe Formulary do
context "when given an alias" do context "when given an alias" do
subject(:formula) { described_class.factory("foo") } 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" } let(:alias_path) { alias_dir/"foo" }
before do before do
@ -152,7 +152,7 @@ RSpec.describe Formulary do
end end
it "calling #alias_path on the returned Formula returns the alias path" do 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
end end
@ -229,23 +229,26 @@ RSpec.describe Formulary do
let(:tap) { Tap.new("homebrew", "foo") } let(:tap) { Tap.new("homebrew", "foo") }
let(:another_tap) { Tap.new("homebrew", "bar") } let(:another_tap) { Tap.new("homebrew", "bar") }
let(:formula_path) { tap.path/"Formula/#{formula_name}.rb" } 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 it "returns a Formula when given a name" do
expect(described_class.factory(formula_name)).to be_a(Formula) expect(described_class.factory(formula_name)).to be_a(Formula)
end end
it "returns a Formula from an Alias path" do it "returns a Formula from an Alias path" do
alias_dir = tap.path/"Aliases" expect(described_class.factory(alias_name)).to be_a(Formula)
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_dir/"bar"
expect(described_class.factory("bar")).to be_a(Formula)
end end
it "returns a Formula from a fully qualified Alias path" do it "returns a Formula from a fully qualified Alias path" do
alias_dir = tap.path/"Aliases" expect(described_class.factory("#{tap.name}/#{alias_name}")).to be_a(Formula)
alias_dir.mkpath
FileUtils.ln_s formula_path, alias_dir/"bar"
expect(described_class.factory("#{tap}/bar")).to be_a(Formula)
end end
it "raises an error when the Formula cannot be found" do it "raises an error when the Formula cannot be found" do