diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index 6997ded4bb..bdae2457e1 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -60,13 +60,14 @@ module Cask source: T.nilable(String), tap: T.nilable(Tap), loaded_from_api: T::Boolean, + api_source: T.nilable(T::Hash[String, T.untyped]), config: T.nilable(Config), allow_reassignment: T::Boolean, loader: T.nilable(CaskLoader::ILoader), block: T.nilable(T.proc.bind(DSL).void), ).void } - def initialize(token, sourcefile_path: nil, source: nil, tap: nil, loaded_from_api: false, + def initialize(token, sourcefile_path: nil, source: nil, tap: nil, loaded_from_api: false, api_source: nil, config: nil, allow_reassignment: false, loader: nil, &block) @token = token @sourcefile_path = sourcefile_path @@ -74,6 +75,7 @@ module Cask @tap = tap @allow_reassignment = allow_reassignment @loaded_from_api = loaded_from_api + @api_source = api_source @loader = loader # Sorbet has trouble with bound procs assigned to instance variables: # https://github.com/sorbet/sorbet/issues/6843 @@ -91,6 +93,9 @@ module Cask sig { returns(T::Boolean) } def loaded_from_api? = @loaded_from_api + sig { returns(T.nilable(T::Hash[String, T.untyped])) } + attr_reader :api_source + # An old name for the cask. sig { returns(T::Array[String]) } def old_tokens @@ -407,8 +412,8 @@ module Cask private_constant :HASH_KEYS_TO_SKIP def to_hash_with_variations - if loaded_from_api? && !Homebrew::EnvConfig.no_install_from_api? - return api_to_local_hash(Homebrew::API::Cask.all_casks[token].dup) + if loaded_from_api? && (json_cask = api_source) && !Homebrew::EnvConfig.no_install_from_api? + return api_to_local_hash(json_cask.dup) end hash = to_h diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index d1903483aa..18666f40c4 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -336,6 +336,7 @@ module Cask cask_options = { loaded_from_api: true, + api_source: json_cask, sourcefile_path: @sourcefile_path, source: JSON.pretty_generate(json_cask), config:, 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/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index e08e8f90e4..106fd94625 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -533,5 +533,21 @@ RSpec.describe Cask::Cask, :cask do expect(h["artifacts"].first[:binary].first).to eq "$APPDIR/some/path" expect(h["caveats"]).to eq "$HOMEBREW_PREFIX and /$HOME\n" end + + context "when loaded from json file" do + let(:expected_json) { (TEST_FIXTURE_DIR/"cask/everything-with-variations.json").read.strip } + + it "returns expected hash with variations" do + expect(Homebrew::API::Cask).not_to receive(:source_download) + cask = Cask::CaskLoader::FromAPILoader.new("everything-with-variations", from_json: JSON.parse(expected_json)) + .load(config: nil) + + hash = cask.to_hash_with_variations + + expect(cask.loaded_from_api?).to be true + expect(hash).to be_a(Hash) + expect(JSON.pretty_generate(hash)).to eq(expected_json) + end + end end 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) diff --git a/Library/Homebrew/test/support/fixtures/cask/everything-with-variations.json b/Library/Homebrew/test/support/fixtures/cask/everything-with-variations.json new file mode 100644 index 0000000000..e8ba774113 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/everything-with-variations.json @@ -0,0 +1,122 @@ +{ + "token": "everything-with-variations", + "full_token": "everything-with-variations", + "old_tokens": [], + "tap": "homebrew/cask", + "name": [ + "Everything" + ], + "desc": "Little bit of everything", + "homepage": "https://www.everything.app/", + "url": "https://cachefly.everything.app/releases/Everything_1.2.3.zip", + "url_specs": { + "cookies": { + "ALL": "1234" + }, + "user_agent": ":fake" + }, + "version": "1.2.3", + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": false, + "installed": null, + "installed_time": null, + "bundle_version": null, + "bundle_short_version": null, + "outdated": false, + "sha256": "c64c05bdc0be845505d6e55e69e696a7f50d40846e76155f0c85d5ff5e7bbb84", + "artifacts": [ + { + "uninstall": [ + { + "launchctl": "com.every.thing.agent", + "signal": [ + [ + "TERM", + "com.every.thing.controller1" + ], + [ + "TERM", + "com.every.thing.bin" + ] + ], + "kext": "com.every.thing.driver", + "delete": "/Library/EverythingHelperTools" + } + ] + }, + { + "installer": [ + { + "script": { + "executable": "~/just/another/path/install.sh", + "args": [ + "--mode=silent" + ], + "sudo": true, + "print_stderr": false + } + } + ] + }, + { + "app": [ + "Everything.app" + ] + }, + { + "zap": [ + { + "trash": [ + "~/.everything", + "~/Library/Everything" + ] + } + ] + } + ], + "caveats": "Installing everything might take a while...\n\neverything requires a kernel extension to work.\nIf the installation fails, retry after you enable it in:\n System Settings → Privacy & Security\n\nFor more information, refer to vendor documentation or this Apple Technical Note:\n https://developer.apple.com/library/content/technotes/tn2459/_index.html\n", + "depends_on": { + "cask": [ + "something" + ], + "macos": { + ">=": [ + "10.11" + ] + } + }, + "conflicts_with": { + "formula": [ + "nothing" + ] + }, + "container": { + "type": "naked" + }, + "auto_updates": true, + "deprecated": false, + "deprecation_date": null, + "deprecation_reason": null, + "deprecation_replacement_formula": null, + "deprecation_replacement_cask": null, + "disabled": false, + "disable_date": null, + "disable_reason": null, + "disable_replacement_formula": null, + "disable_replacement_cask": null, + "tap_git_head": "abcdef1234567890abcdef1234567890abcdef12", + "languages": [ + "en", + "eo" + ], + "ruby_source_path": "Casks/everything-with-variations.rb", + "ruby_source_checksum": { + "sha256": "d8d0d6b2e5ff65388eccb82236fd3aa157b4a29bb043a1f72b97f0e9b70e8320" + }, + "variations": { + "arm64_monterey": { + "url": "https://cachefly.everything.app/releases/arm64_monterey/Everything_1.2.3.zip" + } + } +}