-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
resource.rb
370 lines (298 loc) · 9.99 KB
/
resource.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# typed: true
# frozen_string_literal: true
require "downloadable"
require "mktemp"
require "livecheck"
require "extend/on_system"
# Resource is the fundamental representation of an external resource. The
# primary formula download, along with other declared resources, are instances
# of this class.
class Resource < Downloadable
include FileUtils
include OnSystem::MacOSAndLinux
attr_reader :source_modified_time, :patches, :owner
attr_writer :checksum
attr_accessor :download_strategy
# Formula name must be set after the DSL, as we have no access to the
# formula name before initialization of the formula.
attr_accessor :name
sig { params(name: T.nilable(String), block: T.nilable(T.proc.bind(Resource).void)).void }
def initialize(name = nil, &block)
super()
# Ensure this is synced with `initialize_dup` and `freeze` (excluding simple objects like integers and booleans)
@name = name
@patches = []
@livecheck = Livecheck.new(self)
@livecheckable = false
@insecure = false
instance_eval(&block) if block
end
def initialize_dup(other)
super
@name = @name.dup
@patches = @patches.dup
@livecheck = @livecheck.dup
end
def freeze
@name.freeze
@patches.freeze
@livecheck.freeze
super
end
def owner=(owner)
@owner = owner
patches.each { |p| p.owner = owner }
return if !owner.respond_to?(:full_name) || owner.full_name != "ca-certificates"
return if Homebrew::EnvConfig.no_insecure_redirect?
@insecure = !specs[:bottle] && (DevelopmentTools.ca_file_substitution_required? ||
DevelopmentTools.curl_substitution_required?)
return if @url.nil?
specs = if @insecure
@url.specs.merge({ insecure: true })
else
@url.specs.except(:insecure)
end
@url = URL.new(@url.to_s, specs)
end
# Removes /s from resource names; this allows Go package names
# to be used as resource names without confusing software that
# interacts with {download_name}, e.g. `github.com/foo/bar`.
def escaped_name
name.tr("/", "-")
end
def download_name
return owner.name if name.nil?
return escaped_name if owner.nil?
"#{owner.name}--#{escaped_name}"
end
# Verifies download and unpacks it.
# The block may call `|resource, staging| staging.retain!` to retain the staging
# directory. Subclasses that override stage should implement the tmp
# dir using {Mktemp} so that works with all subtypes.
#
# @api public
def stage(target = nil, debug_symbols: false, &block)
raise ArgumentError, "Target directory or block is required" if !target && block.blank?
prepare_patches
fetch_patches(skip_downloaded: true)
fetch unless downloaded?
unpack(target, debug_symbols:, &block)
end
def prepare_patches
patches.grep(DATAPatch) { |p| p.path = owner.owner.path }
end
def fetch_patches(skip_downloaded: false)
external_patches = patches.select(&:external?)
external_patches.reject!(&:downloaded?) if skip_downloaded
external_patches.each(&:fetch)
end
def apply_patches
return if patches.empty?
ohai "Patching #{name}"
patches.each(&:apply)
end
# If a target is given, unpack there; else unpack to a temp folder.
# If block is given, yield to that block with `|stage|`, where stage
# is a {ResourceStageContext}.
# A target or a block must be given, but not both.
def unpack(target = nil, debug_symbols: false)
current_working_directory = Pathname.pwd
stage_resource(download_name, debug_symbols:) do |staging|
downloader.stage do
@source_modified_time = downloader.source_modified_time
apply_patches
if block_given?
yield ResourceStageContext.new(self, staging)
elsif target
target = Pathname(target)
target = current_working_directory/target if target.relative?
target.install Pathname.pwd.children
end
end
end
end
Partial = Struct.new(:resource, :files)
def files(*files)
Partial.new(self, files)
end
def fetch(verify_download_integrity: true)
fetch_patches
super
end
# {Livecheck} can be used to check for newer versions of the software.
# This method evaluates the DSL specified in the livecheck block of the
# {Resource} (if it exists) and sets the instance variables of a {Livecheck}
# object accordingly. This is used by `brew livecheck` to check for newer
# versions of the software.
#
# ### Example
#
# ```ruby
# livecheck do
# url "https://example.com/foo/releases"
# regex /foo-(\d+(?:\.\d+)+)\.tar/
# end
# ```
#
# @!attribute [w] livecheck
def livecheck(&block)
return @livecheck unless block
@livecheckable = true
@livecheck.instance_eval(&block)
end
# Whether a livecheck specification is defined or not.
# It returns true when a `livecheck` block is present in the {Resource} and
# false otherwise and is used by livecheck.
def livecheckable?
@livecheckable == true
end
def sha256(val)
@checksum = Checksum.new(val)
end
def url(val = nil, **specs)
return @url&.to_s if val.nil?
specs = specs.dup
# Don't allow this to be set.
specs.delete(:insecure)
specs[:insecure] = true if @insecure
@url = URL.new(val, specs)
@downloader = nil
@download_strategy = @url.download_strategy
end
sig { params(val: T.nilable(T.any(String, Version))).returns(T.nilable(Version)) }
def version(val = nil)
return super() if val.nil?
@version = case val
when String
val.blank? ? Version::NULL : Version.new(val)
when Version
val
end
end
def mirror(val)
mirrors << val
end
def patch(strip = :p1, src = nil, &block)
p = Patch.create(strip, src, &block)
patches << p
end
def using
@url&.using
end
def specs
@url&.specs || {}.freeze
end
protected
def stage_resource(prefix, debug_symbols: false, &block)
Mktemp.new(prefix, retain_in_cache: debug_symbols).run(&block)
end
private
def determine_url_mirrors
extra_urls = []
# glibc-bootstrap
if url.start_with?("https://github.com/Homebrew/glibc-bootstrap/releases/download")
if (artifact_domain = Homebrew::EnvConfig.artifact_domain.presence)
artifact_url = url.sub("https://github.com", artifact_domain)
return [artifact_url] if Homebrew::EnvConfig.artifact_domain_no_fallback?
extra_urls << artifact_url
end
if Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
tag, filename = url.split("/").last(2)
extra_urls << "#{Homebrew::EnvConfig.bottle_domain}/glibc-bootstrap/#{tag}/#{filename}"
end
end
# PyPI packages: PEP 503 – Simple Repository API <https://peps.python.org/pep-0503>
if (pip_index_url = Homebrew::EnvConfig.pip_index_url.presence)
pip_index_base_url = pip_index_url.chomp("/").chomp("/simple")
%w[https://files.pythonhosted.org https://pypi.org].each do |base_url|
extra_urls << url.sub(base_url, pip_index_base_url) if url.start_with?("#{base_url}/packages")
end
end
[*extra_urls, *super].uniq
end
# A resource containing a Go package.
class Go < Resource
def stage(target, &block)
super(target/name, &block)
end
end
# A resource for a bottle manifest.
class BottleManifest < Resource
class Error < RuntimeError; end
attr_reader :bottle
def initialize(bottle)
super("#{bottle.name}_bottle_manifest")
@bottle = bottle
end
def verify_download_integrity(_filename)
# We don't have a checksum, but we can at least try parsing it.
tab
rescue Error => e
raise DownloadError.new(self, e)
end
def tab
json = begin
JSON.parse(cached_download.read)
rescue JSON::ParserError
raise Error, "The downloaded GitHub Packages manifest was corrupted or modified (it is not valid JSON): " \
"\n#{cached_download}"
end
manifests = json["manifests"]
raise Error, "Missing 'manifests' section." if manifests.blank?
manifests_annotations = manifests.filter_map { |m| m["annotations"] }
raise Error, "Missing 'annotations' section." if manifests_annotations.blank?
bottle_digest = bottle.resource.checksum.hexdigest
image_ref = GitHubPackages.version_rebuild(bottle.resource.version, bottle.rebuild, bottle.tag.to_s)
manifest_annotations = manifests_annotations.find do |m|
next if m["sh.brew.bottle.digest"] != bottle_digest
m["org.opencontainers.image.ref.name"] == image_ref
end
raise Error, "Couldn't find manifest matching bottle checksum." if manifest_annotations.blank?
tab = manifest_annotations["sh.brew.tab"]
raise Error, "Couldn't find tab from manifest." if tab.blank?
begin
JSON.parse(tab)
rescue JSON::ParserError
raise Error, "Couldn't parse tab JSON."
end
end
end
# A resource containing a patch.
class PatchResource < Resource
attr_reader :patch_files
def initialize(&block)
@patch_files = []
@directory = nil
super "patch", &block
end
def apply(*paths)
paths.flatten!
@patch_files.concat(paths)
@patch_files.uniq!
end
def directory(val = nil)
return @directory if val.nil?
@directory = val
end
end
end
# The context in which a {Resource#stage} occurs. Supports access to both
# the {Resource} and associated {Mktemp} in a single block argument. The interface
# is back-compatible with {Resource} itself as used in that context.
class ResourceStageContext
extend Forwardable
# The {Resource} that is being staged.
attr_reader :resource
# The {Mktemp} in which {#resource} is staged.
attr_reader :staging
def_delegators :@resource, :version, :url, :mirrors, :specs, :using, :source_modified_time
def_delegators :@staging, :retain!
def initialize(resource, staging)
@resource = resource
@staging = staging
end
sig { returns(String) }
def to_s
"<#{self.class}: resource=#{resource} staging=#{staging}>"
end
end