Skip to content

Commit 472cb0a

Browse files
committed
Use FormulaStruct when loading formulae from the API
1 parent 4708b2f commit 472cb0a

File tree

10 files changed

+491
-198
lines changed

10 files changed

+491
-198
lines changed

Library/Homebrew/api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "api/cask"
66
require "api/formula"
77
require "api/internal"
8+
require "api/formula_struct"
89
require "base64"
910
require "utils/output"
1011

Library/Homebrew/api/formula.rb

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "api"
66
require "api/source_download"
77
require "download_queue"
8+
require "autobump_constants"
89

910
module Homebrew
1011
module API
@@ -157,6 +158,148 @@ def self.write_names_and_aliases(regenerate: false)
157158
Homebrew::API.write_names_file!(all_formulae.keys, "formula", regenerate:)
158159
Homebrew::API.write_aliases_file!(all_aliases, "formula", regenerate:)
159160
end
161+
162+
sig { params(hash: T::Hash[String, T.untyped]).returns(FormulaStruct) }
163+
def self.generate_formula_struct_hash(hash)
164+
hash = Homebrew::API.merge_variations(hash).dup
165+
166+
if (caveats = hash["caveats"])
167+
hash["caveats"] = Formulary.replace_placeholders(caveats)
168+
end
169+
170+
hash["bottle_checksums"] = begin
171+
files = hash.dig("bottle", "stable", "files") || {}
172+
files.map do |tag, bottle_spec|
173+
{
174+
cellar: Formulary.convert_to_string_or_symbol(bottle_spec.fetch("cellar")),
175+
tag.to_sym => bottle_spec.fetch("sha256"),
176+
}
177+
end
178+
end
179+
180+
hash["bottle_rebuild"] = hash.dig("bottle", "stable", "rebuild")
181+
182+
conflicts_with = hash["conflicts_with"] || []
183+
conflicts_with_reasons = hash["conflicts_with_reasons"] || []
184+
hash["conflicts"] = conflicts_with.zip(conflicts_with_reasons).map do |name, reason|
185+
if reason.present?
186+
[name, { because: reason }]
187+
else
188+
[name, {}]
189+
end
190+
end
191+
192+
if (deprecation_date = hash["deprecation_date"])
193+
hash["deprecate_args"] = {
194+
date: deprecation_date,
195+
because: DeprecateDisable.to_reason_string_or_symbol(hash["deprecation_reason"],
196+
type: :formula),
197+
replacement_formula: hash["deprecation_replacement_formula"],
198+
replacement_cask: hash["deprecation_replacement_cask"],
199+
}
200+
end
201+
202+
if (disable_date = hash["disable_date"])
203+
hash["disable_args"] = {
204+
date: disable_date,
205+
because: DeprecateDisable.to_reason_string_or_symbol(hash["disable_reason"], type: :formula),
206+
replacement_formula: hash["disable_replacement_formula"],
207+
replacement_cask: hash["disable_replacement_cask"],
208+
}
209+
end
210+
211+
hash["head_dependency_hash"] = hash["head_dependencies"]
212+
213+
hash["head_url_args"] = begin
214+
url = hash.dig("urls", "head", "url")
215+
specs = {
216+
branch: hash.dig("urls", "head", "branch"),
217+
using: hash.dig("urls", "head", "using")&.to_sym,
218+
}.compact_blank
219+
[url, specs]
220+
end
221+
222+
if (keg_only_hash = hash["keg_only_reason"])
223+
reason = Formulary.convert_to_string_or_symbol(keg_only_hash.fetch("reason"))
224+
explanation = keg_only_hash["explanation"]
225+
hash["keg_only_args"] = [reason, explanation].compact
226+
end
227+
228+
hash["license"] = SPDX.string_to_license_expression(hash["license"])
229+
230+
hash["link_overwrite_paths"] = hash["link_overwrite"]
231+
232+
if (reason = hash["no_autobump_message"])
233+
reason = reason.to_sym if NO_AUTOBUMP_REASONS_LIST.key?(reason.to_sym)
234+
hash["no_autobump_args"] = { because: reason }
235+
end
236+
237+
if (condition = hash["pour_bottle_only_if"])
238+
hash["pour_bottle_args"] = { only_if: condition.to_sym }
239+
end
240+
241+
hash["requirements_array"] = hash["requirements"]
242+
243+
hash["ruby_source_checksum"] = hash.dig("ruby_source_checksum", "sha256")
244+
245+
if (service_hash = hash["service"])
246+
service_hash = Homebrew::Service.from_hash(service_hash)
247+
248+
hash["service_run_args"], hash["service_run_kwargs"] = case (run = service_hash[:run])
249+
when Hash
250+
[[], run]
251+
when Array, String
252+
[[run], {}]
253+
else
254+
[[], {}]
255+
end
256+
257+
hash["service_name_args"] = service_hash[:name]
258+
259+
hash["service_args"] = service_hash.filter_map do |key, arg|
260+
[key.to_sym, arg] if key != :name && key != :run
261+
end
262+
end
263+
264+
hash["stable_checksum"] = hash.dig("urls", "stable", "checksum")
265+
266+
hash["stable_dependency_hash"] = {
267+
"dependencies" => hash["dependencies"] || [],
268+
"build_dependencies" => hash["build_dependencies"] || [],
269+
"test_dependencies" => hash["test_dependencies"] || [],
270+
"recommended_dependencies" => hash["recommended_dependencies"] || [],
271+
"optional_dependencies" => hash["optional_dependencies"] || [],
272+
"uses_from_macos" => hash["uses_from_macos"] || [],
273+
"uses_from_macos_bounds" => hash["uses_from_macos_bounds"] || [],
274+
}
275+
276+
hash["stable_url_args"] = begin
277+
url = hash.dig("urls", "stable", "url")
278+
specs = {
279+
tag: hash.dig("urls", "stable", "tag"),
280+
revision: hash.dig("urls", "stable", "revision"),
281+
using: hash.dig("urls", "stable", "using")&.to_sym,
282+
}.compact_blank
283+
[url, specs]
284+
end
285+
286+
hash["stable_version"] = hash.dig("versions", "stable")
287+
288+
# Should match FormulaStruct::PREDICATES
289+
hash["bottle_present"] = hash["bottle"].present?
290+
hash["deprecated_present"] = hash["deprecation_date"].present?
291+
hash["disabled_present"] = hash["disable_date"].present?
292+
hash["head_present"] = hash.dig("urls", "head").present?
293+
hash["keg_only_present"] = hash["keg_only_reason"].present?
294+
hash["no_autobump_message_present"] = hash["no_autobump_message"].present?
295+
hash["pour_bottle_present"] = hash["pour_bottle_only_if"].present?
296+
hash["service_present"] = hash["service"].present?
297+
hash["service_run_present"] = hash.dig("service", "run").present?
298+
hash["service_name_present"] = hash.dig("service", "name").present?
299+
hash["stable_present"] = hash.dig("urls", "stable").present?
300+
301+
FormulaStruct.from_hash(hash)
302+
end
160303
end
161304
end
162305
end
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "service"
5+
require "utils/spdx"
6+
7+
module Homebrew
8+
module API
9+
class FormulaStruct < T::Struct
10+
PREDICATES = [
11+
:bottle,
12+
:deprecated,
13+
:disabled,
14+
:head,
15+
:keg_only,
16+
:no_autobump_message,
17+
:pour_bottle,
18+
:service,
19+
:service_run,
20+
:service_name,
21+
:stable,
22+
].freeze
23+
24+
# `:codesign` and custom requirement classes are not supported.
25+
API_SUPPORTED_REQUIREMENTS = [:arch, :linux, :macos, :maximum_macos, :xcode].freeze
26+
private_constant :API_SUPPORTED_REQUIREMENTS
27+
28+
DependencyArgs = T.type_alias do
29+
T.any(
30+
# Formula name: "foo"
31+
String,
32+
# Formula name and dependency type: { "foo" => :build }
33+
T::Hash[String, Symbol],
34+
)
35+
end
36+
37+
RequirementArgs = T.type_alias do
38+
T.any(
39+
# Requirement name: :macos
40+
Symbol,
41+
# Requirement name and other info: { macos: :build }
42+
T::Hash[Symbol, T::Array[T.anything]],
43+
)
44+
end
45+
46+
UsesFromMacOSArgs = T.type_alias do
47+
[
48+
T.any(
49+
# Formula name: "foo"
50+
String,
51+
# Formula name and dependency type: { "foo" => :build }
52+
# Formula name, dependency type, and version bounds: { "foo" => :build, since: :catalina }
53+
T::Hash[T.any(String, Symbol), T.any(Symbol, T::Array[Symbol])],
54+
),
55+
# If the first argument is only a name, this argument contains the version bounds: { since: :catalina }
56+
T::Hash[Symbol, Symbol],
57+
]
58+
end
59+
60+
PREDICATES.each do |predicate_name|
61+
present_method_name = :"#{predicate_name}_present"
62+
predicate_method_name = :"#{predicate_name}?"
63+
64+
const present_method_name, T::Boolean, default: false
65+
66+
define_method(predicate_method_name) do
67+
send(present_method_name)
68+
end
69+
end
70+
71+
# Changes to this struct must be mirrored in Homebrew::API::Formula.generate_formula_struct_hash
72+
const :aliases, T::Array[String], default: []
73+
const :bottle, T::Hash[String, T.anything], default: {}
74+
const :bottle_checksums, T::Array[T::Hash[String, T.anything]], default: []
75+
const :bottle_rebuild, Integer, default: 0
76+
const :caveats, T.nilable(String)
77+
const :conflicts, T::Array[[String, T::Hash[Symbol, String]]], default: []
78+
const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
79+
const :desc, String
80+
const :disable_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
81+
const :head_url_args, [String, T::Hash[Symbol, T.anything]]
82+
const :homepage, String
83+
const :keg_only_args, T::Array[T.any(String, Symbol)], default: []
84+
const :license, SPDX::LicenseExpression
85+
const :link_overwrite_paths, T::Array[String], default: []
86+
const :no_autobump_args, T::Hash[Symbol, T.any(String, Symbol)], default: {}
87+
const :oldnames, T::Array[String], default: []
88+
const :post_install_defined, T::Boolean, default: true
89+
const :pour_bottle_args, T::Hash[Symbol, Symbol], default: {}
90+
const :revision, Integer, default: 0
91+
const :ruby_source_checksum, String
92+
const :ruby_source_path, String
93+
const :service_args, T::Array[[Symbol, BasicObject]], default: []
94+
const :service_name_args, T::Hash[Symbol, String], default: {}
95+
const :service_run_args, T::Array[Homebrew::Service::RunParam], default: []
96+
const :service_run_kwargs, T::Hash[Symbol, Homebrew::Service::RunParam], default: {}
97+
const :stable_checksum, T.nilable(String)
98+
const :stable_url_args, [String, T::Hash[Symbol, T.anything]]
99+
const :stable_version, String
100+
const :tap_git_head, String
101+
const :version_scheme, Integer, default: 0
102+
const :versioned_formulae, T::Array[String], default: []
103+
104+
sig { returns(T::Array[T.any(DependencyArgs, RequirementArgs)]) }
105+
def head_dependencies
106+
spec_dependencies(:head) + spec_requirements(:head)
107+
end
108+
109+
sig { returns(T::Array[T.any(DependencyArgs, RequirementArgs)]) }
110+
def stable_dependencies
111+
spec_dependencies(:stable) + spec_requirements(:stable)
112+
end
113+
114+
sig { returns(T::Array[UsesFromMacOSArgs]) }
115+
def head_uses_from_macos
116+
spec_uses_from_macos(:head)
117+
end
118+
119+
sig { returns(T::Array[UsesFromMacOSArgs]) }
120+
def stable_uses_from_macos
121+
spec_uses_from_macos(:stable)
122+
end
123+
124+
private
125+
126+
const :stable_dependency_hash, T::Hash[String, T::Array[String]], default: {}
127+
const :head_dependency_hash, T::Hash[String, T::Array[String]], default: {}
128+
const :requirements_array, T::Array[T::Hash[String, T.untyped]], default: []
129+
130+
sig { params(spec: Symbol).returns(T::Array[DependencyArgs]) }
131+
def spec_dependencies(spec)
132+
deps_hash = send("#{spec}_dependency_hash")
133+
dependencies = deps_hash.fetch("dependencies", [])
134+
dependencies + [:build, :test, :recommended, :optional].filter_map do |type|
135+
deps_hash["#{type}_dependencies"]&.map do |dep|
136+
{ dep => type }
137+
end
138+
end.flatten(1)
139+
end
140+
141+
sig { params(spec: Symbol).returns(T::Array[UsesFromMacOSArgs]) }
142+
def spec_uses_from_macos(spec)
143+
deps_hash = send("#{spec}_dependency_hash")
144+
zipped_array = deps_hash["uses_from_macos"]&.zip(deps_hash["uses_from_macos_bounds"])
145+
return [] unless zipped_array
146+
147+
zipped_array.map do |entry, bounds|
148+
bounds ||= {}
149+
bounds = bounds.transform_keys(&:to_sym).transform_values(&:to_sym)
150+
151+
if entry.is_a?(Hash)
152+
# The key is the dependency name, the value is the dep type. Only the type should be a symbol
153+
entry = entry.deep_transform_values(&:to_sym)
154+
# When passing both a dep type and bounds, uses_from_macos expects them both in the first argument
155+
entry = entry.merge(bounds)
156+
[entry, {}]
157+
else
158+
[entry, bounds]
159+
end
160+
end
161+
end
162+
163+
sig { params(spec: Symbol).returns(T::Array[RequirementArgs]) }
164+
def spec_requirements(spec)
165+
requirements_array.filter_map do |req|
166+
next unless req["specs"].include?(spec.to_s)
167+
168+
req_name = req["name"].to_sym
169+
next if API_SUPPORTED_REQUIREMENTS.exclude?(req_name)
170+
171+
req_version = case req_name
172+
when :arch
173+
req["version"]&.to_sym
174+
when :macos, :maximum_macos
175+
MacOSVersion::SYMBOLS.key(req["version"])
176+
else
177+
req["version"]
178+
end
179+
180+
req_tags = []
181+
req_tags << req_version if req_version.present?
182+
req_tags += req["contexts"]&.map do |tag|
183+
case tag
184+
when String
185+
tag.to_sym
186+
when Hash
187+
tag.deep_transform_keys(&:to_sym)
188+
else
189+
tag
190+
end
191+
end
192+
193+
if req_tags.empty?
194+
req_name
195+
else
196+
{ req_name => req_tags }
197+
end
198+
end
199+
end
200+
end
201+
end
202+
end

Library/Homebrew/dependency_collector.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def parse_string_spec(spec, tags)
160160

161161
def parse_symbol_spec(spec, tags)
162162
# When modifying this list of supported requirements, consider
163-
# whether `Formulary::API_SUPPORTED_REQUIREMENTS` should also be changed.
163+
# whether `FormulaStruct::API_SUPPORTED_REQUIREMENTS` should also be changed.
164164
case spec
165165
when :arch then ArchRequirement.new(tags)
166166
when :codesign then CodesignRequirement.new(tags)

0 commit comments

Comments
 (0)