Merge pull request #23381 from Homebrew/install-earlier-downloads

Start install downloads before local-only checks
This commit is contained in:
Mike McQuaid
2026-08-03 08:17:31 +00:00
committed by GitHub
16 changed files with 369 additions and 243 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ module Homebrew
ENV["HOMEBREW_API_UPDATED"] = "1"
begin
download_queue.fetch
download_queue.fetch(heading: "Downloading Homebrew API data")
ensure
download_queue.shutdown
end
+3 -3
View File
@@ -57,9 +57,9 @@ module Cask
unless skip_prefetch
Homebrew::Install.enqueue_cask_installers(cask_installers, download_queue:)
oh1 "Fetching downloads for: #{casks.map { |cask| Formatter.identifier(cask.full_name) }.to_sentence}",
truncate: false
download_queue.fetch
download_queue.fetch(
heading: Homebrew::Install.combined_fetch_downloads_heading(cask_names: casks.map(&:full_name)),
)
end
ensure
download_queue.shutdown if created_download_queue
+5 -5
View File
@@ -241,13 +241,13 @@ module Cask
end
fetchable_casks = upgradable_casks.map(&:last)
fetchable_casks_sentence = fetchable_casks.map { |cask| Formatter.identifier(cask.full_name) }.to_sentence
Homebrew::Install.enqueue_cask_installers(fetchable_cask_installers,
download_queue: prefetch_download_queue)
if fetchable_casks.any?
oh1 "Fetching downloads for: #{fetchable_casks_sentence}", truncate: false
prefetch_download_queue.fetch
end
prefetch_download_queue.fetch(
heading: Homebrew::Install.combined_fetch_downloads_heading(
cask_names: fetchable_casks.map(&:full_name),
),
)
ensure
prefetch_download_queue.shutdown if created_download_queue
end
+34 -31
View File
@@ -276,9 +276,6 @@ module Homebrew
return if formulae.any? && installed_formulae.empty? && casks.empty?
Install.perform_preinstall_checks_once
Install.check_cc_argv(args.cc)
formulae_installer = Install.formula_installers(
installed_formulae,
installed_on_request: !args.as_dependency?,
@@ -305,14 +302,20 @@ module Homebrew
)
shared_download_queue = T.let(nil, T.nilable(Homebrew::DownloadQueue))
if !ask && !args.dry_run? && formulae_installer.any?
if !args.dry_run? && formulae_installer.any?
shared_download_queue = Homebrew::DownloadQueue.new(pour: true)
# Start bottle manifest (and, once downloads are confirmed, bottle)
# transfers before the local-only work below.
formulae_installer = Install.prelude_fetch_formulae(formulae_installer,
download_queue: shared_download_queue)
download_queue: shared_download_queue,
metadata_only: ask)
end
dependants = begin
Upgrade.dependants(
begin
Install.perform_preinstall_checks_once
Install.check_cc_argv(args.cc)
dependants = Upgrade.dependants(
installed_formulae,
flags: args.flags_only,
ask: ask,
@@ -328,30 +331,31 @@ module Homebrew
verbose: args.verbose?,
dry_run: args.dry_run?,
)
# Ensure the early download queue is shut down on interrupts.
# Main block: if asking the user is enabled, show dry-run information.
if ask
shared_download_queue&.fetch(heading: "Downloading bottle manifests")
Install.ask_formulae(
formulae_installer,
dependants,
flags: args.flags_only,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
)
end
# Ensure the early download queue is shut down on interrupts and declined prompts.
rescue Exception # rubocop:disable Lint/RescueException
shared_download_queue&.shutdown
raise
end
# Main block: if asking the user is enabled, show dry-run information.
if ask
Install.ask_formulae(
formulae_installer,
dependants,
flags: args.flags_only,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
)
end
if !args.dry_run? && (formulae_installer.any? || fetch_casks.any?)
download_queue = T.let(shared_download_queue || Homebrew::DownloadQueue.new(pour: true),
Homebrew::DownloadQueue)
@@ -360,10 +364,6 @@ module Homebrew
Cask::Upgrade.show_upgrade_summary(
upgrade_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" },
)
Install.show_combined_fetch_downloads_heading(
formula_names: formulae_installer.map { |fi| fi.formula.name },
cask_names: fetch_casks.map(&:full_name),
)
formulae_installer = Install.enqueue_formulae(formulae_installer, download_queue:)
@@ -386,7 +386,10 @@ module Homebrew
Install.enqueue_cask_installers(fetch_cask_installers, download_queue:)
end
download_queue.fetch
download_queue.fetch(heading: Install.combined_fetch_downloads_heading(
formula_names: formulae_installer.map { |fi| fi.formula.name },
cask_names: fetch_casks.map(&:full_name),
))
ensure
download_queue.shutdown
end
+4 -6
View File
@@ -244,11 +244,6 @@ module Homebrew
shared_download_queue ||= Homebrew::DownloadQueue.new(pour: true)
download_queue = shared_download_queue
begin
Install.show_combined_fetch_downloads_heading(
formula_names: formulae_installers.map { |fi| fi.formula.name },
cask_names: casks.map(&:full_name),
)
valid_formula_installers = Install.enqueue_formulae(formulae_installers,
download_queue:)
@@ -268,7 +263,10 @@ module Homebrew
)
end
Install.enqueue_cask_installers(fetch_cask_installers, download_queue:)
download_queue.fetch
download_queue.fetch(heading: Install.combined_fetch_downloads_heading(
formula_names: valid_formula_installers.map { |fi| fi.formula.name },
cask_names: casks.map(&:full_name),
))
casks_prefetched = true
valid_formula_installers
ensure
+23 -43
View File
@@ -263,21 +263,19 @@ module Homebrew
begin
formulae_prefetched = upgrade_outdated_formulae!(
formulae,
prefetch_only: true,
download_queue: shared_download_queue,
prefetch_names: prefetched_formulae_names,
prefetch_upgrades: prefetched_formulae_upgrades,
show_upgrade_summary: false,
show_downloads_heading: false,
prefetch_only: true,
download_queue: shared_download_queue,
prefetch_names: prefetched_formulae_names,
prefetch_upgrades: prefetched_formulae_upgrades,
show_upgrade_summary: false,
)
prefetched_casks = prefetch_outdated_casks!(
casks,
download_queue: shared_download_queue,
prefetch_names: prefetched_cask_names,
prefetch_upgrades: prefetched_cask_upgrades,
prefetch_casks: prefetched_cask_upgrade_casks,
prefetch_errors: prefetched_cask_errors,
show_downloads_heading: false,
download_queue: shared_download_queue,
prefetch_names: prefetched_cask_names,
prefetch_upgrades: prefetched_cask_upgrades,
prefetch_casks: prefetched_cask_upgrade_casks,
prefetch_errors: prefetched_cask_errors,
)
unless ask
Cask::Upgrade.show_upgrade_summary(
@@ -285,11 +283,10 @@ module Homebrew
dry_run: args.dry_run?,
)
end
Install.show_combined_fetch_downloads_heading(
shared_download_queue.fetch(heading: Install.combined_fetch_downloads_heading(
formula_names: prefetched_formulae_names,
cask_names: prefetched_cask_names,
)
shared_download_queue.fetch
))
if shared_download_queue.fetch_failed
formulae_prefetched = false
prefetched_casks = false
@@ -414,12 +411,6 @@ module Homebrew
Install.perform_preinstall_checks_once
if formulae_to_install.any? do |formula|
formula.bottle&.github_packages_manifest_resource&.downloaded_and_valid? == false
end
oh1 "Downloading bottle manifests"
end
formulae_installer = Upgrade.formula_installers(
formulae_to_install,
flags: args.flags_only,
@@ -602,15 +593,14 @@ module Homebrew
sig {
params(
formulae: T::Array[Formula],
prefetch_only: T::Boolean,
use_prefetched: T::Boolean,
dry_run: T::Boolean,
download_queue: T.nilable(Homebrew::DownloadQueue),
prefetch_names: T.nilable(T::Array[String]),
prefetch_upgrades: T.nilable(T::Array[String]),
show_upgrade_summary: T::Boolean,
show_downloads_heading: T::Boolean,
formulae: T::Array[Formula],
prefetch_only: T::Boolean,
use_prefetched: T::Boolean,
dry_run: T::Boolean,
download_queue: T.nilable(Homebrew::DownloadQueue),
prefetch_names: T.nilable(T::Array[String]),
prefetch_upgrades: T.nilable(T::Array[String]),
show_upgrade_summary: T::Boolean,
).returns(T::Boolean)
}
def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: false,
@@ -618,8 +608,7 @@ module Homebrew
download_queue: nil,
prefetch_names: nil,
prefetch_upgrades: nil,
show_upgrade_summary: true,
show_downloads_heading: true)
show_upgrade_summary: true)
return false if args.cask?
use_prefetched_context = use_prefetched && @prefetched_formulae_upgrade_context
@@ -634,11 +623,6 @@ module Homebrew
prefetch_download_queue = download_queue || Homebrew.default_download_queue
valid_formula_installers = Install.enqueue_formulae(context.formulae_installer,
download_queue: prefetch_download_queue)
if show_downloads_heading
Install.show_combined_fetch_downloads_heading(
formula_names: valid_formula_installers.map { |fi| fi.formula.name },
)
end
prefetch_names&.replace(valid_formula_installers.map { |fi| fi.formula.name })
prefetch_upgrades&.replace(formula_upgrade_descriptions(valid_formula_installers.map(&:formula)))
@prefetched_formulae_upgrade_context = FormulaeUpgradeContext.new(
@@ -719,13 +703,11 @@ module Homebrew
prefetch_names: T.nilable(T::Array[String]),
prefetch_upgrades: T.nilable(T::Array[String]),
prefetch_casks: T.nilable(T::Array[Cask::Cask]),
prefetch_errors: T.nilable(T::Array[StandardError]),
show_downloads_heading: T::Boolean)
prefetch_errors: T.nilable(T::Array[StandardError]))
.returns(T::Boolean)
}
def prefetch_outdated_casks!(casks, download_queue:, prefetch_names: nil,
prefetch_upgrades: nil, prefetch_casks: nil, prefetch_errors: nil,
show_downloads_heading: true)
prefetch_upgrades: nil, prefetch_casks: nil, prefetch_errors: nil)
return false if args.formula?
casks = minimum_version_casks(casks, quiet: true)
@@ -783,8 +765,6 @@ module Homebrew
prefetch_upgrades&.replace(
outdated_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" },
)
Install.show_combined_fetch_downloads_heading(cask_names:) if show_downloads_heading
true
rescue => e
ofail e
+37 -10
View File
@@ -100,15 +100,34 @@ module Homebrew
end
end
sig { void }
def fetch
# Waits for and reports queued downloads. With `only:`, limits that to
# downloadables of the given class, leaving the rest enqueued and
# unreported for a later fetch, e.g. so dependency resolution can wait
# on bottle manifests without reporting in-flight bottles before their
# downloads heading has been printed. A `heading:` is printed only when
# there is something to report, so every report gets a heading and empty
# fetches stay silent.
sig { params(only: T.nilable(T::Class[Downloadable]), heading: T.nilable(String)).void }
def fetch(only: nil, heading: nil)
@fetch_failed = false
@deferred_failure_messages = []
context_before_fetch = Context.current
return if downloads.empty?
fetchable_downloads = if only
downloads.select { |downloadable, _| downloadable.is_a?(only) }
else
downloads
end
return if fetchable_downloads.empty?
if heading
oh1 heading, truncate: false
# Reach the pipe before any unbuffered stderr report lines when
# stdout is block-buffered.
$stdout.flush
end
if concurrency == 1
downloads.each do |downloadable, promise|
fetchable_downloads.each do |downloadable, promise|
promise.wait!
rescue CancelledDownloadError
next
@@ -117,13 +136,15 @@ module Homebrew
ofail "#{downloadable.download_queue_type} reports different checksum: #{e.expected}"
end
else
message_length_max = downloads.keys.map { |download| download.download_queue_message.length }.max || 0
remaining_downloads = downloads.dup.to_a
message_length_max = fetchable_downloads.keys.map do |download|
download.download_queue_message.length
end.max || 0
remaining_downloads = fetchable_downloads.dup.to_a
previous_pending_line_count = 0
max_lines = [concurrency, Tty.height].min
resolution = Concurrent::Event.new
downloads.each_value { |future| future.on_resolution! { resolution.set } }
fetchable_downloads.each_value { |future| future.on_resolution! { resolution.set } }
begin
stdout_print_and_flush_if_tty Tty.hide_cursor
@@ -250,9 +271,15 @@ module Homebrew
# aborts the fetch above.
Context.current = context_before_fetch if context_before_fetch
downloads.clear
@downloads_by_location.clear
@symlink_targets.clear
if only
# Keep unfetched downloads (and their location dedup entries) queued
# for the next fetch.
fetchable_downloads.each_key { |downloadable| downloads.delete(downloadable) }
else
downloads.clear
@downloads_by_location.clear
@symlink_targets.clear
end
end
sig { returns(T::Boolean) }
+62 -39
View File
@@ -146,10 +146,12 @@ class FormulaInstaller
@download_queue = T.let(Homebrew.default_download_queue, Homebrew::DownloadQueue)
@api_bottle = T.let(nil, T.nilable(Bottle))
@api_bottle_loaded = T.let(false, T::Boolean)
@enqueued_bottle_download = T.let(nil, T.nilable(Downloadable))
# 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
@ran_prelude_fetch_metadata = T.let(false, T::Boolean)
@ran_prelude_fetch = T.let(false, T::Boolean)
@ran_prelude = T.let(false, T::Boolean)
end
@@ -298,41 +300,47 @@ class FormulaInstaller
) || false
end
sig { void }
def prelude_fetch
return if @ran_prelude_fetch
sig { params(metadata_only: T::Boolean).void }
def prelude_fetch(metadata_only: false)
unless @ran_prelude_fetch_metadata
deprecate_disable_type = DeprecateDisable.type(formula)
if deprecate_disable_type.present?
message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}"
deprecate_disable_type = DeprecateDisable.type(formula)
if deprecate_disable_type.present?
message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}"
case deprecate_disable_type
when :deprecated
opoo message
when :disabled
if force?
case deprecate_disable_type
when :deprecated
opoo message
else
GitHub::Actions.puts_annotation_if_env_set!(:error, message)
raise CannotInstallFormulaError, message
when :disabled
if force?
opoo message
else
GitHub::Actions.puts_annotation_if_env_set!(:error, message)
raise CannotInstallFormulaError, message
end
end
end
# Run the formula-self forbidden checks before any source or bottle
# download is enqueued so a forbidden formula never triggers a fetch.
forbidden_tap_check(formula_only: true)
forbidden_formula_check(formula_only: true)
# Needs to be done before expand_dependencies for compute_dependencies
fetch_bottle_tab(enqueue: true) if pour_bottle?
fetch_fetch_deps unless ignore_deps?
@ran_prelude_fetch_metadata = true
end
# Run the formula-self forbidden checks before any source or bottle
# download is enqueued so a forbidden formula never triggers a fetch.
forbidden_tap_check(formula_only: true)
forbidden_formula_check(formula_only: true)
return if metadata_only || @ran_prelude_fetch
if pour_bottle?
# Needs to be done before expand_dependencies for compute_dependencies
fetch_bottle_tab(enqueue: true)
@enqueued_bottle_download = enqueue_bottle_download(stage: true)
elsif formula.loaded_from_api?
Homebrew::API::Formula.source_download(formula, download_queue:, enqueue: true)
end
fetch_fetch_deps unless ignore_deps?
@ran_prelude_fetch = true
end
@@ -1490,7 +1498,7 @@ on_request: installed_on_request?, options:)
sig { void }
def fetch
enqueue_fetch
download_queue.fetch
download_queue.fetch(heading: "Fetching downloads for: #{Formatter.identifier(formula.full_name)}")
end
sig { void }
@@ -1498,23 +1506,14 @@ on_request: installed_on_request?, options:)
return if previously_fetched_formula
downloadable_object = T.let(nil, T.nilable(Downloadable))
bottle_download = T.let(nil, T.nilable(Downloadable))
check_attestation = T.let(false, T::Boolean)
local_bottle_path = formula.local_bottle_path
bottle_install = !only_deps? && local_bottle_path.nil? && pour_bottle?(output_warning: true)
# 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.
# We also skip bottle installs from local bottle paths, as these are done in CI
# We skip bottle installs from local bottle paths, as these are done in CI
# as part of the build lifecycle before attestations are produced.
verify_attestation = bottle_install &&
Homebrew::EnvConfig.verify_attestations? &&
(formula.tap&.core_tap? || false) &&
formula.name != "gh"
if bottle_install && @ran_prelude
bottle_download = downloadable
check_attestation = verify_attestation && !bottle_download.cached_download.exist?
download_queue.enqueue(bottle_download, check_attestation:, stage: false)
end
verify_attestation = bottle_install && verify_bottle_attestation?
bottle_download = @enqueued_bottle_download
bottle_download = enqueue_bottle_download(stage: false) if bottle_download.nil? && bottle_install && @ran_prelude
fetch_dependencies
@@ -1535,8 +1534,10 @@ on_request: installed_on_request?, options:)
downloadable_object = downloadable
end
# Check attestation after download completes.
download_queue.enqueue(downloadable_object, check_attestation:)
# Check attestation after download completes. Skip downloads already
# enqueued (with staging) by `prelude_fetch` so a completed early fetch is
# not requeued and reported a second time.
download_queue.enqueue(downloadable_object, check_attestation:) if @enqueued_bottle_download.nil?
self.class.fetched << formula
rescue CannotInstallFormulaError
@@ -1547,6 +1548,28 @@ on_request: installed_on_request?, options:)
raise
end
# Start the formula's own bottle download without waiting for its bottle
# manifest or dependency resolution; both call sites have already checked
# `pour_bottle?`.
sig { params(stage: T::Boolean).returns(T.nilable(Downloadable)) }
def enqueue_bottle_download(stage:)
return if only_deps? || formula.local_bottle_path
bottle_download = downloadable
check_attestation = verify_bottle_attestation? && !bottle_download.cached_download.exist?
download_queue.enqueue(bottle_download, check_attestation:, stage:)
bottle_download
end
sig { returns(T::Boolean) }
def verify_bottle_attestation?
# 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.
Homebrew::EnvConfig.verify_attestations? &&
(formula.tap&.core_tap? || false) &&
formula.name != "gh"
end
sig { returns(Downloadable) }
def downloadable
if (bottle_path = formula.local_bottle_path)
+33 -19
View File
@@ -344,25 +344,28 @@ module Homebrew
shutdown_download_queue: true,
show_downloads_heading: true
)
formulae_names_to_install = formula_installers.map { |fi| fi.formula.name }
return formula_installers if formulae_names_to_install.empty?
return formula_installers if formula_installers.empty?
download_queue = T.let(download_queue || Homebrew::DownloadQueue.new(pour: true), Homebrew::DownloadQueue)
if show_downloads_heading
formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence
oh1 "Fetching downloads for: #{formula_sentence}", truncate: false
end
begin
valid_formula_installers = prelude_fetch_formulae(formula_installers, download_queue:)
download_queue.fetch
# Wait on just the bottle manifests dependency resolution needs so
# in-flight bottles are only reported under the downloads heading.
download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests")
[:prelude, :enqueue_fetch].each do |step|
valid_formula_installers = select_formula_installers(valid_formula_installers, step:)
next if step == :enqueue_fetch && !fetch_after_enqueue
download_queue.fetch
if step == :prelude
download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests")
else
heading = if show_downloads_heading
combined_fetch_downloads_heading(formula_names: valid_formula_installers.map { |fi| fi.formula.name })
end
download_queue.fetch(heading:)
end
end
ensure
download_queue.shutdown if shutdown_download_queue
@@ -375,22 +378,34 @@ module Homebrew
params(
formula_installers: T::Array[FormulaInstaller],
download_queue: Homebrew::DownloadQueue,
metadata_only: T::Boolean,
).returns(T::Array[FormulaInstaller])
}
def prelude_fetch_formulae(formula_installers, download_queue:)
def prelude_fetch_formulae(formula_installers, download_queue:, metadata_only: false)
formula_installers.each do |fi|
fi.download_queue = download_queue
end
select_formula_installers(formula_installers, step: :prelude_fetch)
# Only pass the keyword when limiting the fetch so mocks and
# overrides expecting the historical no-argument call keep working.
action = ->(fi) { metadata_only ? fi.prelude_fetch(metadata_only: true) : fi.prelude_fetch }
select_formula_installers(formula_installers, action:)
end
sig {
params(formula_installers: T::Array[FormulaInstaller], step: Symbol).returns(T::Array[FormulaInstaller])
params(
formula_installers: T::Array[FormulaInstaller],
step: T.nilable(Symbol),
action: T.nilable(T.proc.params(formula_installer: FormulaInstaller).void),
).returns(T::Array[FormulaInstaller])
}
def select_formula_installers(formula_installers, step:)
def select_formula_installers(formula_installers, step: nil, action: nil)
formula_installers.select do |fi|
fi.public_send(step)
if action
action.call(fi)
elsif step
fi.public_send(step)
end
true
rescue CannotInstallFormulaError => e
ofail e.message
@@ -412,13 +427,13 @@ module Homebrew
)
end
sig { params(formula_names: T::Array[String], cask_names: T::Array[String]).void }
def show_combined_fetch_downloads_heading(formula_names: [], cask_names: [])
sig { params(formula_names: T::Array[String], cask_names: T::Array[String]).returns(T.nilable(String)) }
def combined_fetch_downloads_heading(formula_names: [], cask_names: [])
combined_fetch_targets = formula_names.map { |name| Formatter.identifier(name) } +
cask_names.map { |name| Formatter.identifier(name) }
return if combined_fetch_targets.empty?
oh1 "Fetching downloads for: #{combined_fetch_targets.to_sentence}", truncate: false
"Fetching downloads for: #{combined_fetch_targets.to_sentence}"
end
sig { params(cask_installers: T::Array[T.untyped], download_queue: Homebrew::DownloadQueue).void }
@@ -436,9 +451,8 @@ module Homebrew
end
if source_downloads.any?
oh1 "Downloading Cask files"
source_downloads.each { |source_download| download_queue.enqueue(source_download) }
download_queue.fetch
download_queue.fetch(only: Cask::Download, heading: "Downloading Cask files")
end
valid_cask_installers.each do |cask_installer|
+40 -2
View File
@@ -351,7 +351,7 @@ RSpec.describe Homebrew::Cmd::InstallCmd do
allow(Homebrew.messages).to receive(:display_messages)
expect(Homebrew::DownloadQueue).to receive(:new).ordered.and_return(download_queue)
expect(formula_installer).to receive(:download_queue=).with(download_queue).ordered
expect(formula_installer).to receive(:prelude_fetch).ordered
expect(formula_installer).to receive(:prelude_fetch).with(no_args).ordered
expect(Homebrew::Upgrade).to receive(:dependants).ordered.and_return(dependants)
expect(Homebrew::Install).to receive(:enqueue_formulae)
.with([formula_installer], download_queue:)
@@ -363,6 +363,43 @@ RSpec.describe Homebrew::Cmd::InstallCmd do
cmd.run
end
it "drains metadata-only prelude fetches before the dry-run plan when asking" do
cmd = described_class.new(["testball"])
download_queue = instance_double(Homebrew::DownloadQueue, shutdown: nil,
downloads: { instance_double(Downloadable) => nil })
formula = formula("testball") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/testball-0.1.tar.gz"
end
formula_installer = instance_double(FormulaInstaller, formula:)
dependants = Homebrew::Upgrade::Dependents.new(upgradeable: [], pinned: [], skipped: [])
allow(Tap).to receive_messages(with_formula_name: nil, with_cask_token: nil)
allow(Homebrew::Trust).to receive(:trust_fully_qualified_items!)
allow(cmd.args.named).to receive(:to_formulae_and_casks).with(warn: false).and_return([formula])
allow(Homebrew::Install).to receive(:perform_preinstall_checks_once)
allow(Homebrew::Install).to receive(:check_cc_argv)
allow(Homebrew::Install).to receive_messages(install_formula?: true, formula_installers: [formula_installer])
allow(Homebrew::Install).to receive(:install_formulae)
allow(Homebrew::Upgrade).to receive(:upgrade_dependents)
allow(Homebrew::Cleanup).to receive(:periodic_clean!)
allow(Homebrew.messages).to receive(:display_messages)
expect(Homebrew::DownloadQueue).to receive(:new).ordered.and_return(download_queue)
expect(formula_installer).to receive(:download_queue=).with(download_queue).ordered
expect(formula_installer).to receive(:prelude_fetch).with(metadata_only: true).ordered
expect(Homebrew::Upgrade).to receive(:dependants).ordered.and_return(dependants)
expect(download_queue).to receive(:fetch).ordered
expect(Homebrew::Install).to receive(:ask_formulae).ordered
expect(Homebrew::Install).to receive(:enqueue_formulae)
.with([formula_installer], download_queue:)
.ordered
.and_return([formula_installer])
expect(download_queue).to receive(:fetch).ordered
expect(download_queue).to receive(:shutdown).ordered
cmd.run
end
it "does not install `homebrew/cask` when a cask remains unavailable" do
cmd = described_class.new(["foo"])
cask_tap = CoreCaskTap.instance
@@ -505,11 +542,12 @@ RSpec.describe Homebrew::Cmd::InstallCmd do
true
end
expect(download_queue).to receive(:fetch)
.with(heading: "Fetching downloads for: testball_bottle and codex")
expect { cmd.run }.to output(<<~EOS).to_stdout
==> Upgrading 1 outdated package:
codex 0.117.0 -> 0.118.0
==> Fetching downloads for: testball_bottle and codex
EOS
end
end
@@ -68,7 +68,6 @@ RSpec.describe Homebrew::Cmd::Reinstall do
allow(Homebrew::Reinstall).to receive(:build_install_context).and_return(reinstall_context)
allow(Homebrew::Upgrade).to receive(:dependants).and_return(dependants)
allow(Homebrew::Install).to receive(:ask_formulae)
allow(Homebrew::Install).to receive(:show_combined_fetch_downloads_heading)
allow(Homebrew::Install).to receive(:enqueue_formulae).and_return([formula_installer])
allow(Homebrew::Install).to receive(:enqueue_cask_installers)
allow(Cask::Installer).to receive(:new).and_return(instance_double(Cask::Installer))
+26 -49
View File
@@ -427,12 +427,11 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
expect(cmd).to receive(:upgrade_outdated_formulae!)
.with(
[],
prefetch_only: true,
prefetch_only: true,
download_queue:,
prefetch_names: [],
prefetch_upgrades: [],
show_upgrade_summary: false,
show_downloads_heading: false,
prefetch_names: [],
prefetch_upgrades: [],
show_upgrade_summary: false,
)
.ordered
.and_return(true)
@@ -440,11 +439,10 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
.with(
[],
download_queue:,
prefetch_names: [],
prefetch_upgrades: [],
prefetch_casks: [],
prefetch_errors: [],
show_downloads_heading: false,
prefetch_names: [],
prefetch_upgrades: [],
prefetch_casks: [],
prefetch_errors: [],
)
.ordered
.and_return(true)
@@ -758,12 +756,13 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
allow(Homebrew::Cleanup).to receive(:periodic_clean!)
allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!)
allow(Homebrew.messages).to receive(:display_messages)
expect(download_queue).to receive(:fetch)
.with(heading: "Fetching downloads for: deno and codex")
expect { cmd.run }.to output(<<~EOS).to_stdout
==> Upgrading 2 outdated packages:
deno 2.7.10 -> 2.7.11
codex 0.117.0 -> 0.118.0
==> Fetching downloads for: deno and codex
EOS
end
@@ -885,19 +884,21 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
allow(Cask::Installer).to receive(:new).and_return(installer)
expect(installer).to receive(:prelude_fetch_download).and_return(source_download)
expect(download_queue).to receive(:enqueue).with(source_download).ordered
expect(download_queue).to receive(:fetch).ordered
expect(download_queue).to receive(:fetch).ordered
expect(download_queue).to receive(:fetch)
.with(only: Cask::Download, heading: "Downloading Cask files")
.ordered
expect(download_queue).to receive(:fetch)
.with(heading: "Fetching downloads for: deno and codex")
.ordered
allow(Cask::Upgrade).to receive_messages(outdated_casks: [cask], upgrade_casks!: true)
allow(Homebrew::Cleanup).to receive(:periodic_clean!)
allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!)
allow(Homebrew.messages).to receive(:display_messages)
expect { cmd.run }.to output(<<~EOS).to_stdout
==> Downloading Cask files
==> Upgrading 2 outdated packages:
deno 2.7.10 -> 2.7.11
codex 0.117.0 -> 0.118.0
==> Fetching downloads for: deno and codex
EOS
end
@@ -944,7 +945,6 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
prefetch_upgrades:,
prefetch_casks:,
prefetch_errors:,
show_downloads_heading: false,
),
).to be(true)
expect(prefetch_names).to eq(["codex"])
@@ -986,7 +986,9 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
end
allow(Cask::Installer).to receive(:new).and_return(installer)
expect(installer).to receive(:prelude_fetch_download).and_return(nil)
expect(download_queue).to receive(:fetch).once
expect(download_queue).to receive(:fetch)
.with(heading: "Fetching downloads for: deno and codex")
.once
allow(Cask::Upgrade).to receive_messages(outdated_casks: [cask], upgrade_casks!: true)
allow(Homebrew::Cleanup).to receive(:periodic_clean!)
allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!)
@@ -996,12 +998,10 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
==> Upgrading 2 outdated packages:
deno 2.7.10 -> 2.7.11
codex 0.117.0 -> 0.118.0
==> Fetching downloads for: deno and codex
EOS
end
it "prints a bottle manifest heading before formula prefetches" do
cmd = described_class.new([])
it "passes a bottle manifest heading to the tab prefetch queue" do
formula = formula("deno") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/deno-2.7.11.tar.gz"
@@ -1012,37 +1012,14 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
end
end
download_queue = instance_double(Homebrew::DownloadQueue, enqueue: nil, shutdown: nil)
allow(formula).to receive_messages(outdated?: true, latest_formula: formula, latest_version_installed?: false)
allow(Homebrew::Install).to receive(:perform_preinstall_checks_once)
allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([])
allow(formula).to receive(:latest_formula).and_return(formula)
allow(Migrator).to receive(:migrate_if_needed)
allow(Homebrew::DownloadQueue).to receive(:new).and_return(download_queue)
expect(download_queue).to receive(:fetch).with(heading: "Downloading bottle manifests")
expect do
cmd.formulae_upgrade_context([formula], show_upgrade_summary: false)
end.to output("==> Downloading bottle manifests\n").to_stdout
end
it "omits the bottle manifest heading for cached formula manifests" do
cmd = described_class.new([])
formula = formula("deno") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/deno-2.7.11.tar.gz"
bottle do
root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN
sha256 cellar: :any_skip_relocation,
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97"
end
end
allow(formula).to receive_messages(outdated?: true, latest_formula: formula, latest_version_installed?: false)
allow(formula.bottle&.github_packages_manifest_resource).to receive(:downloaded_and_valid?).and_return(true)
allow(Homebrew::Install).to receive(:perform_preinstall_checks_once)
allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([])
expect do
cmd.formulae_upgrade_context([formula], show_upgrade_summary: false)
end.not_to output(/Downloading bottle manifests/).to_stdout
Homebrew::Upgrade.formula_installers([formula], flags: [])
end
it "does not trust failed shared prefetches" do
@@ -71,6 +71,28 @@ RSpec.describe Homebrew::DownloadQueue do
expect(failure_index).to be > show_cursor_index
end
it "fetches only downloads of the given class and keeps others queued unreported" do
manifest = instance_double(
Resource::BottleManifest,
cached_download: HOMEBREW_CACHE/"downloads/testball_manifest.json",
checksum: nil,
downloaded_and_valid?: true,
downloader: nil,
download_queue_message: "Bottle Manifest testball",
download_queue_name: "testball",
download_queue_type: "Bottle Manifest",
)
allow(manifest).to receive(:is_a?) { |klass| klass == Resource::BottleManifest }
download_queue.enqueue(manifest)
download_queue.enqueue(downloadable)
expect do
download_queue.fetch(only: Resource::BottleManifest)
end.to output(/Bottle Manifest testball/).to_stderr
expect(download_queue.downloads.keys).to eq [downloadable]
end
it "raises and clears queue state on a bottle manifest failure in parallel mode" do
allow(retryable_download).to receive(:fetch).and_raise(Resource::BottleManifest::Error.new("manifest missing"))
+74 -32
View File
@@ -380,6 +380,17 @@ RSpec.describe FormulaInstaller do
installer.enqueue_fetch
end
it "does not requeue a bottle already enqueued by the prelude fetch" do
allow(installer).to receive(:pour_bottle?).and_return(true)
expect(download_queue).to receive(:enqueue)
.with(bottle, check_attestation: false, stage: true).once
installer.prelude_fetch
expect(installer).to receive(:fetch_dependencies)
installer.enqueue_fetch
end
end
describe "linking defaults" do
@@ -1254,41 +1265,72 @@ RSpec.describe FormulaInstaller do
end
describe "#prelude_fetch" do
it "uses API bottle metadata for API-loaded formula manifests" do
formula = formula("deno") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/deno-2.7.11.tar.gz"
context "with an API-loaded bottled formula" do
let(:deno_formula) do
formula("deno") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/deno-2.7.11.tar.gz"
end
end
let(:formula_struct) do
Homebrew::API::FormulaStruct.new(
bottle_checksums: [
{
cellar: :any_skip_relocation,
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
},
],
bottle_present: true,
desc: "deno",
homepage: "https://brew.sh",
license: "MIT",
ruby_source_checksum: "abc123",
stable_present: true,
stable_version: "2.7.11",
)
end
let(:installer) do
installer = described_class.new(deno_formula, ignore_deps: true)
installer.download_queue = instance_double(Homebrew::DownloadQueue)
installer
end
formula_struct = Homebrew::API::FormulaStruct.new(
bottle_checksums: [
{
cellar: :any_skip_relocation,
Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97",
},
],
bottle_present: true,
desc: "deno",
homepage: "https://brew.sh",
license: "MIT",
ruby_source_checksum: "abc123",
stable_present: true,
stable_version: "2.7.11",
)
installer = described_class.new(formula, ignore_deps: true)
installer.download_queue = instance_double(Homebrew::DownloadQueue)
allow(formula).to receive_messages(
bottle_tag?: true,
core_formula?: true,
loaded_from_internal_api?: true,
pour_bottle?: true,
)
allow(Homebrew::API::Internal).to receive(:formula_struct).with("deno").and_return(formula_struct)
expect(formula).not_to receive(:bottle_for_tag)
expect(formula).not_to receive(:bottle)
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest))
before do
allow(deno_formula).to receive_messages(
bottle_tag?: true,
core_formula?: true,
loaded_from_internal_api?: true,
pour_bottle?: true,
)
allow(Homebrew::API::Internal).to receive(:formula_struct).with("deno").and_return(formula_struct)
end
installer.prelude_fetch
it "uses API bottle metadata to enqueue the manifest and bottle" do
expect(deno_formula).not_to receive(:bottle_for_tag)
expect(deno_formula).not_to receive(:bottle)
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest))
expect(installer.download_queue).to receive(:enqueue)
.with(an_instance_of(Bottle), check_attestation: false, stage: true)
installer.prelude_fetch
end
it "enqueues only the bottle manifest when fetching metadata" do
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest))
installer.prelude_fetch(metadata_only: true)
end
it "enqueues the bottle without repeating metadata work after a metadata-only run" do
expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)).once
installer.prelude_fetch(metadata_only: true)
expect(installer.download_queue).to receive(:enqueue)
.with(an_instance_of(Bottle), check_attestation: false, stage: true)
installer.prelude_fetch
end
end
it "does not repeat source download prelude work" do
+1 -1
View File
@@ -134,7 +134,7 @@ module Homebrew
end
end
download_queue.fetch
download_queue.fetch(heading: "Downloading bottle manifests")
ensure
download_queue.shutdown
end
+4 -1
View File
@@ -54,7 +54,10 @@ module Homebrew
if defined?(Homebrew::API::Internal)
instrument(Homebrew::API::Internal.singleton_class, :formula_struct, "api_metadata_load")
end
instrument(Homebrew::Install.singleton_class, :formula_installers, "planning") if defined?(Homebrew::Install)
if defined?(Homebrew::Install)
instrument(Homebrew::Install.singleton_class, :formula_installers, "planning")
instrument(Homebrew::Install.singleton_class, :perform_preinstall_checks_once, "preinstall_checks")
end
if defined?(FormulaInstaller)
instrument(FormulaInstaller, :prelude, "planning")
instrument(FormulaInstaller, :compute_dependencies, "dependency_resolution")