diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index e023501b22..80f292bfd9 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -557,6 +557,12 @@ class Formula # @see .loaded_from_api? delegate loaded_from_api?: :"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 { void } def update_head_version return unless head? @@ -2625,9 +2631,8 @@ class Formula hash = 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.fetch(name).dup - return json_formula.merge( + if loaded_from_api? && (json_formula = api_source) && !Homebrew::EnvConfig.no_install_from_api? + return json_formula.dup.merge( hash.slice("name", "installed", "linked_keg", "pinned", "outdated"), ) end @@ -3360,6 +3365,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)) + @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| [phase, DEFAULT_NETWORK_ACCESS_ALLOWED] @@ -3384,6 +3390,10 @@ class Formula sig { returns(T::Boolean) } def loaded_from_api? = !!@loaded_from_api + # Whether this formula was loaded using the formulae.brew.sh API. + sig { returns(T.nilable(T::Hash[String, T.untyped])) } + attr_reader :api_source + # Whether this formula contains OS/arch-specific blocks # (e.g. `on_macos`, `on_arm`, `on_monterey :or_older`, `on_system :linux, macos: :big_sur_or_newer`). sig { returns(T::Boolean) } diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index f416dad77c..b436f80030 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -173,9 +173,9 @@ module Formulary platform_cache[:path][path] = klass end - sig { params(name: String, flags: T::Array[String]).returns(T.class_of(Formula)) } - def self.load_formula_from_api!(name, flags:) - namespace = :"FormulaNamespaceAPI#{namespace_key(name)}" + sig { params(name: String, json_formula_with_variations: T::Hash[String, T.untyped], flags: T::Array[String]).returns(T.class_of(Formula)) } + def self.load_formula_from_json!(name, json_formula_with_variations, flags:) + namespace = :"FormulaNamespaceAPI#{namespace_key(json_formula_with_variations.to_json)}" mod = Module.new remove_const(namespace) if const_defined?(namespace) @@ -184,10 +184,7 @@ module Formulary mod.const_set(:BUILD_FLAGS, flags) class_name = class_s(name) - json_formula = Homebrew::API::Formula.all_formulae[name] - raise FormulaUnavailableError, name if json_formula.nil? - - json_formula = Homebrew::API.merge_variations(json_formula) + json_formula = Homebrew::API.merge_variations(json_formula_with_variations) uses_from_macos_names = json_formula.fetch("uses_from_macos", []).map do |dep| next dep unless dep.is_a? Hash @@ -273,6 +270,7 @@ module Formulary # rubocop:todo Sorbet/BlockMethodDefinition klass = Class.new(::Formula) do @loaded_from_api = true + @api_source = json_formula_with_variations desc json_formula["desc"] homepage json_formula["homepage"] @@ -911,7 +909,10 @@ module Formulary private def load_from_api(flags:) - Formulary.load_formula_from_api!(name, flags:) + json_formula = Homebrew::API::Formula.all_formulae[name] + raise FormulaUnavailableError, name if json_formula.nil? + + Formulary.load_formula_from_json!(name, json_formula, flags:) end end diff --git a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi index fedb315954..7a0cd62a4e 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi @@ -9,6 +9,9 @@ class Formula sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def allow_network_access!(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def api_source(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) } def autobump?(*args, &block); end diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 64897d7c30..c2457856d4 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -292,8 +292,12 @@ RSpec.describe Formulary do def formula_json_contents(extra_items = {}) { formula_name => { + "name" => formula_name, "desc" => "testball", "homepage" => "https://example.com", + "installed" => [], + "outdated" => false, + "pinned" => false, "license" => "MIT", "revision" => 0, "version_scheme" => 0, @@ -340,6 +344,7 @@ RSpec.describe Formulary do "conflicts_with" => ["conflicting_formula"], "conflicts_with_reasons" => ["it does"], "link_overwrite" => ["bin/abc"], + "linked_keg" => nil, "caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX", "service" => { "name" => { macos: "custom.launchd.name", linux: "custom.systemd.name" }, @@ -447,6 +452,17 @@ RSpec.describe Formulary do end.to raise_error("Cannot build from source from abstract formula.") end + it "returns a Formula that can regenerate its JSON API" do + allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents + + formula = described_class.factory(formula_name) + expect(formula).to be_a(Formula) + expect(formula.loaded_from_api?).to be true + + expected_hash = formula_json_contents[formula_name] + expect(formula.to_hash_with_variations).to eq(expected_hash) + end + it "returns a deprecated Formula when given a name" do allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(deprecate_json)