Skip to content

Commit 6e82544

Browse files
committed
Extract queue staging hooks
- Keep bottle pre-pouring behaviour while moving it behind `Downloadable` queue staging hooks. - Move cask extraction and rename helpers onto `Cask::Download` so installer orchestration stays thinner. - Leave cask downloads as a no-op for queued staging so this stays behaviour-neutral before adding new queue work.
1 parent decec77 commit 6e82544

8 files changed

Lines changed: 168 additions & 108 deletions

File tree

Library/Homebrew/bottle.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "unpack_strategy"
5+
46
class Bottle
57
include Downloadable
68

@@ -247,6 +249,64 @@ def sbom_supplement
247249
sig { returns(Filename) }
248250
def filename = Filename.new(@name, @pkg_version, @tag, @spec.rebuild)
249251

252+
sig { returns(Pathname) }
253+
def staged_path_from_download_queue
254+
bottle_filename = filename
255+
HOMEBREW_TEMP_CELLAR/bottle_filename.name/bottle_filename.version.to_s
256+
end
257+
258+
sig { returns(Pathname) }
259+
def staged_path_from_download_queue_marker
260+
Pathname("#{staged_path_from_download_queue}.poured")
261+
end
262+
263+
sig { returns(Pathname) }
264+
def staged_path_from_download_queue_lock
265+
Pathname("#{staged_path_from_download_queue_marker}.lock")
266+
end
267+
268+
sig { override.params(_download: Pathname, pour: T::Boolean).returns(T::Boolean) }
269+
def stage_from_download_queue?(_download, pour:)
270+
pour
271+
end
272+
273+
sig { override.params(download: Pathname, pour: T::Boolean).void }
274+
def stage_from_download_queue(download, pour:)
275+
return unless pour
276+
277+
bottle_tmp_keg = staged_path_from_download_queue
278+
bottle_poured_file = staged_path_from_download_queue_marker
279+
280+
begin
281+
HOMEBREW_TEMP_CELLAR.mkpath
282+
283+
File.open(staged_path_from_download_queue_lock, File::RDWR | File::CREAT, 0644) do |lock|
284+
lock.flock(File::LOCK_EX)
285+
286+
next if bottle_poured_file.exist?
287+
288+
FileUtils.rm(bottle_poured_file) if bottle_poured_file.symlink?
289+
FileUtils.rm_r(bottle_tmp_keg) if bottle_tmp_keg.directory?
290+
291+
UnpackStrategy.detect(download, prioritize_extension: true)
292+
.extract_nestedly(to: HOMEBREW_TEMP_CELLAR)
293+
294+
# Create a separate file to mark a completed extraction. This avoids
295+
# a potential race condition if a user interrupts the install.
296+
# We use a symlink to easily check that both this extra status file
297+
# and the real extracted directory exist via `Pathname#exist?`.
298+
FileUtils.ln_s(bottle_tmp_keg, bottle_poured_file)
299+
end
300+
# Catch any exception type here to clean up partial queued extractions.
301+
rescue Exception # rubocop:disable Lint/RescueException
302+
ignore_interrupts do
303+
FileUtils.rm_r(bottle_tmp_keg) if bottle_tmp_keg.directory?
304+
bottle_tmp_keg.parent.rmdir_if_possible
305+
end
306+
raise
307+
end
308+
end
309+
250310
sig { returns(T.nilable(Resource::BottleManifest)) }
251311
def github_packages_manifest_resource
252312
return if @resource.download_strategy != CurlGitHubPackagesDownloadStrategy

Library/Homebrew/cask/download.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
require "downloadable"
55
require "fileutils"
6+
require "unpack_strategy"
67
require "cask/cache"
78
require "cask/quarantine"
89

@@ -87,6 +88,58 @@ def basename
8788
downloader.basename
8889
end
8990

91+
sig { returns(UnpackStrategy) }
92+
def primary_container
93+
@primary_container ||= T.let(
94+
begin
95+
downloaded_path = cask.download || fetch(quiet: true)
96+
UnpackStrategy.detect(downloaded_path, type: cask.container&.type, merge_xattrs: true)
97+
end,
98+
T.nilable(UnpackStrategy),
99+
)
100+
end
101+
102+
sig { params(to: Pathname, verbose: T::Boolean).void }
103+
def extract_primary_container(to:, verbose:)
104+
odebug "Extracting primary container"
105+
106+
container = primary_container
107+
raise "unexpected nil primary_container" unless container
108+
109+
odebug "Using container class #{container.class} for #{container.path}"
110+
111+
if (nested_container = cask.container&.nested)
112+
Dir.mktmpdir("cask-installer", HOMEBREW_TEMP) do |tmpdir|
113+
tmpdir = Pathname(tmpdir)
114+
container.extract(to: tmpdir, basename:, verbose:)
115+
116+
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose
117+
118+
UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
119+
.extract_nestedly(to:, verbose:)
120+
end
121+
else
122+
container.extract_nestedly(to:, basename:, verbose:)
123+
end
124+
125+
return unless @quarantine
126+
return unless Quarantine.available?
127+
128+
Quarantine.propagate(from: container.path, to:)
129+
end
130+
131+
sig { params(target_dir: Pathname).void }
132+
def process_rename_operations(target_dir:)
133+
return if cask.rename.empty?
134+
135+
odebug "Processing rename operations in #{target_dir}"
136+
137+
cask.rename.each do |rename_operation|
138+
odebug "Renaming #{rename_operation.from} to #{rename_operation.to}"
139+
rename_operation.perform_rename(target_dir)
140+
end
141+
end
142+
90143
sig { override.returns(T::Boolean) }
91144
def downloaded_and_valid?
92145
return false unless super

Library/Homebrew/cask/installer.rb

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,9 @@ def download(quiet: nil, timeout: nil)
264264

265265
sig { returns(UnpackStrategy) }
266266
def primary_container
267-
@primary_container ||= T.let(
268-
begin
269-
downloaded_path = download(quiet: true)
270-
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
271-
end,
272-
T.nilable(UnpackStrategy),
273-
)
267+
download(quiet: true) if @cask.download.nil?
268+
269+
downloader.primary_container
274270
end
275271

276272
sig { returns(ArtifactSet) }
@@ -280,46 +276,12 @@ def artifacts
280276

281277
sig { params(to: Pathname).void }
282278
def extract_primary_container(to: @cask.staged_path)
283-
odebug "Extracting primary container"
284-
285-
container = primary_container
286-
raise "unexpected nil primary_container" unless container
287-
288-
odebug "Using container class #{container.class} for #{container.path}"
289-
290-
basename = downloader.basename
291-
292-
if (nested_container = @cask.container&.nested)
293-
Dir.mktmpdir("cask-installer", HOMEBREW_TEMP) do |tmpdir|
294-
tmpdir = Pathname(tmpdir)
295-
container.extract(to: tmpdir, basename:, verbose: verbose?)
296-
297-
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?
298-
299-
UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
300-
.extract_nestedly(to:, verbose: verbose?)
301-
end
302-
else
303-
container.extract_nestedly(to:, basename:, verbose: verbose?)
304-
end
305-
306-
return unless quarantine?
307-
return unless Quarantine.available?
308-
309-
Quarantine.propagate(from: container.path, to:)
279+
downloader.extract_primary_container(to:, verbose: verbose?)
310280
end
311281

312282
sig { params(target_dir: T.nilable(Pathname)).void }
313283
def process_rename_operations(target_dir: nil)
314-
return if @cask.rename.empty?
315-
316-
working_dir = target_dir || @cask.staged_path
317-
odebug "Processing rename operations in #{working_dir}"
318-
319-
@cask.rename.each do |rename_operation|
320-
odebug "Renaming #{rename_operation.from} to #{rename_operation.to}"
321-
rename_operation.perform_rename(working_dir)
322-
end
284+
downloader.process_rename_operations(target_dir: target_dir || @cask.staged_path)
323285
end
324286

325287
sig { params(predecessor: T.nilable(Cask)).void }

Library/Homebrew/download_queue.rb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,19 @@ def initialize(retries: 1, force: false, pour: false)
4040
params(
4141
downloadable: Downloadable,
4242
check_attestation: T::Boolean,
43+
stage: T::Boolean,
4344
).void
4445
}
45-
def enqueue(downloadable, check_attestation: false)
46+
def enqueue(downloadable, check_attestation: false, stage: pour)
4647
@cancelled.make_false
4748
cached_location = downloadable.cached_download
4849

4950
@symlink_targets[cached_location] ||= Set.new
5051
targets = @symlink_targets.fetch(cached_location)
5152
targets << downloadable
5253

53-
@downloads_by_location[cached_location] ||= Concurrent::Promises.future_on(
54-
pool, RetryableDownload.new(downloadable, tries:, pour:),
54+
download = @downloads_by_location[cached_location] ||= Concurrent::Promises.future_on(
55+
pool, RetryableDownload.new(downloadable, tries:),
5556
@cancelled, force, quiet, check_attestation
5657
) do |download, cancelled, force, quiet, check_attestation|
5758
raise CancelledDownloadError if cancelled.true?
@@ -70,14 +71,28 @@ def enqueue(downloadable, check_attestation: false)
7071

7172
check_bottle_attestation(downloadable, check_attestation:)
7273
create_symlinks_for_shared_download(cached_location)
74+
cached_location
7375
rescue Interrupt
7476
raise CancelledDownloadError
7577
ensure
7678
@download_threads.delete(Thread.current)
7779
end
7880
end
7981

80-
downloads[downloadable] = @downloads_by_location.fetch(cached_location)
82+
downloads[downloadable] = if stage
83+
download.then_on(
84+
pool, downloadable, pour
85+
) do |downloaded_path, queued_downloadable, queue_pour|
86+
if queued_downloadable.stage_from_download_queue?(downloaded_path, pour: queue_pour)
87+
queued_downloadable.extracting!
88+
queued_downloadable.stage_from_download_queue(downloaded_path, pour: queue_pour)
89+
queued_downloadable.downloaded!
90+
end
91+
downloaded_path
92+
end
93+
else
94+
download
95+
end
8196
end
8297

8398
sig { void }

Library/Homebrew/downloadable.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ def fetch(verify_download_integrity: true, timeout: nil, quiet: false)
168168
download
169169
end
170170

171+
sig { overridable.params(_download: Pathname, pour: T::Boolean).returns(T::Boolean) }
172+
def stage_from_download_queue?(_download, pour:)
173+
false
174+
end
175+
176+
sig { overridable.params(_download: Pathname, pour: T::Boolean).void }
177+
def stage_from_download_queue(_download, pour:); end
178+
171179
sig { overridable.params(filename: Pathname).void }
172180
def verify_download_integrity(filename)
173181
verifying!

Library/Homebrew/formula_installer.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,22 +1570,22 @@ def api_bottle
15701570
sig { void }
15711571
def pour
15721572
HOMEBREW_CELLAR.cd do
1573-
ohai "Pouring #{downloadable.downloader.basename}"
1573+
downloadable_object = downloadable
1574+
ohai "Pouring #{downloadable_object.downloader.basename}"
15741575

15751576
formula.rack.mkpath
15761577

15771578
# Download queue may have already extracted the bottle to a temporary directory.
15781579
# We cannot rely on `download_queue` here as dependencies may be poured by another installer.
1579-
formula_prefix_relative_to_cellar = formula.prefix.relative_path_from(HOMEBREW_CELLAR)
1580-
bottle_tmp_keg = HOMEBREW_TEMP_CELLAR/formula_prefix_relative_to_cellar
1581-
bottle_poured_file = Pathname("#{bottle_tmp_keg}.poured")
1582-
1583-
if bottle_poured_file.exist?
1580+
if downloadable_object.is_a?(Bottle) &&
1581+
(bottle_poured_file = downloadable_object.staged_path_from_download_queue_marker).exist?
1582+
bottle_tmp_keg = downloadable_object.staged_path_from_download_queue
15841583
FileUtils.rm(bottle_poured_file)
15851584
FileUtils.mv(bottle_tmp_keg, formula.prefix)
1585+
FileUtils.rm_f(downloadable_object.staged_path_from_download_queue_lock)
15861586
bottle_tmp_keg.parent.rmdir_if_possible
15871587
else
1588-
downloadable.downloader.stage
1588+
downloadable_object.downloader.stage
15891589
end
15901590
end
15911591

0 commit comments

Comments
 (0)