Merge pull request #20425 from Homebrew/internal-api-helper

Create `Homebrew::API::Internal` for working with internal API
This commit is contained in:
Mike McQuaid 2025-08-12 07:32:18 +00:00 committed by GitHub
commit b8c82b44b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 390 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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