Skip to content

Commit c8fbbb7

Browse files
authored
Merge pull request #21456 from Homebrew/serialize-formula-structs
Serialize `FormulaStruct`s in the internal API
2 parents b172481 + ad57998 commit c8fbbb7

File tree

3 files changed

+380
-54
lines changed

3 files changed

+380
-54
lines changed

Library/Homebrew/api/formula_struct.rb

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def self.from_hash(formula_hash)
2929
:stable,
3030
].freeze
3131

32+
SKIP_SERIALIZATION = [
33+
# Bottle checksums have special serialization done by the serialize_bottle method
34+
:bottle_checksums,
35+
].freeze
36+
37+
SPECS = [:head, :stable].freeze
38+
39+
# :any_skip_relocation is the most common in homebrew/core
40+
DEFAULT_CELLAR = :any_skip_relocation
41+
3242
DependsOnArgs = T.type_alias do
3343
T.any(
3444
# Dependencies
@@ -83,7 +93,7 @@ def self.from_hash(formula_hash)
8393
const :desc, String
8494
const :disable_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
8595
const :head_dependencies, T::Array[DependsOnArgs], default: []
86-
const :head_url_args, [String, T::Hash[Symbol, T.anything]]
96+
const :head_url_args, [String, T::Hash[Symbol, T.anything]], default: ["", {}]
8797
const :head_uses_from_macos, T::Array[UsesFromMacOSArgs], default: []
8898
const :homepage, String
8999
const :keg_only_args, T::Array[T.any(String, Symbol)], default: []
@@ -101,11 +111,201 @@ def self.from_hash(formula_hash)
101111
const :service_run_kwargs, T::Hash[Symbol, Homebrew::Service::RunParam], default: {}
102112
const :stable_dependencies, T::Array[DependsOnArgs], default: []
103113
const :stable_checksum, T.nilable(String)
104-
const :stable_url_args, [String, T::Hash[Symbol, T.anything]]
114+
const :stable_url_args, [String, T::Hash[Symbol, T.anything]], default: ["", {}]
105115
const :stable_uses_from_macos, T::Array[UsesFromMacOSArgs], default: []
106116
const :stable_version, String
107117
const :version_scheme, Integer, default: 0
108118
const :versioned_formulae, T::Array[String], default: []
119+
120+
sig { params(bottle_tag: ::Utils::Bottles::Tag).returns(T.nilable(T::Hash[String, T.untyped])) }
121+
def serialize_bottle(bottle_tag: ::Utils::Bottles.tag)
122+
bottle_collector = ::Utils::Bottles::Collector.new
123+
bottle_checksums.each do |bottle_info|
124+
bottle_info = bottle_info.dup
125+
cellar = bottle_info.delete(:cellar) || :any
126+
tag = T.must(bottle_info.keys.first)
127+
checksum = T.cast(bottle_info.values.first, String)
128+
129+
bottle_collector.add(
130+
::Utils::Bottles::Tag.from_symbol(tag),
131+
checksum: Checksum.new(checksum),
132+
cellar:,
133+
)
134+
end
135+
return unless (bottle_spec = bottle_collector.specification_for(bottle_tag))
136+
137+
tag = (bottle_spec.tag if bottle_spec.tag != bottle_tag)
138+
cellar = (bottle_spec.cellar if bottle_spec.cellar != DEFAULT_CELLAR)
139+
140+
{
141+
"bottle_tag" => tag&.to_sym,
142+
"bottle_cellar" => cellar,
143+
"bottle_checksum" => bottle_spec.checksum.to_s,
144+
}
145+
end
146+
147+
sig { params(bottle_tag: ::Utils::Bottles::Tag).returns(T::Hash[String, T.untyped]) }
148+
def serialize(bottle_tag: ::Utils::Bottles.tag)
149+
hash = self.class.decorator.all_props.filter_map do |prop|
150+
next if PREDICATES.any? { |predicate| prop == :"#{predicate}_present" }
151+
next if SKIP_SERIALIZATION.include?(prop)
152+
153+
[prop.to_s, send(prop)]
154+
end.to_h
155+
156+
if (bottle_hash = serialize_bottle(bottle_tag:))
157+
hash = hash.merge(bottle_hash)
158+
end
159+
160+
hash = self.class.deep_stringify_symbols(hash)
161+
self.class.deep_compact_blank(hash)
162+
end
163+
164+
sig { params(hash: T::Hash[String, T.untyped], bottle_tag: ::Utils::Bottles::Tag).returns(FormulaStruct) }
165+
def self.deserialize(hash, bottle_tag: ::Utils::Bottles.tag)
166+
hash = deep_unstringify_symbols(hash)
167+
168+
# Items that don't follow the `hash["foo_present"] = hash["foo_args"].present?` pattern are overridden below
169+
PREDICATES.each do |name|
170+
hash["#{name}_present"] = hash["#{name}_args"].present?
171+
end
172+
173+
if (bottle_checksum = hash["bottle_checksum"])
174+
tag = hash.fetch("bottle_tag", bottle_tag.to_sym)
175+
cellar = hash.fetch("bottle_cellar", DEFAULT_CELLAR)
176+
177+
hash["bottle_present"] = true
178+
hash["bottle_checksums"] = [{ cellar: cellar, tag => bottle_checksum }]
179+
else
180+
hash["bottle_present"] = false
181+
end
182+
183+
# *_url_args need to be in [String, Hash] format, but the hash may have been dropped if empty
184+
SPECS.each do |key|
185+
if (url_args = hash["#{key}_url_args"])
186+
hash["#{key}_present"] = true
187+
hash["#{key}_url_args"] = format_arg_pair(url_args, last: {})
188+
else
189+
hash["#{key}_present"] = false
190+
end
191+
192+
next unless (uses_from_macos = hash["#{key}_uses_from_macos"])
193+
194+
hash["#{key}_uses_from_macos"] = uses_from_macos.map do |args|
195+
format_arg_pair(args, last: {})
196+
end
197+
end
198+
199+
hash["service_args"] = if (service_args = hash["service_args"])
200+
service_args.map { |service_arg| format_arg_pair(service_arg, last: nil) }
201+
end
202+
203+
hash["conflicts"] = if (conflicts = hash["conflicts"])
204+
conflicts.map { |conflict| format_arg_pair(conflict, last: {}) }
205+
end
206+
207+
from_hash(hash)
208+
end
209+
210+
# Format argument pairs into proper [first, last] format if serialization has removed some elements.
211+
# Pass a default value for last to be used when only one element is present.
212+
#
213+
# format_arg_pair(["foo"], last: {}) # => ["foo", {}]
214+
# format_arg_pair([{ "foo" => :build }], last: {}) # => [{ "foo" => :build }, {}]
215+
# format_arg_pair(["foo", { since: :catalina }], last: {}) # => ["foo", { since: :catalina }]
216+
sig {
217+
type_parameters(:U, :V)
218+
.params(
219+
args: T.any([T.type_parameter(:U)], [T.type_parameter(:U), T.type_parameter(:V)]),
220+
last: T.type_parameter(:V),
221+
).returns([T.type_parameter(:U), T.type_parameter(:V)])
222+
}
223+
def self.format_arg_pair(args, last:)
224+
args = case args
225+
in [elem]
226+
[elem, last]
227+
in [elem1, elem2]
228+
[elem1, elem2]
229+
end
230+
231+
# The case above is exhaustive so args will never be nil, but sorbet cannot infer that.
232+
T.must(args)
233+
end
234+
235+
# Converts a symbol to a string starting with `:`, otherwise returns the input.
236+
#
237+
# stringify_symbol(:example) # => ":example"
238+
# stringify_symbol("example") # => "example"
239+
sig { params(value: T.any(String, Symbol)).returns(T.nilable(String)) }
240+
def self.stringify_symbol(value)
241+
return ":#{value}" if value.is_a?(Symbol)
242+
243+
value
244+
end
245+
246+
sig { params(obj: T.untyped).returns(T.untyped) }
247+
def self.deep_stringify_symbols(obj)
248+
case obj
249+
when String
250+
# Escape leading : or \ to avoid confusion with stringified symbols
251+
# ":foo" -> "\:foo"
252+
# "\foo" -> "\\foo"
253+
if obj.start_with?(":", "\\")
254+
"\\#{obj}"
255+
else
256+
obj
257+
end
258+
when Symbol
259+
":#{obj}"
260+
when Hash
261+
obj.to_h { |k, v| [deep_stringify_symbols(k), deep_stringify_symbols(v)] }
262+
when Array
263+
obj.map { |v| deep_stringify_symbols(v) }
264+
else
265+
obj
266+
end
267+
end
268+
269+
sig { params(obj: T.untyped).returns(T.untyped) }
270+
def self.deep_unstringify_symbols(obj)
271+
case obj
272+
when String
273+
if obj.start_with?("\\")
274+
obj[1..]
275+
elsif obj.start_with?(":")
276+
T.must(obj[1..]).to_sym
277+
else
278+
obj
279+
end
280+
when Hash
281+
obj.to_h { |k, v| [deep_unstringify_symbols(k), deep_unstringify_symbols(v)] }
282+
when Array
283+
obj.map { |v| deep_unstringify_symbols(v) }
284+
else
285+
obj
286+
end
287+
end
288+
289+
sig {
290+
type_parameters(:U)
291+
.params(obj: T.all(T.type_parameter(:U), Object))
292+
.returns(T.nilable(T.type_parameter(:U)))
293+
}
294+
def self.deep_compact_blank(obj)
295+
obj = case obj
296+
when Hash
297+
obj.transform_values { |v| deep_compact_blank(v) }
298+
.compact
299+
when Array
300+
obj.filter_map { |v| deep_compact_blank(v) }
301+
else
302+
obj
303+
end
304+
305+
return if obj.blank? || (obj.is_a?(Numeric) && obj.zero?)
306+
307+
obj
308+
end
109309
end
110310
end
111311
end

Library/Homebrew/dev-cmd/generate-formula-api.rb

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -69,61 +69,15 @@ def run
6969
File.write("_data/formula_canonical.json", "#{canonical_json}\n") unless args.dry_run?
7070

7171
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
72-
macos_version = bottle_tag.to_macos_version if bottle_tag.macos?
73-
74-
aliases = {}
75-
renames = {}
76-
variation_formulae = all_formulae.to_h do |name, formula|
77-
formula = Homebrew::API.merge_variations(formula, bottle_tag:)
78-
79-
formula["aliases"]&.each do |alias_name|
80-
aliases[alias_name] = name
81-
end
82-
83-
formula["oldnames"]&.each do |oldname|
84-
renames[oldname] = name
85-
end
86-
87-
version = Version.new(formula.dig("versions", "stable"))
88-
pkg_version = PkgVersion.new(version, formula["revision"])
89-
version_scheme = formula.fetch("version_scheme", 0)
90-
rebuild = formula.dig("bottle", "stable", "rebuild") || 0
91-
92-
bottle_collector = Utils::Bottles::Collector.new
93-
formula.dig("bottle", "stable", "files")&.each do |tag, data|
94-
tag = Utils::Bottles::Tag.from_symbol(tag)
95-
bottle_collector.add tag, checksum: Checksum.new(data["sha256"]), cellar: :any
96-
end
97-
98-
sha256 = bottle_collector.specification_for(bottle_tag)&.checksum&.to_s
99-
100-
dependencies = Set.new(formula["dependencies"])
101-
102-
if macos_version
103-
uses_from_macos = formula["uses_from_macos"].zip(formula["uses_from_macos_bounds"])
104-
dependencies += uses_from_macos.filter_map do |dep, bounds|
105-
next if bounds.blank?
106-
107-
since = bounds[:since]
108-
next if since.blank?
109-
110-
since_macos_version = MacOSVersion.from_symbol(since)
111-
next if since_macos_version <= macos_version
112-
113-
dep
114-
end
115-
else
116-
dependencies += formula["uses_from_macos"]
117-
end
118-
119-
[name, [pkg_version.to_s, version_scheme, rebuild, sha256, dependencies.to_a]]
72+
formulae = all_formulae.to_h do |name, hash|
73+
hash = Homebrew::API::Formula::FormulaStructGenerator.generate_formula_struct_hash(hash, bottle_tag:)
74+
.serialize(bottle_tag:)
75+
[name, hash]
12076
end
12177

12278
json_contents = {
123-
formulae: variation_formulae,
124-
casks: [],
125-
aliases: aliases,
126-
renames: renames,
79+
formulae:,
80+
tap_git_head: CoreTap.instance.git_head,
12781
tap_migrations: CoreTap.instance.tap_migrations,
12882
}
12983

0 commit comments

Comments
 (0)