Skip to content

Commit 0a4a299

Browse files
authored
Merge pull request #20245 from Homebrew/download_queue_install
Optionally use `download_queue` for `brew install`
2 parents 21e9db6 + 5cc6722 commit 0a4a299

8 files changed

Lines changed: 122 additions & 75 deletions

File tree

Library/Homebrew/dependency.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ def installed?(minimum_version: nil, minimum_revision: nil)
5151
end
5252
return false unless formula
5353

54+
# If the opt prefix doesn't exist: we likely have an incomplete installation.
55+
return false unless formula.opt_prefix.exist?
56+
5457
return true if formula.latest_version_installed?
5558

5659
return false if minimum_version.blank?
5760

58-
# If the opt prefix doesn't exist: we likely have an incomplete installation.
59-
return false unless formula.opt_prefix.exist?
60-
6161
installed_keg = formula.any_installed_keg
6262
return false unless installed_keg
6363

Library/Homebrew/download_queue.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
module Homebrew
1010
class DownloadQueue
11-
sig { params(retries: Integer, force: T::Boolean).void }
12-
def initialize(retries: 0, force: false)
11+
sig { params(retries: Integer, force: T::Boolean, pour: T::Boolean).void }
12+
def initialize(retries: 0, force: false, pour: false)
1313
@concurrency = T.let(EnvConfig.download_concurrency, Integer)
1414
@quiet = T.let(@concurrency > 1, T::Boolean)
1515
@tries = T.let(retries + 1, Integer)
1616
@force = force
17+
@pour = pour
1718
@pool = T.let(Concurrent::FixedThreadPool.new(concurrency), Concurrent::FixedThreadPool)
1819
end
1920

@@ -24,6 +25,10 @@ def enqueue(downloadable)
2425
) do |download, force, quiet|
2526
download.clear_cache if force
2627
download.fetch(quiet:)
28+
if pour && download.bottle?
29+
UnpackStrategy.detect(download.cached_download, prioritize_extension: true)
30+
.extract_nestedly(to: HOMEBREW_CELLAR)
31+
end
2732
end
2833
end
2934

@@ -168,6 +173,9 @@ def cancel
168173
sig { returns(T::Boolean) }
169174
attr_reader :quiet
170175

176+
sig { returns(T::Boolean) }
177+
attr_reader :pour
178+
171179
sig { returns(T::Hash[Downloadable, Concurrent::Promises::Future]) }
172180
def downloads
173181
@downloads ||= T.let({}, T.nilable(T::Hash[Downloadable, Concurrent::Promises::Future]))

Library/Homebrew/download_strategy.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,6 @@ class AbstractDownloadStrategy
3030

3131
abstract!
3232

33-
# Extension for bottle downloads.
34-
module Pourable
35-
extend T::Helpers
36-
37-
requires_ancestor { AbstractDownloadStrategy }
38-
39-
sig { params(block: T.nilable(T.proc.params(arg0: String).returns(T.anything))).returns(T.nilable(T.anything)) }
40-
def stage(&block)
41-
ohai "Pouring #{basename}"
42-
super
43-
end
44-
end
45-
4633
# The download URL.
4734
#
4835
# @api public
@@ -74,7 +61,6 @@ def initialize(url, name, version, **meta)
7461
@cache = T.let(meta.fetch(:cache, HOMEBREW_CACHE), Pathname)
7562
@meta = T.let(meta, T::Hash[Symbol, T.untyped])
7663
@quiet = T.let(false, T.nilable(T::Boolean))
77-
extend Pourable if meta[:bottle]
7864
end
7965

8066
# Download and cache the resource at {#cached_location}.
@@ -826,7 +812,6 @@ class LocalBottleDownloadStrategy < AbstractFileDownloadStrategy
826812
sig { params(path: Pathname).void }
827813
def initialize(path)
828814
@cached_location = T.let(path, Pathname)
829-
extend Pourable
830815
end
831816
# rubocop:enable Lint/MissingSuper
832817

Library/Homebrew/formula_installer.rb

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class FormulaInstaller
4444
sig { returns(T::Boolean) }
4545
attr_accessor :link_keg
4646

47+
sig { returns(T.nilable(Homebrew::DownloadQueue)) }
48+
attr_accessor :download_queue
49+
4750
sig {
4851
params(
4952
formula: Formula,
@@ -136,9 +139,12 @@ def initialize(
136139
@hold_locks = T.let(false, T::Boolean)
137140
@show_summary_heading = T.let(false, T::Boolean)
138141
@etc_var_preinstall = T.let([], T::Array[Pathname])
142+
@download_queue = T.let(nil, T.nilable(Homebrew::DownloadQueue))
139143

140144
# Take the original formula instance, which might have been swapped from an API instance to a source instance
141145
@formula = T.let(T.must(previously_fetched_formula), Formula) if previously_fetched_formula
146+
147+
@ran_prelude_fetch = T.let(false, T::Boolean)
142148
end
143149

144150
sig { returns(T::Boolean) }
@@ -294,7 +300,7 @@ def install_bottle_for?(dep, build)
294300
end
295301

296302
sig { void }
297-
def prelude
303+
def prelude_fetch
298304
deprecate_disable_type = DeprecateDisable.type(formula)
299305
if deprecate_disable_type.present?
300306
message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}"
@@ -312,8 +318,24 @@ def prelude
312318
end
313319
end
314320

321+
# Needs to be done before expand_dependencies for compute_dependencies
322+
fetch_bottle_tab if pour_bottle?
323+
324+
@ran_prelude_fetch = true
325+
end
326+
327+
sig { void }
328+
def prelude
329+
prelude_fetch unless @ran_prelude_fetch
330+
315331
Tab.clear_cache
316332

333+
# Setup bottle_tab_runtime_dependencies for compute_dependencies
334+
@bottle_tab_runtime_dependencies = formula.bottle_tab_attributes
335+
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
336+
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
337+
.freeze
338+
317339
verify_deps_exist unless ignore_deps?
318340

319341
forbidden_license_check
@@ -778,13 +800,15 @@ def install_dependencies(deps)
778800
if deps.empty? && only_deps?
779801
puts "All dependencies for #{formula.full_name} are satisfied."
780802
elsif !deps.empty?
781-
oh1 "Installing dependencies for #{formula.full_name}: " \
782-
"#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}",
783-
truncate: false
803+
if deps.length > 1
804+
oh1 "Installing dependencies for #{formula.full_name}: " \
805+
"#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}",
806+
truncate: false
807+
end
784808
deps.each { |dep, options| install_dependency(dep, options) }
785809
end
786810

787-
@show_header = true unless deps.empty?
811+
@show_header = true if deps.length > 1
788812
end
789813

790814
sig { params(dep: Dependency).void }
@@ -808,6 +832,7 @@ def fetch_dependency(dep)
808832
quiet: quiet?,
809833
verbose: verbose?,
810834
)
835+
fi.download_queue = download_queue
811836
fi.prelude
812837
fi.fetch
813838
end
@@ -830,7 +855,7 @@ def install_dependency(dep, inherited_options)
830855
installed_keg = Keg.new(df.prefix)
831856
tab ||= installed_keg.tab
832857
tmp_keg = Pathname.new("#{installed_keg}.tmp")
833-
installed_keg.rename(tmp_keg)
858+
installed_keg.rename(tmp_keg) unless tmp_keg.directory?
834859
end
835860

836861
if df.tap.present? && tab.present? && (tab_tap = tab.source["tap"].presence) &&
@@ -867,6 +892,7 @@ def install_dependency(dep, inherited_options)
867892
verbose: verbose?,
868893
)
869894
oh1 "Installing #{formula.full_name} dependency: #{Formatter.identifier(dep.name)}"
895+
fi.prelude
870896
fi.install
871897
fi.finish
872898
# Handle all possible exceptions installing deps.
@@ -1337,9 +1363,13 @@ def fetch_dependencies
13371363

13381364
return if deps.empty?
13391365

1340-
oh1 "Fetching dependencies for #{formula.full_name}: " \
1341-
"#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}",
1342-
truncate: false
1366+
unless download_queue
1367+
dependencies_string = deps.map(&:first)
1368+
.map { Formatter.identifier(_1) }
1369+
.to_sentence
1370+
oh1 "Fetching dependencies for #{formula.full_name}: #{dependencies_string}",
1371+
truncate: false
1372+
end
13431373

13441374
deps.each { |(dep, _options)| fetch_dependency(dep) }
13451375
end
@@ -1360,15 +1390,18 @@ def previously_fetched_formula
13601390
def fetch_bottle_tab(quiet: false)
13611391
return if @fetch_bottle_tab
13621392

1363-
begin
1364-
formula.fetch_bottle_tab(quiet: quiet)
1365-
@bottle_tab_runtime_dependencies = formula.bottle_tab_attributes
1366-
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
1367-
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
1368-
.freeze
1369-
rescue DownloadError, Resource::BottleManifest::Error
1370-
# do nothing
1393+
if (download_queue = self.download_queue) &&
1394+
(bottle = formula.bottle) &&
1395+
(manifest_resource = bottle.github_packages_manifest_resource)
1396+
download_queue.enqueue(manifest_resource)
1397+
else
1398+
begin
1399+
formula.fetch_bottle_tab(quiet: quiet)
1400+
rescue DownloadError, Resource::BottleManifest::Error
1401+
# do nothing
1402+
end
13711403
end
1404+
13721405
@fetch_bottle_tab = T.let(true, T.nilable(TrueClass))
13731406
end
13741407

@@ -1381,7 +1414,7 @@ def fetch
13811414
return if only_deps?
13821415
return if formula.local_bottle_path.present?
13831416

1384-
oh1 "Fetching #{Formatter.identifier(formula.full_name)}".strip
1417+
oh1 "Fetching #{Formatter.identifier(formula.full_name)}".strip unless download_queue
13851418

13861419
downloadable_object = downloadable
13871420
check_attestation = if pour_bottle?(output_warning: true)
@@ -1391,19 +1424,31 @@ def fetch
13911424
else
13921425
@formula = Homebrew::API::Formula.source_download(formula) if formula.loaded_from_api?
13931426

1394-
formula.fetch_patches
1395-
formula.resources.each(&:fetch)
1427+
if (download_queue = self.download_queue)
1428+
formula.enqueue_resources_and_patches(download_queue:)
1429+
else
1430+
formula.fetch_patches
1431+
formula.resources.each(&:fetch)
1432+
end
1433+
13961434
downloadable_object = downloadable
13971435

13981436
false
13991437
end
1400-
downloadable_object.fetch
1438+
1439+
if (download_queue = self.download_queue)
1440+
download_queue.enqueue(downloadable_object)
1441+
else
1442+
downloadable_object.fetch
1443+
end
14011444

14021445
# We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts
14031446
# to explicitly `brew install gh` without already having a version for bootstrapping.
14041447
# We also skip bottle installs from local bottle paths, as these are done in CI
14051448
# as part of the build lifecycle before attestations are produced.
14061449
if check_attestation &&
1450+
# TODO: support this for download queues at some point
1451+
download_queue.nil? &&
14071452
Homebrew::Attestation.enabled? &&
14081453
formula.tap&.core_tap? &&
14091454
formula.name != "gh"
@@ -1489,7 +1534,10 @@ def downloadable
14891534
sig { void }
14901535
def pour
14911536
HOMEBREW_CELLAR.cd do
1492-
downloadable.downloader.stage
1537+
# download queue has already done the actual staging but we'll lie about
1538+
# pouring now for nicer output
1539+
ohai "Pouring #{downloadable.downloader.basename}"
1540+
downloadable.downloader.stage unless download_queue
14931541
end
14941542

14951543
Tab.clear_cache

Library/Homebrew/install.rb

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require "hardware"
77
require "development_tools"
88
require "upgrade"
9+
require "download_queue"
910

1011
module Homebrew
1112
# Helper module for performing (pre-)install checks.
@@ -313,30 +314,43 @@ def install_formulae(
313314
skip_post_install: false,
314315
skip_link: false
315316
)
316-
unless dry_run
317+
formulae_names_to_install = formula_installers.map { |fi| fi.formula.name }
318+
return if formulae_names_to_install.empty?
319+
320+
if dry_run
321+
ohai "Would install #{Utils.pluralize("formula", formulae_names_to_install.count,
322+
plural: "e", include_count: true)}:"
323+
puts formulae_names_to_install.join(" ")
324+
317325
formula_installers.each do |fi|
318-
fi.prelude
319-
fi.fetch
320-
rescue CannotInstallFormulaError => e
321-
ofail e.message
322-
next
323-
rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e
324-
ofail "#{fi.formula}: #{e}"
325-
next
326+
print_dry_run_dependencies(fi.formula, fi.compute_dependencies, &:name)
326327
end
328+
return
327329
end
328330

329-
if dry_run
330-
if (formulae_name_to_install = formula_installers.map { |fi| fi.formula.name })
331-
ohai "Would install #{Utils.pluralize("formula", formulae_name_to_install.count,
332-
plural: "e", include_count: true)}:"
333-
puts formulae_name_to_install.join(" ")
334-
331+
formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence
332+
oh1 "Fetching downloads for: #{formula_sentence}", truncate: false
333+
if EnvConfig.download_concurrency > 1
334+
download_queue = Homebrew::DownloadQueue.new(pour: true)
335+
formula_installers.each do |fi|
336+
fi.download_queue = download_queue
337+
end
338+
end
339+
begin
340+
[:prelude_fetch, :prelude, :fetch].each do |step|
335341
formula_installers.each do |fi|
336-
print_dry_run_dependencies(fi.formula, fi.compute_dependencies, &:name)
342+
fi.public_send(step)
343+
rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e
344+
ofail "#{fi.formula}: #{e}"
345+
next
337346
end
347+
download_queue&.fetch
348+
rescue CannotInstallFormulaError => e
349+
ofail e.message
350+
next
338351
end
339-
return
352+
ensure
353+
download_queue&.shutdown
340354
end
341355

342356
formula_installers.each do |fi|

Library/Homebrew/retryable_download.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ def verify_download_integrity(filename) = downloadable.verify_download_integrity
8989
sig { override.returns(String) }
9090
def download_name = downloadable.download_name
9191

92+
sig { returns(T::Boolean) }
93+
def bottle? = downloadable.is_a?(Bottle)
94+
9295
private
9396

9497
sig { returns(Downloadable) }

Library/Homebrew/test/cmd/deps_spec.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@
2929
setup_test_formula "recommended_test"
3030
setup_test_formula "installed"
3131

32-
# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory
33-
keg_dir = HOMEBREW_CELLAR/"installed"/"1.0"
32+
# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory and opt link
33+
keg_dir = HOMEBREW_CELLAR/"installed/1.0"
3434
keg_dir.mkpath
3535
touch keg_dir/AbstractTab::FILENAME
36+
opt_link = HOMEBREW_PREFIX/"opt/installed"
37+
opt_link.parent.mkpath
38+
FileUtils.ln_sf keg_dir, opt_link
3639

3740
expect { brew "deps", "baz", "--include-test", "--missing", "--skip-recommended" }
3841
.to be_a_success

0 commit comments

Comments
 (0)