Merge pull request #16541 from apainintheneck/next-gen-api-formula-json-v3

Next gen api formula json v3
This commit is contained in:
Kevin 2024-02-04 11:16:01 -08:00 committed by GitHub
commit d94772171f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 259 additions and 144 deletions

View File

@ -46,7 +46,7 @@ module Homebrew
raise TapUnavailableError, tap.name unless tap.installed?
unless args.dry_run?
directories = ["_data/formula", "api/formula", "formula"]
directories = ["_data/formula", "api/formula", "formula", "api/internal/v3"]
FileUtils.rm_rf directories + ["_data/formula_canonical.json"]
FileUtils.mkdir_p directories
end
@ -58,6 +58,14 @@ module Homebrew
Formulary.enable_factory_cache!
Formula.generating_hash!
homebrew_core_tap_hash = {
"tap_git_head" => tap.git_head,
"aliases" => tap.alias_table,
"renames" => tap.formula_renames,
"tap_migrations" => tap.tap_migrations,
"formulae" => {},
}
tap.formula_names.each do |name|
formula = Formulary.factory(name)
name = formula.name
@ -69,11 +77,16 @@ module Homebrew
File.write("api/formula/#{name}.json", FORMULA_JSON_TEMPLATE)
File.write("formula/#{name}.html", html_template_name)
end
homebrew_core_tap_hash["formulae"][formula.name] =
formula.to_hash_with_variations(hash_method: :to_api_hash)
rescue
onoe "Error while generating data for formula '#{name}'."
raise
end
homebrew_core_tap_json = JSON.generate(homebrew_core_tap_hash)
File.write("api/internal/v3/homebrew-core.json", homebrew_core_tap_json) unless args.dry_run?
canonical_json = JSON.pretty_generate(tap.formula_renames.merge(tap.alias_table))
File.write("_data/formula_canonical.json", "#{canonical_json}\n") unless args.dry_run?
end

View File

@ -2214,15 +2214,6 @@ class Formula
# @private
def to_hash
# Create a hash of spec names (stable/head) to the list of dependencies under each
dependencies = self.class.spec_syms.to_h do |sym|
[sym, send(sym)&.declared_deps]
end
dependencies.transform_values! { |deps| deps&.reject(&:implicit?) } # Remove all implicit deps from all lists
requirements = self.class.spec_syms.to_h do |sym|
[sym, send(sym)&.requirements]
end
hsh = {
"name" => name,
"full_name" => full_name,
@ -2239,7 +2230,7 @@ class Formula
"head" => head&.version&.to_s,
"bottle" => bottle_defined?,
},
"urls" => {},
"urls" => urls_hash,
"revision" => revision,
"version_scheme" => version_scheme,
"bottle" => {},
@ -2254,12 +2245,11 @@ class Formula
"optional_dependencies" => [],
"uses_from_macos" => [],
"uses_from_macos_bounds" => [],
"requirements" => [],
"requirements" => serialized_requirements,
"conflicts_with" => conflicts.map(&:name),
"conflicts_with_reasons" => conflicts.map(&:reason),
"link_overwrite" => self.class.link_overwrite_paths.to_a,
"caveats" => caveats&.gsub(HOMEBREW_PREFIX, HOMEBREW_PREFIX_PLACEHOLDER)
&.gsub(HOMEBREW_CELLAR, HOMEBREW_CELLAR_PLACEHOLDER),
"caveats" => caveats_with_placeholders,
"installed" => [],
"linked_keg" => linked_version&.to_s,
"pinned" => pinned?,
@ -2271,46 +2261,265 @@ class Formula
"disable_date" => disable_date,
"disable_reason" => disable_reason,
"post_install_defined" => post_install_defined?,
"service" => (service.serialize if service?),
"service" => (service.to_hash if service?),
"tap_git_head" => tap_git_head,
"ruby_source_path" => ruby_source_path,
"ruby_source_checksum" => {},
}
hsh["bottle"]["stable"] = bottle_hash if stable && bottle_defined?
hsh["options"] = options.map do |opt|
{ "option" => opt.flag, "description" => opt.description }
end
hsh.merge!(dependencies_hash)
hsh["installed"] = installed_kegs.sort_by(&:version).map do |keg|
tab = Tab.for_keg keg
{
"version" => keg.version.to_s,
"used_options" => tab.used_options.as_flags,
"built_as_bottle" => tab.built_as_bottle,
"poured_from_bottle" => tab.poured_from_bottle,
"time" => tab.time,
"runtime_dependencies" => tab.runtime_dependencies,
"installed_as_dependency" => tab.installed_as_dependency,
"installed_on_request" => tab.installed_on_request,
}
end
if (source_checksum = ruby_source_checksum)
hsh["ruby_source_checksum"] = {
"sha256" => source_checksum.hexdigest,
}
end
hsh
end
# @private
def to_api_hash
api_hash = {
"desc" => desc,
"license" => SPDX.license_expression_to_string(license),
"homepage" => homepage,
"urls" => urls_hash.transform_values(&:compact),
"post_install_defined" => post_install_defined?,
"ruby_source_path" => ruby_source_path,
"ruby_source_sha256" => ruby_source_checksum&.hexdigest,
}
dep_hash = dependencies_hash
.except("recommended_dependencies", "optional_dependencies")
.transform_values(&:presence)
.compact
api_hash.merge!(dep_hash)
# Exclude default values.
api_hash["revision"] = revision unless revision.zero?
api_hash["version_scheme"] = version_scheme unless version_scheme.zero?
# Optional values.
api_hash["keg_only_reason"] = keg_only_reason.to_hash if keg_only_reason
api_hash["pour_bottle_only_if"] = self.class.pour_bottle_only_if.to_s if self.class.pour_bottle_only_if
api_hash["link_overwrite"] = self.class.link_overwrite_paths.to_a if self.class.link_overwrite_paths.present?
api_hash["caveats"] = caveats_with_placeholders if caveats
api_hash["service"] = service.to_hash if service?
if stable
api_hash["version"] = stable&.version&.to_s
api_hash["bottle"] = bottle_hash(compact_for_api: true) if bottle_defined?
end
if (versioned_formulae_list = versioned_formulae.presence)
# Could we just use `versioned_formulae_names` here instead?
api_hash["versioned_formulae"] = versioned_formulae_list.map(&:name)
end
if (requirements_array = serialized_requirements.presence)
api_hash["requirements"] = requirements_array
end
if conflicts.present?
api_hash["conflicts_with"] = conflicts.map(&:name)
api_hash["conflicts_with_reasons"] = conflicts.map(&:reason)
end
if deprecation_date
api_hash["deprecation_date"] = deprecation_date
api_hash["deprecation_reason"] = deprecation_reason
end
if disable_date
api_hash["disable_date"] = disable_date
api_hash["disable_reason"] = disable_reason
end
api_hash
end
# @private
def to_hash_with_variations(hash_method: :to_hash)
if loaded_from_api? && hash_method == :to_api_hash
raise ArgumentError, "API Hash must be generated from Ruby source files"
end
namespace_prefix = case hash_method
when :to_hash
"Variations"
when :to_api_hash
"APIVariations"
else
raise ArgumentError, "Unknown hash method #{hash_method.inspect}"
end
hash = public_send(hash_method)
# 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[name].dup
return json_formula.merge(
hash.slice("name", "installed", "linked_keg", "pinned", "outdated"),
)
end
variations = {}
if path.exist? && on_system_blocks_exist?
formula_contents = path.read
OnSystem::ALL_OS_ARCH_COMBINATIONS.each do |os, arch|
bottle_tag = Utils::Bottles::Tag.new(system: os, arch: arch)
next unless bottle_tag.valid_combination?
Homebrew::SimulateSystem.with os: os, arch: arch do
variations_namespace = Formulary.class_s("#{namespace_prefix}#{bottle_tag.to_sym.capitalize}")
variations_formula_class = Formulary.load_formula(name, path, formula_contents, variations_namespace,
flags: self.class.build_flags, ignore_errors: true)
variations_formula = variations_formula_class.new(name, path, :stable,
alias_path: alias_path, force_bottle: force_bottle)
variations_formula.public_send(hash_method).each do |key, value|
next if value.to_s == hash[key].to_s
variations[bottle_tag.to_sym] ||= {}
variations[bottle_tag.to_sym][key] = value
end
end
end
end
hash["variations"] = variations if hash_method != :to_api_hash || variations.present?
hash
end
# Returns the bottle information for a formula.
def bottle_hash(compact_for_api: false)
bottle_spec = T.must(stable).bottle_specification
hash = {}
hash["rebuild"] = bottle_spec.rebuild if !compact_for_api || !bottle_spec.rebuild.zero?
hash["root_url"] = bottle_spec.root_url unless compact_for_api
hash["files"] = {}
bottle_spec.collector.each_tag do |tag|
tag_spec = bottle_spec.collector.specification_for(tag, no_older_versions: true)
os_cellar = tag_spec.cellar
os_cellar = os_cellar.inspect if os_cellar.is_a?(Symbol)
checksum = tag_spec.checksum.hexdigest
file_hash = {}
file_hash["cellar"] = os_cellar
unless compact_for_api
filename = Bottle::Filename.create(self, tag, bottle_spec.rebuild)
path, = Utils::Bottles.path_resolved_basename(bottle_spec.root_url, name, checksum, filename)
file_hash["url"] = "#{bottle_spec.root_url}/#{path}"
end
file_hash["sha256"] = checksum
hash["files"][tag.to_sym] = file_hash
end
hash
end
# @private
def urls_hash
hash = {}
if stable
stable_spec = T.must(stable)
hsh["urls"]["stable"] = {
hash["stable"] = {
"url" => stable_spec.url,
"tag" => stable_spec.specs[:tag],
"revision" => stable_spec.specs[:revision],
"using" => (stable_spec.using if stable_spec.using.is_a?(Symbol)),
"checksum" => stable_spec.checksum&.to_s,
}
hsh["bottle"]["stable"] = bottle_hash if bottle_defined?
end
if head
hsh["urls"]["head"] = {
hash["head"] = {
"url" => T.must(head).url,
"branch" => T.must(head).specs[:branch],
"using" => (T.must(head).using if T.must(head).using.is_a?(Symbol)),
}
end
hsh["options"] = options.map do |opt|
{ "option" => opt.flag, "description" => opt.description }
hash
end
# @private
def serialized_requirements
requirements = self.class.spec_syms.to_h do |sym|
[sym, send(sym)&.requirements]
end
merge_spec_dependables(requirements).map do |data|
req = data[:dependable]
req_name = req.name.dup
req_name.prepend("maximum_") if req.respond_to?(:comparator) && req.comparator == "<="
req_version = if req.respond_to?(:version)
req.version
elsif req.respond_to?(:arch)
req.arch
end
{
"name" => req_name,
"cask" => req.cask,
"download" => req.download,
"version" => req_version,
"contexts" => req.tags,
"specs" => data[:specs],
}
end
end
# @private
def caveats_with_placeholders
caveats&.gsub(HOMEBREW_PREFIX, HOMEBREW_PREFIX_PLACEHOLDER)
&.gsub(HOMEBREW_CELLAR, HOMEBREW_CELLAR_PLACEHOLDER)
end
# @private
def dependencies_hash
# Create a hash of spec names (stable/head) to the list of dependencies under each
dependencies = self.class.spec_syms.to_h do |sym|
[sym, send(sym)&.declared_deps]
end
dependencies.transform_values! { |deps| deps&.reject(&:implicit?) } # Remove all implicit deps from all lists
hash = {}
dependencies.each do |spec_sym, spec_deps|
next if spec_deps.nil?
dep_hash = if spec_sym == :stable
hsh
hash
else
next if spec_deps == dependencies[:stable]
hsh["#{spec_sym}_dependencies"] ||= {}
hash["#{spec_sym}_dependencies"] ||= {}
end
dep_hash["build_dependencies"] = spec_deps.select(&:build?)
@ -2350,113 +2559,6 @@ class Formula
dep_hash["uses_from_macos_bounds"] = uses_from_macos_deps.map(&:bounds)
end
hsh["requirements"] = merge_spec_dependables(requirements).map do |data|
req = data[:dependable]
req_name = req.name.dup
req_name.prepend("maximum_") if req.respond_to?(:comparator) && req.comparator == "<="
req_version = if req.respond_to?(:version)
req.version
elsif req.respond_to?(:arch)
req.arch
end
{
"name" => req_name,
"cask" => req.cask,
"download" => req.download,
"version" => req_version,
"contexts" => req.tags,
"specs" => data[:specs],
}
end
hsh["installed"] = installed_kegs.sort_by(&:version).map do |keg|
tab = Tab.for_keg keg
{
"version" => keg.version.to_s,
"used_options" => tab.used_options.as_flags,
"built_as_bottle" => tab.built_as_bottle,
"poured_from_bottle" => tab.poured_from_bottle,
"time" => tab.time,
"runtime_dependencies" => tab.runtime_dependencies,
"installed_as_dependency" => tab.installed_as_dependency,
"installed_on_request" => tab.installed_on_request,
}
end
if (source_checksum = ruby_source_checksum)
hsh["ruby_source_checksum"] = {
"sha256" => source_checksum.hexdigest,
}
end
hsh
end
# @private
def to_hash_with_variations
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[name].dup
return json_formula.merge(
hash.slice("name", "installed", "linked_keg", "pinned", "outdated"),
)
end
variations = {}
if path.exist? && on_system_blocks_exist?
formula_contents = path.read
OnSystem::ALL_OS_ARCH_COMBINATIONS.each do |os, arch|
bottle_tag = Utils::Bottles::Tag.new(system: os, arch: arch)
next unless bottle_tag.valid_combination?
Homebrew::SimulateSystem.with os: os, arch: arch do
variations_namespace = Formulary.class_s("Variations#{bottle_tag.to_sym.capitalize}")
variations_formula_class = Formulary.load_formula(name, path, formula_contents, variations_namespace,
flags: self.class.build_flags, ignore_errors: true)
variations_formula = variations_formula_class.new(name, path, :stable,
alias_path: alias_path, force_bottle: force_bottle)
variations_formula.to_hash.each do |key, value|
next if value.to_s == hash[key].to_s
variations[bottle_tag.to_sym] ||= {}
variations[bottle_tag.to_sym][key] = value
end
end
end
end
hash["variations"] = variations
hash
end
# Returns the bottle information for a formula.
def bottle_hash
bottle_spec = T.must(stable).bottle_specification
hash = {
"rebuild" => bottle_spec.rebuild,
"root_url" => bottle_spec.root_url,
"files" => {},
}
bottle_spec.collector.each_tag do |tag|
tag_spec = bottle_spec.collector.specification_for(tag, no_older_versions: true)
os_cellar = tag_spec.cellar
os_cellar = os_cellar.inspect if os_cellar.is_a?(Symbol)
checksum = tag_spec.checksum.hexdigest
filename = Bottle::Filename.create(self, tag, bottle_spec.rebuild)
path, = Utils::Bottles.path_resolved_basename(bottle_spec.root_url, name, checksum, filename)
url = "#{bottle_spec.root_url}/#{path}"
hash["files"][tag.to_sym] = {
"cellar" => os_cellar,
"url" => url,
"sha256" => checksum,
}
end
hash
end

View File

@ -341,7 +341,7 @@ module Formulary
end
if (service_hash = json_formula["service"].presence)
service_hash = Homebrew::Service.deserialize(service_hash)
service_hash = Homebrew::Service.from_hash(service_hash)
service do
T.bind(self, Homebrew::Service)

View File

@ -515,7 +515,7 @@ module Homebrew
# Prepare the service hash for inclusion in the formula API JSON.
sig { returns(Hash) }
def serialize
def to_hash
name_params = {
macos: (plist_name if plist_name != default_plist_name),
linux: (service_name if service_name != default_service_name),
@ -568,7 +568,7 @@ module Homebrew
# Turn the service API hash values back into what is expected by the formula DSL.
sig { params(api_hash: Hash).returns(Hash) }
def self.deserialize(api_hash)
def self.from_hash(api_hash)
hash = {}
hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name")

View File

@ -738,7 +738,7 @@ describe Formula do
url "https://brew.sh/test-1.0.tbz"
end
expect(f.service.serialize).to eq({})
expect(f.service.to_hash).to eq({})
end
specify "service complicated" do
@ -754,7 +754,7 @@ describe Formula do
keep_alive true
end
end
expect(f.service.serialize.keys)
expect(f.service.to_hash.keys)
.to contain_exactly(:run, :run_type, :error_log_path, :log_path, :working_dir, :keep_alive)
end
@ -766,7 +766,7 @@ describe Formula do
end
end
expect(f.service.serialize.keys).to contain_exactly(:run, :run_type)
expect(f.service.to_hash.keys).to contain_exactly(:run, :run_type)
end
specify "service with only custom names" do
@ -779,7 +779,7 @@ describe Formula do
expect(f.plist_name).to eq("custom.macos.beanstalkd")
expect(f.service_name).to eq("custom.linux.beanstalkd")
expect(f.service.serialize.keys).to contain_exactly(:name)
expect(f.service.to_hash.keys).to contain_exactly(:name)
end
specify "service helpers return data" do

View File

@ -1044,7 +1044,7 @@ describe Homebrew::Service do
end
end
describe "#serialize" do
describe "#to_hash" do
let(:serialized_hash) do
{
environment_variables: {
@ -1072,12 +1072,12 @@ describe Homebrew::Service do
end
Formula.generating_hash!
expect(f.service.serialize).to eq(serialized_hash)
expect(f.service.to_hash).to eq(serialized_hash)
Formula.generated_hash!
end
end
describe ".deserialize" do
describe ".from_hash" do
let(:serialized_hash) do
{
"name" => {
@ -1111,12 +1111,12 @@ describe Homebrew::Service do
end
it "replaces placeholders with local paths" do
expect(described_class.deserialize(serialized_hash)).to eq(deserialized_hash)
expect(described_class.from_hash(serialized_hash)).to eq(deserialized_hash)
end
describe "run command" do
it "handles String argument correctly" do
expect(described_class.deserialize({
expect(described_class.from_hash({
"run" => "$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd",
})).to eq({
run: "#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd",
@ -1124,7 +1124,7 @@ describe Homebrew::Service do
end
it "handles Array argument correctly" do
expect(described_class.deserialize({
expect(described_class.from_hash({
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "--option"],
})).to eq({
run: ["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "--option"],
@ -1132,7 +1132,7 @@ describe Homebrew::Service do
end
it "handles Hash argument correctly" do
expect(described_class.deserialize({
expect(described_class.from_hash({
"run" => {
"linux" => "$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd",
"macos" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "--option"],