diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 2d1c3c9362..c70ff74e94 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -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 diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 52728158a5..d57a8606ca 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -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? "/" diff --git a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi index 7a0cd62a4e..83076fb032 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi @@ -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