Enable strict typing in Formulary

This commit is contained in:
Rylan Polster 2025-08-24 12:58:48 -04:00
parent 566290dcbc
commit 4410388043
No known key found for this signature in database
2 changed files with 164 additions and 73 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "digest/sha2" require "digest/sha2"
@ -34,23 +34,41 @@ module Formulary
# @api internal # @api internal
sig { void } sig { void }
def self.enable_factory_cache! def self.enable_factory_cache!
@factory_cache = true @factory_cache_enabled = T.let(true, T.nilable(T::Boolean))
cache[platform_cache_tag] ||= {}
cache[platform_cache_tag][:formulary_factory] ||= {}
end end
sig { returns(T::Boolean) }
def self.factory_cached? def self.factory_cached?
!@factory_cache.nil? !@factory_cache_enabled.nil?
end end
sig { returns(String) }
def self.platform_cache_tag
"#{Homebrew::SimulateSystem.current_os}_#{Homebrew::SimulateSystem.current_arch}"
end
private_class_method :platform_cache_tag
sig { returns(T::Hash[Symbol, T::Hash[String, T.class_of(Formula)]]) }
def self.platform_cache def self.platform_cache
cache["#{Homebrew::SimulateSystem.current_os}_#{Homebrew::SimulateSystem.current_arch}"] ||= {} cache[platform_cache_tag] ||= {}
end end
sig { returns(T::Hash[String, Formula]) }
def self.factory_cache
cache[platform_cache_tag] ||= {}
cache[platform_cache_tag][:formulary_factory] ||= {}
end
sig { params(path: T.any(String, Pathname)).returns(T::Boolean) }
def self.formula_class_defined_from_path?(path) def self.formula_class_defined_from_path?(path)
platform_cache.key?(:path) && platform_cache[:path].key?(path) platform_cache.key?(:path) && platform_cache.fetch(:path).key?(path.to_s)
end end
sig { params(name: String).returns(T::Boolean) }
def self.formula_class_defined_from_api?(name) def self.formula_class_defined_from_api?(name)
platform_cache.key?(:api) && platform_cache[:api].key?(name) platform_cache.key?(:api) && platform_cache.fetch(:api).key?(name)
end end
sig { params(name: String).returns(T::Boolean) } sig { params(name: String).returns(T::Boolean) }
@ -58,12 +76,14 @@ module Formulary
platform_cache.key?(:stub) && platform_cache.fetch(:stub).key?(name) platform_cache.key?(:stub) && platform_cache.fetch(:stub).key?(name)
end end
sig { params(path: T.any(String, Pathname)).returns(T.class_of(Formula)) }
def self.formula_class_get_from_path(path) def self.formula_class_get_from_path(path)
platform_cache[:path].fetch(path) platform_cache.fetch(:path).fetch(path.to_s)
end end
sig { params(name: String).returns(T.class_of(Formula)) }
def self.formula_class_get_from_api(name) def self.formula_class_get_from_api(name)
platform_cache[:api].fetch(name) platform_cache.fetch(:api).fetch(name)
end end
sig { params(name: String).returns(T.class_of(Formula)) } sig { params(name: String).returns(T.class_of(Formula)) }
@ -71,6 +91,7 @@ module Formulary
platform_cache.fetch(:stub).fetch(name) platform_cache.fetch(:stub).fetch(name)
end end
sig { void }
def self.clear_cache def self.clear_cache
platform_cache.each do |type, cached_objects| platform_cache.each do |type, cached_objects|
next if type == :formulary_factory next if type == :formulary_factory
@ -92,10 +113,10 @@ module Formulary
end end
module PathnameWriteMkpath module PathnameWriteMkpath
# TODO: migrate away from refinements here, they don't play nicely with # TODO: migrate away from refinements here, they don't play nicely with Sorbet
# Sorbet, when we migrate to `typed: strict`
# rubocop:todo Sorbet/BlockMethodDefinition # rubocop:todo Sorbet/BlockMethodDefinition
refine Pathname do refine Pathname do
sig { params(content: Object, offset: T.nilable(Integer), open_args: T.untyped).returns(Integer) }
def write(content, offset = nil, **open_args) def write(content, offset = nil, **open_args)
T.bind(self, Pathname) T.bind(self, Pathname)
raise "Will not overwrite #{self}" if exist? && !offset && !open_args[:mode]&.match?(/^a\+?$/) raise "Will not overwrite #{self}" if exist? && !offset && !open_args[:mode]&.match?(/^a\+?$/)
@ -109,6 +130,17 @@ module Formulary
end end
using PathnameWriteMkpath using PathnameWriteMkpath
sig {
params(
name: String,
path: Pathname,
contents: String,
namespace: String,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(T.class_of(Formula))
}
def self.load_formula(name, path, contents, namespace, flags:, ignore_errors:) def self.load_formula(name, path, contents, namespace, flags:, ignore_errors:)
raise "Formula loading disabled by `$HOMEBREW_DISABLE_LOAD_FORMULA`!" if Homebrew::EnvConfig.disable_load_formula? raise "Formula loading disabled by `$HOMEBREW_DISABLE_LOAD_FORMULA`!" if Homebrew::EnvConfig.disable_load_formula?
@ -121,6 +153,7 @@ module Formulary
$stdout = StringIO.new $stdout = StringIO.new
mod = Module.new mod = Module.new
namespace = namespace.to_sym
remove_const(namespace) if const_defined?(namespace) remove_const(namespace) if const_defined?(namespace)
const_set(namespace, mod) const_set(namespace, mod)
@ -128,7 +161,7 @@ module Formulary
# Set `BUILD_FLAGS` in the formula's namespace so we can # Set `BUILD_FLAGS` in the formula's namespace so we can
# access them from within the formula's class scope. # access them from within the formula's class scope.
mod.const_set(:BUILD_FLAGS, flags) mod.const_set(:BUILD_FLAGS, flags)
mod.module_eval(contents, path) mod.module_eval(contents, path.to_s)
rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersion::Error => e rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersion::Error => e
if e.is_a?(Ignorable::ExceptionMixin) if e.is_a?(Ignorable::ExceptionMixin)
e.ignore e.ignore
@ -174,6 +207,13 @@ module Formulary
) )
end end
sig { params(string: String).returns(String) }
def self.replace_placeholders(string)
string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
end
sig { sig {
params(name: String, path: Pathname, flags: T::Array[String], ignore_errors: T::Boolean) params(name: String, path: Pathname, flags: T::Array[String], ignore_errors: T::Boolean)
.returns(T.class_of(Formula)) .returns(T.class_of(Formula))
@ -183,7 +223,7 @@ module Formulary
namespace = "FormulaNamespace#{namespace_key(path.to_s)}" namespace = "FormulaNamespace#{namespace_key(path.to_s)}"
klass = load_formula(name, path, contents, namespace, flags:, ignore_errors:) klass = load_formula(name, path, contents, namespace, flags:, ignore_errors:)
platform_cache[:path] ||= {} platform_cache[:path] ||= {}
platform_cache[:path][path] = klass platform_cache.fetch(:path)[path.to_s] = klass
end end
sig { params(name: String, json_formula_with_variations: T::Hash[String, T.untyped], flags: T::Array[String]).returns(T.class_of(Formula)) } sig { params(name: String, json_formula_with_variations: T::Hash[String, T.untyped], flags: T::Array[String]).returns(T.class_of(Formula)) }
@ -199,6 +239,8 @@ module Formulary
class_name = class_s(name) class_name = class_s(name)
json_formula = Homebrew::API.merge_variations(json_formula_with_variations) json_formula = Homebrew::API.merge_variations(json_formula_with_variations)
caveats_string = (replace_placeholders(json_formula["caveats"]) if json_formula["caveats"])
uses_from_macos_names = json_formula.fetch("uses_from_macos", []).map do |dep| uses_from_macos_names = json_formula.fetch("uses_from_macos", []).map do |dep|
next dep unless dep.is_a? Hash next dep unless dep.is_a? Hash
@ -278,12 +320,9 @@ module Formulary
end end
end end
# TODO: migrate away from this inline class here, they don't play nicely with
# Sorbet, when we migrate to `typed: strict`
# rubocop:todo Sorbet/BlockMethodDefinition
klass = Class.new(::Formula) do klass = Class.new(::Formula) do
@loaded_from_api = true @loaded_from_api = T.let(true, T.nilable(T::Boolean))
@api_source = json_formula_with_variations @api_source = T.let(json_formula_with_variations, T.nilable(T::Hash[String, T.untyped]))
desc json_formula["desc"] desc json_formula["desc"]
homepage json_formula["homepage"] homepage json_formula["homepage"]
@ -373,13 +412,13 @@ module Formulary
link_overwrite overwrite_path link_overwrite overwrite_path
end end
def install define_method(:install) do
raise "Cannot build from source from abstract formula." raise NotImplementedError, "Cannot build from source from abstract formula."
end end
@post_install_defined_boolean = json_formula["post_install_defined"] @post_install_defined_boolean = T.let(json_formula["post_install_defined"], T.nilable(T::Boolean))
@post_install_defined_boolean = true if @post_install_defined_boolean.nil? # Backwards compatibility @post_install_defined_boolean = true if @post_install_defined_boolean.nil? # Backwards compatibility
def post_install_defined? define_method(:post_install_defined?) do
self.class.instance_variable_get(:@post_install_defined_boolean) self.class.instance_variable_get(:@post_install_defined_boolean)
end end
@ -407,55 +446,48 @@ module Formulary
end end
end end
@caveats_string = json_formula["caveats"] @caveats_string = T.let(caveats_string, T.nilable(String))
def caveats define_method(:caveats) do
caveats_string = self.class.instance_variable_get(:@caveats_string) self.class.instance_variable_get(:@caveats_string)
return unless caveats_string
caveats_string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
end end
@tap_git_head_string = json_formula["tap_git_head"] @tap_git_head_string = T.let(json_formula["tap_git_head"], T.nilable(String))
define_method(:tap_git_head) do
def tap_git_head
self.class.instance_variable_get(:@tap_git_head_string) self.class.instance_variable_get(:@tap_git_head_string)
end end
@oldnames_array = json_formula["oldnames"] || [json_formula["oldname"]].compact @oldnames_array = T.let(json_formula["oldnames"] || [json_formula["oldname"]].compact, T.nilable(T::Array[String]))
def oldnames define_method(:oldnames) do
self.class.instance_variable_get(:@oldnames_array) self.class.instance_variable_get(:@oldnames_array)
end end
@aliases_array = json_formula.fetch("aliases", []) @aliases_array = T.let(json_formula.fetch("aliases", []), T.nilable(T::Array[String]))
def aliases define_method(:aliases) do
self.class.instance_variable_get(:@aliases_array) self.class.instance_variable_get(:@aliases_array)
end end
@versioned_formulae_array = json_formula.fetch("versioned_formulae", []) @versioned_formulae_array = T.let(json_formula.fetch("versioned_formulae", []), T.nilable(T::Array[String]))
def versioned_formulae_names define_method(:versioned_formulae_names) do
self.class.instance_variable_get(:@versioned_formulae_array) self.class.instance_variable_get(:@versioned_formulae_array)
end end
@ruby_source_path_string = json_formula["ruby_source_path"] @ruby_source_path_string = T.let(json_formula["ruby_source_path"], T.nilable(String))
def ruby_source_path define_method(:ruby_source_path) do
self.class.instance_variable_get(:@ruby_source_path_string) self.class.instance_variable_get(:@ruby_source_path_string)
end end
@ruby_source_checksum_string = json_formula.dig("ruby_source_checksum", "sha256") @ruby_source_checksum_string = T.let(json_formula.dig("ruby_source_checksum", "sha256"), T.nilable(String))
@ruby_source_checksum_string ||= json_formula["ruby_source_sha256"] @ruby_source_checksum_string ||= json_formula["ruby_source_sha256"]
def ruby_source_checksum define_method(:ruby_source_checksum) do
checksum = self.class.instance_variable_get(:@ruby_source_checksum_string) checksum = self.class.instance_variable_get(:@ruby_source_checksum_string)
Checksum.new(checksum) if checksum Checksum.new(checksum) if checksum
end end
end end
# rubocop:enable Sorbet/BlockMethodDefinition
mod.const_set(class_name, klass) mod.const_set(class_name, klass)
platform_cache[:api] ||= {} platform_cache[:api] ||= {}
platform_cache[:api][name] = klass platform_cache.fetch(:api)[name] = klass
end end
sig { params(name: String, formula_stub: Homebrew::FormulaStub, flags: T::Array[String]).returns(T.class_of(Formula)) } sig { params(name: String, formula_stub: Homebrew::FormulaStub, flags: T::Array[String]).returns(T.class_of(Formula)) }
@ -471,8 +503,8 @@ module Formulary
class_name = class_s(name) class_name = class_s(name)
klass = Class.new(::Formula) do klass = Class.new(::Formula) do
@loaded_from_api = true @loaded_from_api = T.let(true, T.nilable(T::Boolean))
@loaded_from_stub = true @loaded_from_stub = T.let(true, T.nilable(T::Boolean))
url "formula-stub://#{name}/#{formula_stub.pkg_version}" url "formula-stub://#{name}/#{formula_stub.pkg_version}"
version formula_stub.version.to_s version formula_stub.version.to_s
@ -496,7 +528,7 @@ module Formulary
mod.const_set(class_name, klass) mod.const_set(class_name, klass)
platform_cache[:stub] ||= {} platform_cache[:stub] ||= {}
platform_cache[:stub][name] = klass platform_cache.fetch(:stub)[name] = klass
end end
sig { sig {
@ -540,10 +572,12 @@ module Formulary
f f
end end
sig { params(io: IO).returns(IO) }
def self.ensure_utf8_encoding(io) def self.ensure_utf8_encoding(io)
io.set_encoding(Encoding::UTF_8) io.set_encoding(Encoding::UTF_8)
end end
sig { params(name: String).returns(String) }
def self.class_s(name) def self.class_s(name)
class_name = name.capitalize class_name = name.capitalize
class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { T.must(Regexp.last_match(1)).upcase } class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { T.must(Regexp.last_match(1)).upcase }
@ -552,8 +586,9 @@ module Formulary
class_name class_name
end end
sig { params(string: String).returns(T.any(String, Symbol)) }
def self.convert_to_string_or_symbol(string) def self.convert_to_string_or_symbol(string)
return string[1..].to_sym if string.start_with?(":") return T.must(string[1..]).to_sym if string.start_with?(":")
string string
end end
@ -573,14 +608,16 @@ module Formulary
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)) } sig { returns(T.nilable(T.any(Pathname, String))) }
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)) } sig { returns(T.nilable(Tap)) }
attr_reader :tap attr_reader :tap
sig { params(name: String, path: Pathname, alias_path: T.nilable(Pathname), tap: T.nilable(Tap)).void } sig {
params(name: String, path: Pathname, alias_path: T.nilable(T.any(Pathname, String)), tap: T.nilable(Tap)).void
}
def initialize(name, path, alias_path: nil, tap: nil) def initialize(name, path, alias_path: nil, tap: nil)
@name = name @name = name
@path = path @path = path
@ -591,12 +628,23 @@ module Formulary
# Gets the formula instance. # Gets the formula instance.
# `alias_path` can be overridden here in case an alias was used to refer to # `alias_path` can be overridden here in case an alias was used to refer to
# a formula that was loaded in another way. # a formula that was loaded in another way.
sig {
overridable.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
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)
alias_path ||= self.alias_path alias_path ||= self.alias_path
alias_path = Pathname(alias_path) if alias_path.is_a?(String)
klass(flags:, ignore_errors:) klass(flags:, ignore_errors:)
.new(name, path, spec, alias_path:, tap:, force_bottle:) .new(name, path, spec, alias_path:, tap:, force_bottle:)
end end
sig { overridable.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
load_file(flags:, ignore_errors:) unless Formulary.formula_class_defined_from_path?(path) load_file(flags:, ignore_errors:) unless Formulary.formula_class_defined_from_path?(path)
Formulary.formula_class_get_from_path(path) Formulary.formula_class_get_from_path(path)
@ -604,6 +652,7 @@ module Formulary
private private
sig { overridable.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:) def load_file(flags:, ignore_errors:)
raise FormulaUnavailableError, name unless path.file? raise FormulaUnavailableError, name unless path.file?
@ -627,13 +676,23 @@ module Formulary
new(ref) if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(ref) && File.exist?(ref) new(ref) if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(ref) && File.exist?(ref)
end end
sig { params(bottle_name: String, warn: T::Boolean).void }
def initialize(bottle_name, warn: false) def initialize(bottle_name, warn: false)
@bottle_path = Pathname(bottle_name).realpath @bottle_path = T.let(Pathname(bottle_name).realpath, Pathname)
name, full_name = Utils::Bottles.resolve_formula_names(@bottle_path) name, full_name = Utils::Bottles.resolve_formula_names(@bottle_path)
super name, Formulary.path(full_name) super name, Formulary.path(full_name)
end end
def get_formula(spec, force_bottle: false, flags: [], ignore_errors: false, **) sig {
override.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
formula = begin formula = begin
contents = Utils::Bottles.formula_contents(@bottle_path, name:) contents = Utils::Bottles.formula_contents(@bottle_path, name:)
Formulary.from_contents(name, path, contents, spec, force_bottle:, Formulary.from_contents(name, path, contents, spec, force_bottle:,
@ -736,10 +795,10 @@ module Formulary
return if Homebrew::EnvConfig.forbid_packages_from_paths? return if Homebrew::EnvConfig.forbid_packages_from_paths?
# Cache compiled regex # Cache compiled regex
@uri_regex ||= begin @uri_regex ||= T.let(begin
uri_regex = ::URI::RFC2396_PARSER.make_regexp uri_regex = ::URI::RFC2396_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options) Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end end, T.nilable(Regexp))
uri = ref.to_s uri = ref.to_s
return unless uri.match?(@uri_regex) return unless uri.match?(@uri_regex)
@ -751,6 +810,7 @@ module Formulary
new(uri, from:) new(uri, from:)
end end
sig { returns(T.any(URI::Generic, String)) }
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 }
@ -764,6 +824,7 @@ module Formulary
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri_path) super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri_path)
end end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:) def load_file(flags:, ignore_errors:)
url_scheme = URI(url).scheme url_scheme = URI(url).scheme
if ALLOWED_URL_SCHEMES.exclude?(url_scheme) if ALLOWED_URL_SCHEMES.exclude?(url_scheme)
@ -777,7 +838,7 @@ module Formulary
Utils::Curl.curl_download url.to_s, to: path Utils::Curl.curl_download url.to_s, to: path
super super
rescue MethodDeprecatedError => e rescue MethodDeprecatedError => e
if (match_data = url.match(%r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/}).presence) if (match_data = url.to_s.match(%r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/}).presence)
e.issues_url = "https://github.com/#{match_data[:user]}/#{match_data[:repo]}/issues/new" e.issues_url = "https://github.com/#{match_data[:user]}/#{match_data[:repo]}/issues/new"
end end
raise raise
@ -794,7 +855,7 @@ module Formulary
sig { sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean) params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(FormulaLoader)) .returns(T.nilable(T.attached_class))
} }
def self.try_new(ref, from: nil, warn: false) def self.try_new(ref, from: nil, warn: false)
ref = ref.to_s ref = ref.to_s
@ -810,7 +871,7 @@ module Formulary
end end
if type == :migration && tap.core_tap? && (loader = FromAPILoader.try_new(name)) if type == :migration && tap.core_tap? && (loader = FromAPILoader.try_new(name))
loader T.cast(loader, T.attached_class)
else else
new(name, path, tap:, alias_name:) new(name, path, tap:, alias_name:)
end end
@ -824,6 +885,15 @@ module Formulary
@tap = tap @tap = tap
end end
sig {
override.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
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
@ -834,6 +904,7 @@ module Formulary
raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace
end end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:) def load_file(flags:, ignore_errors:)
super super
rescue MethodDeprecatedError => e rescue MethodDeprecatedError => e
@ -845,8 +916,8 @@ module Formulary
# Loads a formula from a name, as long as it exists only in a single tap. # Loads a formula from a name, as long as it exists only in a single tap.
class FromNameLoader < FromTapLoader class FromNameLoader < FromTapLoader
sig { sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean) override.params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(FormulaLoader)) .returns(T.nilable(T.attached_class))
} }
def self.try_new(ref, from: nil, warn: false) def self.try_new(ref, from: nil, warn: false)
return unless ref.is_a?(String) return unless ref.is_a?(String)
@ -922,7 +993,16 @@ module Formulary
super name, Formulary.core_path(name) super name, Formulary.core_path(name)
end end
def get_formula(*) sig {
override.params(
_spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(_spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
raise FormulaUnavailableError, name raise FormulaUnavailableError, name
end end
end end
@ -930,13 +1010,16 @@ module Formulary
# Load formulae directly from their contents. # Load formulae directly from their contents.
class FormulaContentsLoader < FormulaLoader class FormulaContentsLoader < FormulaLoader
# The formula's contents. # The formula's contents.
sig { returns(String) }
attr_reader :contents attr_reader :contents
sig { params(name: String, path: Pathname, contents: String).void }
def initialize(name, path, contents) def initialize(name, path, contents)
@contents = contents @contents = contents
super name, path super name, path
end end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}" namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}"
Formulary.load_formula(name, path, contents, namespace, flags:, ignore_errors:) Formulary.load_formula(name, path, contents, namespace, flags:, ignore_errors:)
@ -979,6 +1062,7 @@ module Formulary
super(name, Formulary.core_path(name), alias_path:, tap:) super(name, Formulary.core_path(name), alias_path:, tap:)
end end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
load_from_api(flags:) unless Formulary.formula_class_defined_from_api?(name) load_from_api(flags:) unless Formulary.formula_class_defined_from_api?(name)
Formulary.formula_class_get_from_api(name) Formulary.formula_class_get_from_api(name)
@ -986,6 +1070,7 @@ module Formulary
private private
sig { overridable.params(flags: T::Array[String]).void }
def load_from_api(flags:) def load_from_api(flags:)
json_formula = Homebrew::API::Formula.all_formulae[name] json_formula = Homebrew::API::Formula.all_formulae[name]
raise FormulaUnavailableError, name if json_formula.nil? raise FormulaUnavailableError, name if json_formula.nil?
@ -996,6 +1081,7 @@ module Formulary
# Load formulae directly from their JSON contents. # Load formulae directly from their JSON contents.
class FormulaJSONContentsLoader < FromAPILoader class FormulaJSONContentsLoader < FromAPILoader
sig { params(name: String, contents: T::Hash[String, T.untyped], tap: T.nilable(Tap), alias_name: T.nilable(String)).void }
def initialize(name, contents, tap: nil, alias_name: nil) def initialize(name, contents, tap: nil, alias_name: nil)
@contents = contents @contents = contents
super(name, tap: tap, alias_name: alias_name) super(name, tap: tap, alias_name: alias_name)
@ -1003,6 +1089,7 @@ module Formulary
private private
sig { override.params(flags: T::Array[String]).void }
def load_from_api(flags:) def load_from_api(flags:)
Formulary.load_formula_from_json!(name, @contents, flags:) Formulary.load_formula_from_json!(name, @contents, flags:)
end end
@ -1011,8 +1098,8 @@ module Formulary
# Load a formula stub from the internal API. # Load a formula stub from the internal API.
class FormulaStubLoader < FromAPILoader class FormulaStubLoader < FromAPILoader
sig { sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean) override.params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(T.attached_class)) .returns(T.nilable(T.attached_class))
} }
def self.try_new(ref, from: nil, warn: false) def self.try_new(ref, from: nil, warn: false)
return unless Homebrew::EnvConfig.use_internal_api? return unless Homebrew::EnvConfig.use_internal_api?
@ -1020,6 +1107,7 @@ module Formulary
super super
end end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
load_from_api(flags:) unless Formulary.formula_class_defined_from_stub?(name) load_from_api(flags:) unless Formulary.formula_class_defined_from_stub?(name)
Formulary.formula_class_get_from_stub(name) Formulary.formula_class_get_from_stub(name)
@ -1027,6 +1115,7 @@ module Formulary
private private
sig { override.params(flags: T::Array[String]).void }
def load_from_api(flags:) def load_from_api(flags:)
formula_stub = Homebrew::API::Internal.formula_stub(name) formula_stub = Homebrew::API::Internal.formula_stub(name)
@ -1068,18 +1157,13 @@ module Formulary
prefer_stub: false prefer_stub: false
) )
cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}-#{prefer_stub}" cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}-#{prefer_stub}"
if factory_cached? && platform_cache[:formulary_factory]&.key?(cache_key) return factory_cache.fetch(cache_key) if factory_cached? && factory_cache.key?(cache_key)
return platform_cache[:formulary_factory][cache_key]
end
loader = FormulaStubLoader.try_new(ref, from:, warn:) if prefer_stub loader = FormulaStubLoader.try_new(ref, from:, warn:) if prefer_stub
loader ||= loader_for(ref, from:, warn:) loader ||= loader_for(ref, from:, warn:)
formula = loader.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:) formula = loader.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
if factory_cached? factory_cache[cache_key] ||= formula if factory_cached?
platform_cache[:formulary_factory] ||= {}
platform_cache[:formulary_factory][cache_key] ||= formula
end
formula formula
end end
@ -1118,6 +1202,7 @@ module Formulary
end end
# Return whether given rack is keg-only. # Return whether given rack is keg-only.
sig { params(rack: Pathname).returns(T::Boolean) }
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 rescue FormulaUnavailableError, TapFormulaAmbiguityError
@ -1224,6 +1309,7 @@ module Formulary
.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:) .get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
end end
sig { params(ref: String).returns(Pathname) }
def self.to_rack(ref) def self.to_rack(ref)
# If using a fully-scoped reference, check if the formula can be resolved. # If using a fully-scoped reference, check if the formula can be resolved.
factory(ref) if ref.include? "/" factory(ref) if ref.include? "/"
@ -1237,6 +1323,7 @@ module Formulary
(HOMEBREW_CELLAR/canonical_name(ref)).resolved_path (HOMEBREW_CELLAR/canonical_name(ref)).resolved_path
end end
sig { params(ref: String).returns(String) }
def self.canonical_name(ref) def self.canonical_name(ref)
loader_for(ref).name loader_for(ref).name
rescue TapFormulaAmbiguityError rescue TapFormulaAmbiguityError
@ -1245,6 +1332,7 @@ module Formulary
ref.downcase ref.downcase
end end
sig { params(ref: String).returns(Pathname) }
def self.path(ref) def self.path(ref)
loader_for(ref).path loader_for(ref).path
end end
@ -1293,6 +1381,7 @@ module Formulary
[name, tap, type] [name, tap, type]
end end
sig { params(ref: T.any(String, Pathname), from: T.nilable(Symbol), warn: T::Boolean).returns(FormulaLoader) }
def self.loader_for(ref, from: nil, warn: true) def self.loader_for(ref, from: nil, warn: true)
[ [
FromBottleLoader, FromBottleLoader,
@ -1303,15 +1392,17 @@ module Formulary
FromNameLoader, FromNameLoader,
FromKegLoader, FromKegLoader,
FromCacheLoader, FromCacheLoader,
NullLoader,
].each do |loader_class| ].each do |loader_class|
if (loader = loader_class.try_new(ref, from:, warn:)) if (loader = loader_class.try_new(ref, from:, warn:))
$stderr.puts "#{$PROGRAM_NAME} (#{loader_class}): loading #{ref}" if verbose? && debug? $stderr.puts "#{$PROGRAM_NAME} (#{loader_class}): loading #{ref}" if verbose? && debug?
return loader return loader
end end
end end
NullLoader.new(ref)
end end
sig { params(name: String).returns(Pathname) }
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

View File

@ -171,10 +171,10 @@ module Homebrew
log_command = "git log --since='1 month ago' --diff-filter=D " \ log_command = "git log --since='1 month ago' --diff-filter=D " \
"--name-only --max-count=1 " \ "--name-only --max-count=1 " \
"--format=%H\\\\n%h\\\\n%B -- #{relative_path}" "--format=%H\\\\n%h\\\\n%B -- #{relative_path}"
hash, short_hash, *commit_message, relative_path = hash, short_hash, *commit_message, relative_path_string =
Utils.popen_read(log_command).gsub("\\n", "\n").lines.map(&:chomp) Utils.popen_read(log_command).gsub("\\n", "\n").lines.map(&:chomp)
if hash.blank? || short_hash.blank? || relative_path.blank? if hash.blank? || short_hash.blank? || relative_path_string.blank?
ofail "No previously deleted formula found." unless silent ofail "No previously deleted formula found." unless silent
return return
end end
@ -189,7 +189,7 @@ module Homebrew
#{commit_message} #{commit_message}
To show the formula before removal, run: To show the formula before removal, run:
git -C "$(brew --repo #{tap})" show #{short_hash}^:#{relative_path} git -C "$(brew --repo #{tap})" show #{short_hash}^:#{relative_path_string}
If you still use this formula, consider creating your own tap: If you still use this formula, consider creating your own tap:
#{Formatter.url("https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap")} #{Formatter.url("https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap")}