This commit is contained in:
Mike McQuaid 2025-07-11 17:53:25 +01:00
parent f4e629331f
commit ef1d641e1a
No known key found for this signature in database
7 changed files with 129 additions and 53 deletions

View File

@ -150,12 +150,7 @@ module Homebrew
download_queue.enqueue(resource) download_queue.enqueue(resource)
end end
formula.resources.each do |r| formula.enqueue_resources_and_patches(download_queue:)
download_queue.enqueue(r)
r.patches.each { |patch| download_queue.enqueue(patch.resource) if patch.external? }
end
formula.patchlist.each { |patch| download_queue.enqueue(patch.resource) if patch.external? }
end end
end end
else else
@ -190,7 +185,11 @@ module Homebrew
sig { returns(Integer) } sig { returns(Integer) }
def concurrency def concurrency
@concurrency ||= T.let(args.concurrency&.to_i || 1, T.nilable(Integer)) @concurrency ||= T.let(
# TODO: document this variable when ready to publicly announce it.
args.concurrency&.to_i || ENV.fetch("HOMEBREW_DOWNLOAD_CONCURRENCY", "1").to_i,
T.nilable(Integer),
)
end end
sig { returns(Integer) } sig { returns(Integer) }

View File

@ -47,6 +47,7 @@ module Homebrew
description: "Ask for confirmation before downloading and installing formulae. " \ description: "Ask for confirmation before downloading and installing formulae. " \
"Print download and install sizes of bottles and dependencies.", "Print download and install sizes of bottles and dependencies.",
env: :ask env: :ask
flag "--concurrency=", description: "Number of concurrent downloads.", hidden: true
[ [
[:switch, "--formula", "--formulae", { [:switch, "--formula", "--formulae", {
description: "Treat all named arguments as formulae.", description: "Treat all named arguments as formulae.",

View File

@ -17,7 +17,7 @@ module Homebrew
@pool = T.let(Concurrent::FixedThreadPool.new(concurrency), Concurrent::FixedThreadPool) @pool = T.let(Concurrent::FixedThreadPool.new(concurrency), Concurrent::FixedThreadPool)
end end
sig { params(downloadable: T.any(Resource, Bottle, Cask::Download)).void } sig { params(downloadable: T.any(Resource, Bottle, Cask::Download, Downloadable)).void }
def enqueue(downloadable) def enqueue(downloadable)
downloads[downloadable] ||= Concurrent::Promises.future_on( downloads[downloadable] ||= Concurrent::Promises.future_on(
pool, RetryableDownload.new(downloadable, tries:), force, quiet pool, RetryableDownload.new(downloadable, tries:), force, quiet
@ -135,6 +135,13 @@ module Homebrew
end end
end end
sig { void }
def wait
downloads.each do |downloadable, promise|
promise.wait!
end
end
sig { void } sig { void }
def shutdown def shutdown
pool.shutdown pool.shutdown
@ -166,9 +173,9 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
attr_reader :quiet attr_reader :quiet
sig { returns(T::Hash[T.any(Resource, Bottle, Cask::Download), Concurrent::Promises::Future]) } sig { returns(T::Hash[T.any(Resource, Bottle, Cask::Download, Downloadable), Concurrent::Promises::Future]) }
def downloads def downloads
@downloads ||= T.let({}, T.nilable(T::Hash[T.any(Resource, Bottle, Cask::Download), @downloads ||= T.let({}, T.nilable(T::Hash[T.any(Resource, Bottle, Cask::Download, Downloadable),
Concurrent::Promises::Future])) Concurrent::Promises::Future]))
end end

View File

@ -3202,6 +3202,15 @@ class Formula
end end
end end
sig { params(download_queue: Homebrew::DownloadQueue).void }
def enqueue_resources_and_patches(download_queue:)
resources.each do |resource|
download_queue.enqueue(resource)
resource.patches.select(&:external?).each { |patch| download_queue.enqueue(patch.resource) }
end
patchlist.select(&:external?).each { |patch| download_queue.enqueue(patch.resource) }
end
sig { void } sig { void }
def fetch_patches def fetch_patches
patchlist.select(&:external?).each(&:fetch) patchlist.select(&:external?).each(&:fetch)

View File

@ -139,6 +139,8 @@ class FormulaInstaller
# Take the original formula instance, which might have been swapped from an API instance to a source instance # Take the original formula instance, which might have been swapped from an API instance to a source instance
@formula = T.let(T.must(previously_fetched_formula), Formula) if previously_fetched_formula @formula = T.let(T.must(previously_fetched_formula), Formula) if previously_fetched_formula
@ran_prelude_fetch = T.let(false, T::Boolean)
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
@ -293,8 +295,8 @@ class FormulaInstaller
dep.bottle&.compatible_locations? dep.bottle&.compatible_locations?
end end
sig { void } sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def prelude def prelude_fetch(download_queue: nil)
deprecate_disable_type = DeprecateDisable.type(formula) deprecate_disable_type = DeprecateDisable.type(formula)
if deprecate_disable_type.present? if deprecate_disable_type.present?
message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}" message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}"
@ -312,8 +314,24 @@ class FormulaInstaller
end end
end end
# Needs to be done before expand_dependencies for compute_dependencies
fetch_bottle_tab(download_queue:) if pour_bottle?
@ran_prelude_fetch = true
end
sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def prelude(download_queue: nil)
prelude_fetch(download_queue:) unless @ran_prelude_fetch
Tab.clear_cache Tab.clear_cache
# Setup bottle_tab_runtime_dependencies for compute_dependencies
@bottle_tab_runtime_dependencies = formula.bottle_tab_attributes
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
.freeze
verify_deps_exist unless ignore_deps? verify_deps_exist unless ignore_deps?
forbidden_license_check forbidden_license_check
@ -321,7 +339,7 @@ class FormulaInstaller
forbidden_formula_check forbidden_formula_check
check_install_sanity check_install_sanity
install_fetch_deps unless ignore_deps? install_fetch_deps(download_queue:) unless ignore_deps?
end end
sig { void } sig { void }
@ -450,14 +468,14 @@ class FormulaInstaller
sig { params(_formula: Formula).returns(T.nilable(T::Boolean)) } sig { params(_formula: Formula).returns(T.nilable(T::Boolean)) }
def fresh_install?(_formula) = false def fresh_install?(_formula) = false
sig { void } sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def install_fetch_deps def install_fetch_deps(download_queue: nil)
return if @compute_dependencies.blank? return if @compute_dependencies.blank?
compute_dependencies(use_cache: false) if @compute_dependencies.any? do |dep, options| compute_dependencies(use_cache: false) if @compute_dependencies.any? do |dep, options|
next false unless dep.implicit? next false unless dep.implicit?
fetch_dependencies fetch_dependencies(download_queue:)
install_dependency(dep, options) install_dependency(dep, options)
true true
end end
@ -787,8 +805,8 @@ on_request: installed_on_request?, options:)
@show_header = true unless deps.empty? @show_header = true unless deps.empty?
end end
sig { params(dep: Dependency).void } sig { params(dep: Dependency, download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def fetch_dependency(dep) def fetch_dependency(dep, download_queue: nil)
df = dep.to_formula df = dep.to_formula
fi = FormulaInstaller.new( fi = FormulaInstaller.new(
df, df,
@ -808,8 +826,8 @@ on_request: installed_on_request?, options:)
quiet: quiet?, quiet: quiet?,
verbose: verbose?, verbose: verbose?,
) )
fi.prelude fi.prelude(download_queue:)
fi.fetch fi.fetch(download_queue:)
end end
sig { params(dep: Dependency, inherited_options: Options).void } sig { params(dep: Dependency, inherited_options: Options).void }
@ -1326,8 +1344,8 @@ on_request: installed_on_request?, options:)
@show_summary_heading = true @show_summary_heading = true
end end
sig { void } sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def fetch_dependencies def fetch_dependencies(download_queue: nil)
return if ignore_deps? return if ignore_deps?
# Don't output dependencies if we're explicitly installing them. # Don't output dependencies if we're explicitly installing them.
@ -1337,11 +1355,16 @@ on_request: installed_on_request?, options:)
return if deps.empty? return if deps.empty?
dependencies_string = deps.map(&:first)
.map { Formatter.identifier(_1) }
.to_sentence
unless download_queue
oh1 "Fetching dependencies for #{formula.full_name}: " \ oh1 "Fetching dependencies for #{formula.full_name}: " \
"#{deps.map(&:first).map { Formatter.identifier(_1) }.to_sentence}", "#{dependencies_string}",
truncate: false truncate: false
end
deps.each { |(dep, _options)| fetch_dependency(dep) } deps.each { |(dep, _options)| fetch_dependency(dep, download_queue:) }
end end
sig { returns(T.nilable(Formula)) } sig { returns(T.nilable(Formula)) }
@ -1356,54 +1379,67 @@ on_request: installed_on_request?, options:)
end end
end end
sig { params(quiet: T::Boolean).void } sig { params(download_queue: T.nilable(Homebrew::DownloadQueue), quiet: T::Boolean).void }
def fetch_bottle_tab(quiet: false) def fetch_bottle_tab(download_queue: nil, quiet: false)
return if @fetch_bottle_tab return if @fetch_bottle_tab
if download_queue && (bottle = formula.bottle) && (manifest_resource = bottle.github_packages_manifest_resource)
download_queue.enqueue(manifest_resource)
else
begin begin
formula.fetch_bottle_tab(quiet: quiet) formula.fetch_bottle_tab(quiet: quiet)
@bottle_tab_runtime_dependencies = formula.bottle_tab_attributes
.fetch("runtime_dependencies", []).then { |deps| deps || [] }
.each_with_object({}) { |dep, h| h[dep["full_name"]] = dep }
.freeze
rescue DownloadError, Resource::BottleManifest::Error rescue DownloadError, Resource::BottleManifest::Error
# do nothing # do nothing
end end
end
@fetch_bottle_tab = T.let(true, T.nilable(TrueClass)) @fetch_bottle_tab = T.let(true, T.nilable(TrueClass))
end end
sig { void } sig { params(download_queue: T.nilable(Homebrew::DownloadQueue)).void }
def fetch def fetch(download_queue: nil)
return if previously_fetched_formula return if previously_fetched_formula
fetch_dependencies fetch_dependencies(download_queue:)
return if only_deps? return if only_deps?
return if formula.local_bottle_path.present? return if formula.local_bottle_path.present?
oh1 "Fetching #{Formatter.identifier(formula.full_name)}".strip oh1 "Fetching #{Formatter.identifier(formula.full_name)}".strip unless download_queue
downloadable_object = downloadable downloadable_object = downloadable
check_attestation = if pour_bottle?(output_warning: true) check_attestation = if pour_bottle?(output_warning: true)
fetch_bottle_tab fetch_bottle_tab(download_queue:)
!downloadable_object.cached_download.exist? !downloadable_object.cached_download.exist?
else else
@formula = Homebrew::API::Formula.source_download(formula) if formula.loaded_from_api? @formula = Homebrew::API::Formula.source_download(formula) if formula.loaded_from_api?
if download_queue
formula.enqueue_resources_and_patches(download_queue:)
else
formula.fetch_patches formula.fetch_patches
formula.resources.each(&:fetch) formula.resources.each(&:fetch)
end
downloadable_object = downloadable downloadable_object = downloadable
false false
end end
if download_queue
download_queue.enqueue(downloadable_object)
else
downloadable_object.fetch downloadable_object.fetch
end
# We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts # We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts
# to explicitly `brew install gh` without already having a version for bootstrapping. # to explicitly `brew install gh` without already having a version for bootstrapping.
# We also skip bottle installs from local bottle paths, as these are done in CI # We also skip bottle installs from local bottle paths, as these are done in CI
# as part of the build lifecycle before attestations are produced. # as part of the build lifecycle before attestations are produced.
if check_attestation && if check_attestation &&
# TODO: support this for download queues at some point
download_queue.nil? &&
Homebrew::Attestation.enabled? && Homebrew::Attestation.enabled? &&
formula.tap&.core_tap? && formula.tap&.core_tap? &&
formula.name != "gh" formula.name != "gh"

View File

@ -6,6 +6,7 @@ require "fileutils"
require "hardware" require "hardware"
require "development_tools" require "development_tools"
require "upgrade" require "upgrade"
require "download_queue"
module Homebrew module Homebrew
# Helper module for performing (pre-)install checks. # Helper module for performing (pre-)install checks.
@ -314,9 +315,28 @@ module Homebrew
skip_link: false skip_link: false
) )
unless dry_run unless dry_run
concurrency = ENV.fetch("HOMEBREW_DOWNLOAD_CONCURRENCY", 1).to_i
if concurrency > 1
retries = 0
# TODO: disable force when done testing locally!!!
force = true
raise if ENV["CI"] && force == true
download_queue = Homebrew::DownloadQueue.new(concurrency:, retries:, force:)
end
begin
formula_installers.each do |fi| formula_installers.each do |fi|
fi.prelude fi.prelude_fetch(download_queue:)
fi.fetch download_queue&.start
download_queue&.wait
fi.prelude(download_queue:)
download_queue&.start
download_queue&.wait
fi.fetch(download_queue:)
download_queue&.start
download_queue&.wait
rescue CannotInstallFormulaError => e rescue CannotInstallFormulaError => e
ofail e.message ofail e.message
next next
@ -324,6 +344,9 @@ module Homebrew
ofail "#{fi.formula}: #{e}" ofail "#{fi.formula}: #{e}"
next next
end end
ensure
download_queue&.shutdown
end
end end
if dry_run if dry_run

View File

@ -91,6 +91,7 @@ class Resource
patches.grep(DATAPatch) { |p| p.path = owner.owner.path } patches.grep(DATAPatch) { |p| p.path = owner.owner.path }
end end
sig { params(skip_downloaded: T::Boolean).void }
def fetch_patches(skip_downloaded: false) def fetch_patches(skip_downloaded: false)
external_patches = patches.select(&:external?) external_patches = patches.select(&:external?)
external_patches.reject!(&:downloaded?) if skip_downloaded external_patches.reject!(&:downloaded?) if skip_downloaded