Enable loading stubbed formulae

This commit is contained in:
Rylan Polster 2025-08-19 00:05:44 -04:00
parent 99cdd7d8c0
commit b04b0971a1
No known key found for this signature in database
3 changed files with 159 additions and 8 deletions

View File

@ -271,6 +271,7 @@ class Formula
@prefix_returns_versioned_prefix = T.let(false, T.nilable(T::Boolean))
@oldname_locks = T.let([], T::Array[FormulaLock])
@on_system_blocks_exist = T.let(false, T::Boolean)
@fully_loaded_formula = T.let(nil, T.nilable(Formula))
end
sig { params(spec_sym: Symbol).void }
@ -557,12 +558,34 @@ class Formula
# @see .loaded_from_api?
delegate loaded_from_api?: :"self.class"
# Whether this formula was loaded using the formulae.brew.sh API.
# @!method loaded_from_stub?
# @see .loaded_from_stub?
delegate loaded_from_stub?: :"self.class"
# The API source data used to load this formula.
# Returns `nil` if the formula was not loaded from the API.
# @!method api_source
# @see .api_source
delegate api_source: :"self.class"
sig { returns(Formula) }
def fully_loaded_formula
@fully_loaded_formula ||= if loaded_from_stub?
json_contents = Homebrew::API::Formula.formula_json(name)
Formulary.from_json_contents(name, json_contents)
else
self
end
end
sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def fetch_fully_loaded_formula!(download_queue: nil)
return unless loaded_from_stub?
Homebrew::API::Formula.fetch_formula_json!(name, download_queue:)
end
sig { void }
def update_head_version
return unless head?
@ -3366,6 +3389,7 @@ class Formula
@skip_clean_paths = T.let(Set.new, T.nilable(T::Set[T.any(String, Symbol)]))
@link_overwrite_paths = T.let(Set.new, T.nilable(T::Set[String]))
@loaded_from_api = T.let(false, T.nilable(T::Boolean))
@loaded_from_stub = T.let(false, T.nilable(T::Boolean))
@api_source = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
@on_system_blocks_exist = T.let(false, T.nilable(T::Boolean))
@network_access_allowed = T.let(SUPPORTED_NETWORK_ACCESS_PHASES.to_h do |phase|
@ -3391,6 +3415,10 @@ class Formula
sig { returns(T::Boolean) }
def loaded_from_api? = !!@loaded_from_api
# Whether this formula was loaded using the internal formulae.brew.sh API.
sig { returns(T::Boolean) }
def loaded_from_stub? = !!@loaded_from_stub
# Whether this formula was loaded using the formulae.brew.sh API.
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
attr_reader :api_source

View File

@ -50,6 +50,11 @@ module Formulary
platform_cache.key?(:api) && platform_cache[:api].key?(name)
end
sig { params(name: String).returns(T::Boolean) }
def self.formula_class_defined_from_stub?(name)
platform_cache.key?(:stub) && platform_cache.fetch(:stub).key?(name)
end
def self.formula_class_get_from_path(path)
platform_cache[:path].fetch(path)
end
@ -58,6 +63,11 @@ module Formulary
platform_cache[:api].fetch(name)
end
sig { params(name: String).returns(T.class_of(Formula)) }
def self.formula_class_get_from_stub(name)
platform_cache.fetch(:stub).fetch(name)
end
def self.clear_cache
platform_cache.each do |type, cached_objects|
next if type == :formulary_factory
@ -445,17 +455,59 @@ module Formulary
platform_cache[:api][name] = klass
end
sig { params(name: String, formula_stub: Homebrew::FormulaStub, flags: T::Array[String]).returns(T.class_of(Formula)) }
def self.load_formula_from_stub!(name, formula_stub, flags:)
namespace = :"FormulaNamespaceStub#{namespace_key(formula_stub.to_json)}"
mod = Module.new
remove_const(namespace) if const_defined?(namespace)
const_set(namespace, mod)
mod.const_set(:BUILD_FLAGS, flags)
class_name = class_s(name)
klass = Class.new(::Formula) do
@loaded_from_api = true
@loaded_from_stub = true
url "formula-stub://#{name}/#{formula_stub.pkg_version}"
version formula_stub.version.to_s
revision formula_stub.revision
bottle do
if Homebrew::EnvConfig.bottle_domain == HOMEBREW_BOTTLE_DEFAULT_DOMAIN
root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN
else
root_url Homebrew::EnvConfig.bottle_domain
end
rebuild formula_stub.rebuild
sha256 Utils::Bottles.tag.to_sym => formula_stub.sha256
end
define_method :install do
raise NotImplementedError, "Cannot build from source from abstract stubbed formula."
end
end
mod.const_set(class_name, klass)
platform_cache[:stub] ||= {}
platform_cache[:stub][name] = klass
end
sig {
params(name: String, spec: T.nilable(Symbol), force_bottle: T::Boolean, flags: T::Array[String]).returns(Formula)
params(name: String, spec: T.nilable(Symbol), force_bottle: T::Boolean, flags: T::Array[String], prefer_stub: T::Boolean).returns(Formula)
}
def self.resolve(
name,
spec: nil,
force_bottle: false,
flags: []
flags: [],
prefer_stub: false
)
if name.include?("/") || File.exist?(name)
f = factory(name, *spec, force_bottle:, flags:)
f = factory(name, *spec, force_bottle:, flags:, prefer_stub:)
if f.any_version_installed?
tab = Tab.for_formula(f)
resolved_spec = spec || tab.spec
@ -468,7 +520,7 @@ module Formulary
end
else
rack = to_rack(name)
alias_path = factory(name, force_bottle:, flags:).alias_path
alias_path = factory(name, force_bottle:, flags:, prefer_stub:).alias_path
f = from_rack(rack, *spec, alias_path:, force_bottle:, flags:)
end
@ -936,6 +988,46 @@ module Formulary
end
end
# Load formulae directly from their JSON contents.
class FormulaJSONContentsLoader < FromAPILoader
def initialize(name, contents, tap: nil, alias_name: nil)
@contents = contents
super(name, tap: tap, alias_name: alias_name)
end
private
def load_from_api(flags:)
Formulary.load_formula_from_json!(name, @contents, flags:)
end
end
# Load a formula stub from the internal API.
class FormulaStubLoader < FromAPILoader
sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, from: nil, warn: false)
return unless Homebrew::EnvConfig.use_internal_api?
super
end
def klass(flags:, ignore_errors:)
load_from_api(flags:) unless Formulary.formula_class_defined_from_stub?(name)
Formulary.formula_class_get_from_stub(name)
end
private
def load_from_api(flags:)
formula_stub = Homebrew::API::Internal.formula_stub(name)
Formulary.load_formula_from_stub!(name, formula_stub, flags:)
end
end
# Return a {Formula} instance for the given reference.
# `ref` is a string containing:
#
@ -955,6 +1047,7 @@ module Formulary
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
prefer_stub: T::Boolean,
).returns(Formula)
}
def self.factory(
@ -965,15 +1058,17 @@ module Formulary
warn: false,
force_bottle: false,
flags: [],
ignore_errors: false
ignore_errors: false,
prefer_stub: false
)
cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}"
cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}-#{prefer_stub}"
if factory_cached? && platform_cache[:formulary_factory]&.key?(cache_key)
return platform_cache[:formulary_factory][cache_key]
end
formula = loader_for(ref, from:, warn:)
.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
loader = FormulaStubLoader.try_new(ref, from:, warn:) if prefer_stub
loader ||= loader_for(ref, from:, warn:)
formula = loader.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
if factory_cached?
platform_cache[:formulary_factory] ||= {}
@ -1098,6 +1193,31 @@ module Formulary
.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
end
# Return a {Formula} instance directly from JSON contents.
sig {
params(
name: String,
contents: T::Hash[String, T.untyped],
spec: Symbol,
alias_path: T.nilable(Pathname),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def self.from_json_contents(
name,
contents,
spec = :stable,
alias_path: nil,
force_bottle: false,
flags: [],
ignore_errors: false
)
FormulaJSONContentsLoader.new(name, contents)
.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
end
def self.to_rack(ref)
# If using a fully-scoped reference, check if the formula can be resolved.
factory(ref) if ref.include? "/"

View File

@ -111,6 +111,9 @@ class Formula
sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) }
def loaded_from_api?(*args, &block); end
sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) }
def loaded_from_stub?(*args, &block); end
sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) }
def network_access_allowed?(*args, &block); end