From cf450d99486c924be1dd42751579302159e05f01 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 12 Aug 2025 02:38:16 -0400 Subject: [PATCH] Create `Homebrew::API::Internal` Add type aliase and fix cask content issue --- Library/Homebrew/api/internal.rb | 177 ++++++++++++++++++++ Library/Homebrew/formula_stub.rb | 34 ++++ Library/Homebrew/test/api/internal_spec.rb | 179 +++++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 Library/Homebrew/api/internal.rb create mode 100644 Library/Homebrew/formula_stub.rb create mode 100644 Library/Homebrew/test/api/internal_spec.rb diff --git a/Library/Homebrew/api/internal.rb b/Library/Homebrew/api/internal.rb new file mode 100644 index 0000000000..c7b83e2193 --- /dev/null +++ b/Library/Homebrew/api/internal.rb @@ -0,0 +1,177 @@ +# typed: strict +# frozen_string_literal: true + +require "cachable" +require "api" +require "api/source_download" +require "download_queue" +require "formula_stub" + +module Homebrew + module API + # Helper functions for using the JSON internal API. + module Internal + extend Cachable + + private_class_method :cache + + sig { returns(String) } + def self.formula_endpoint + "internal/formula.#{SimulateSystem.current_tag}.jws.json" + end + + sig { returns(String) } + def self.cask_endpoint + "internal/cask.#{SimulateSystem.current_tag}.jws.json" + end + + sig { params(name: String).returns(Homebrew::FormulaStub) } + def self.formula_stub(name) + return cache["formula_stubs"][name] if cache.key?("formula_stubs") && cache["formula_stubs"].key?(name) + + stub_array = formula_arrays[name] + raise "No formula stub found for #{name}" unless stub_array + + stub = Homebrew::FormulaStub.new( + name: name, + pkg_version: PkgVersion.parse(stub_array[0]), + rebuild: stub_array[1], + sha256: stub_array[2], + ) + + cache["formula_stubs"] ||= {} + cache["formula_stubs"][name] = stub + + stub + end + + sig { + params(download_queue: T.nilable(Homebrew::DownloadQueue), stale_seconds: Integer) + .returns([T::Hash[String, T.untyped], T::Boolean]) + } + def self.fetch_formula_api!(download_queue: nil, stale_seconds: Homebrew::EnvConfig.api_auto_update_secs.to_i) + json_contents, updated = (Homebrew::API.fetch_json_api_file formula_endpoint, stale_seconds:, download_queue:) + [T.cast(json_contents, T::Hash[String, T.untyped]), updated] + end + + sig { + params(download_queue: T.nilable(Homebrew::DownloadQueue), stale_seconds: Integer) + .returns([T::Hash[String, T.untyped], T::Boolean]) + } + def self.fetch_cask_api!(download_queue: nil, stale_seconds: Homebrew::EnvConfig.api_auto_update_secs.to_i) + json_contents, updated = (Homebrew::API.fetch_json_api_file cask_endpoint, stale_seconds:, download_queue:) + [T.cast(json_contents, T::Hash[String, T.untyped]), updated] + end + + sig { returns(T::Boolean) } + def self.download_and_cache_formula_data! + json_contents, updated = fetch_formula_api! + cache["formula_stubs"] = {} + cache["formula_aliases"] = json_contents["aliases"] + cache["formula_renames"] = json_contents["renames"] + cache["formula_tap_migrations"] = json_contents["tap_migrations"] + cache["formula_arrays"] = json_contents["formulae"] + + updated + end + private_class_method :download_and_cache_formula_data! + + sig { returns(T::Boolean) } + def self.download_and_cache_cask_data! + json_contents, updated = fetch_cask_api! + cache["cask_stubs"] = {} + cache["cask_renames"] = json_contents["renames"] + cache["cask_tap_migrations"] = json_contents["tap_migrations"] + cache["cask_hashes"] = json_contents["casks"] + + updated + end + private_class_method :download_and_cache_cask_data! + + sig { params(regenerate: T::Boolean).void } + def self.write_formula_names_and_aliases(regenerate: false) + download_and_cache_formula_data! unless cache.key?("formula_arrays") + + Homebrew::API.write_names_file!(formula_arrays.keys, "formula", regenerate:) + Homebrew::API.write_aliases_file!(formula_aliases, "formula", regenerate:) + end + + sig { params(regenerate: T::Boolean).void } + def self.write_cask_names(regenerate: false) + download_and_cache_cask_data! unless cache.key?("cask_hashes") + + Homebrew::API.write_names_file!(cask_hashes.keys, "cask", regenerate:) + end + + sig { returns(T::Hash[String, [String, Integer, T.nilable(String)]]) } + def self.formula_arrays + unless cache.key?("formula_arrays") + updated = download_and_cache_formula_data! + write_formula_names_and_aliases(regenerate: updated) + end + + cache["formula_arrays"] + end + + sig { returns(T::Hash[String, String]) } + def self.formula_aliases + unless cache.key?("formula_aliases") + updated = download_and_cache_formula_data! + write_formula_names_and_aliases(regenerate: updated) + end + + cache["formula_aliases"] + end + + sig { returns(T::Hash[String, String]) } + def self.formula_renames + unless cache.key?("formula_renames") + updated = download_and_cache_formula_data! + write_formula_names_and_aliases(regenerate: updated) + end + + cache["formula_renames"] + end + + sig { returns(T::Hash[String, String]) } + def self.formula_tap_migrations + unless cache.key?("formula_tap_migrations") + updated = download_and_cache_formula_data! + write_formula_names_and_aliases(regenerate: updated) + end + + cache["formula_tap_migrations"] + end + + sig { returns(T::Hash[String, T::Hash[String, T.untyped]]) } + def self.cask_hashes + unless cache.key?("cask_hashes") + updated = download_and_cache_cask_data! + write_cask_names(regenerate: updated) + end + + cache["cask_hashes"] + end + + sig { returns(T::Hash[String, String]) } + def self.cask_renames + unless cache.key?("cask_renames") + updated = download_and_cache_cask_data! + write_cask_names(regenerate: updated) + end + + cache["cask_renames"] + end + + sig { returns(T::Hash[String, String]) } + def self.cask_tap_migrations + unless cache.key?("cask_tap_migrations") + updated = download_and_cache_cask_data! + write_cask_names(regenerate: updated) + end + + cache["cask_tap_migrations"] + end + end + end +end diff --git a/Library/Homebrew/formula_stub.rb b/Library/Homebrew/formula_stub.rb new file mode 100644 index 0000000000..0177ef6bdc --- /dev/null +++ b/Library/Homebrew/formula_stub.rb @@ -0,0 +1,34 @@ +# typed: strict +# frozen_string_literal: true + +require "pkg_version" + +module Homebrew + # A stub for a formula, with only the information needed to fetch the bottle manifest. + class FormulaStub < T::Struct + const :name, String + const :pkg_version, PkgVersion + const :rebuild, Integer, default: 0 + const :sha256, T.nilable(String) + + sig { returns(Version) } + def version + pkg_version.version + end + + sig { returns(Integer) } + def revision + pkg_version.revision + end + + sig { params(other: T.anything).returns(T::Boolean) } + def ==(other) + case other + when FormulaStub + name == other.name && pkg_version == other.pkg_version && rebuild == other.rebuild && sha256 == other.sha256 + else + false + end + end + end +end diff --git a/Library/Homebrew/test/api/internal_spec.rb b/Library/Homebrew/test/api/internal_spec.rb new file mode 100644 index 0000000000..13e6bed6b0 --- /dev/null +++ b/Library/Homebrew/test/api/internal_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "api/internal" + +RSpec.describe Homebrew::API::Internal do + let(:cache_dir) { mktmpdir } + + before do + FileUtils.mkdir_p(cache_dir/"internal") + stub_const("Homebrew::API::HOMEBREW_CACHE_API", cache_dir) + end + + def mock_curl_download(stdout:) + allow(Utils::Curl).to receive(:curl_download) do |*_args, **kwargs| + kwargs[:to].write stdout + end + allow(Homebrew::API).to receive(:verify_and_parse_jws) do |json_data| + [true, json_data] + end + end + + context "for formulae" do + let(:formula_json) do + <<~JSON + { + "formulae": { + "foo": ["1.0.0", 0, "09f88b61e36045188ddb1b1ba8e402b9f3debee1770cc4ca91355eeccb5f4a38"], + "bar": ["0.4.0_5", 0, "bb6e3408f39a404770529cfce548dc2666e861077acd173825cb3138c27c205a"], + "baz": ["10.4.5_2", 2, "404c97537d65ca0b75c389e7d439dcefb9b56f34d3b98017669eda0d0501add7"] + }, + "aliases": { + "foo-alias1": "foo", + "foo-alias2": "foo", + "bar-alias": "bar" + }, + "renames": { + "foo-old": "foo", + "bar-old": "bar", + "baz-old": "baz" + }, + "tap_migrations": { + "abc": "some/tap", + "def": "another/tap" + } + } + JSON + end + let(:formula_arrays) do + { + "foo" => ["1.0.0", 0, "09f88b61e36045188ddb1b1ba8e402b9f3debee1770cc4ca91355eeccb5f4a38"], + "bar" => ["0.4.0_5", 0, "bb6e3408f39a404770529cfce548dc2666e861077acd173825cb3138c27c205a"], + "baz" => ["10.4.5_2", 2, "404c97537d65ca0b75c389e7d439dcefb9b56f34d3b98017669eda0d0501add7"], + } + end + let(:formula_stubs) do + formula_arrays.to_h do |name, (pkg_version, rebuild, sha256)| + stub = Homebrew::FormulaStub.new( + name: name, + pkg_version: PkgVersion.parse(pkg_version), + rebuild: rebuild, + sha256: sha256, + ) + [name, stub] + end + end + let(:formulae_aliases) do + { + "foo-alias1" => "foo", + "foo-alias2" => "foo", + "bar-alias" => "bar", + } + end + let(:formulae_renames) do + { + "foo-old" => "foo", + "bar-old" => "bar", + "baz-old" => "baz", + } + end + let(:formula_tap_migrations) do + { + "abc" => "some/tap", + "def" => "another/tap", + } + end + + it "returns the expected formula stubs" do + mock_curl_download stdout: formula_json + formula_stubs.each do |name, stub| + expect(described_class.formula_stub(name)).to eq stub + end + end + + it "returns the expected formula arrays" do + mock_curl_download stdout: formula_json + formula_arrays_output = described_class.formula_arrays + expect(formula_arrays_output).to eq formula_arrays + end + + it "returns the expected formula alias list" do + mock_curl_download stdout: formula_json + formula_aliases_output = described_class.formula_aliases + expect(formula_aliases_output).to eq formulae_aliases + end + + it "returns the expected formula rename list" do + mock_curl_download stdout: formula_json + formula_renames_output = described_class.formula_renames + expect(formula_renames_output).to eq formulae_renames + end + + it "returns the expected formula tap migrations list" do + mock_curl_download stdout: formula_json + formula_tap_migrations_output = described_class.formula_tap_migrations + expect(formula_tap_migrations_output).to eq formula_tap_migrations + end + end + + context "for casks" do + let(:cask_json) do + <<~JSON + { + "casks": { + "foo": { "version": "1.0.0" }, + "bar": { "version": "0.4.0" }, + "baz": { "version": "10.4.5" } + }, + "renames": { + "foo-old": "foo", + "bar-old": "bar", + "baz-old": "baz" + }, + "tap_migrations": { + "abc": "some/tap", + "def": "another/tap" + } + } + JSON + end + let(:cask_hashes) do + { + "foo" => { "version" => "1.0.0" }, + "bar" => { "version" => "0.4.0" }, + "baz" => { "version" => "10.4.5" }, + } + end + let(:cask_renames) do + { + "foo-old" => "foo", + "bar-old" => "bar", + "baz-old" => "baz", + } + end + let(:cask_tap_migrations) do + { + "abc" => "some/tap", + "def" => "another/tap", + } + end + + it "returns the expected cask hashes" do + mock_curl_download stdout: cask_json + cask_hashes_output = described_class.cask_hashes + expect(cask_hashes_output).to eq cask_hashes + end + + it "returns the expected cask rename list" do + mock_curl_download stdout: cask_json + cask_renames_output = described_class.cask_renames + expect(cask_renames_output).to eq cask_renames + end + + it "returns the expected cask tap migrations list" do + mock_curl_download stdout: cask_json + cask_tap_migrations_output = described_class.cask_tap_migrations + expect(cask_tap_migrations_output).to eq cask_tap_migrations + end + end +end