brew/Library/Homebrew/cask/upgrade.rb
Mike McQuaid 64311c2889
Add Cask::Installer#prelude to check before download queueing
Fixes #20374

When using HOMEBREW_DOWNLOAD_CONCURRENCY, cask binaries were being
downloaded before checking if the cask could actually be installed
(e.g., disabled casks or conflict checks). This resulted in unnecessary
downloads for casks that would ultimately fail to install.

This change adds a `prelude` method to Cask::Installer that performs
early validation checks (deprecation/disable status and conflicts)
similar to Formula#prelude_fetch. The prelude method is called before
enqueueing downloads in all download queue scenarios (install, reinstall,
and upgrade commands), ensuring that validation failures occur before
the "Fetching downloads for:" message is displayed.

Key changes:
- Add Cask::Installer#prelude method with @ran_prelude tracking
- Call prelude before enqueueing downloads in install/reinstall/upgrade
- Refactor to avoid creating installer objects multiple times
- Maintain backward compatibility for non-download-queue scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 14:38:24 +01:00

256 lines
8.2 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "env_config"
require "cask/config"
module Cask
class Upgrade
sig {
params(
casks: Cask,
args: Homebrew::CLI::Args,
force: T.nilable(T::Boolean),
greedy: T.nilable(T::Boolean),
greedy_latest: T.nilable(T::Boolean),
greedy_auto_updates: T.nilable(T::Boolean),
dry_run: T.nilable(T::Boolean),
skip_cask_deps: T.nilable(T::Boolean),
verbose: T.nilable(T::Boolean),
quiet: T.nilable(T::Boolean),
binaries: T.nilable(T::Boolean),
quarantine: T.nilable(T::Boolean),
require_sha: T.nilable(T::Boolean),
).returns(T::Boolean)
}
def self.upgrade_casks!(
*casks,
args:,
force: false,
greedy: false,
greedy_latest: false,
greedy_auto_updates: false,
dry_run: false,
skip_cask_deps: false,
verbose: false,
quiet: false,
binaries: nil,
quarantine: nil,
require_sha: nil
)
quarantine = true if quarantine.nil?
greedy = true if Homebrew::EnvConfig.upgrade_greedy?
greedy_casks = if (upgrade_greedy_casks = Homebrew::EnvConfig.upgrade_greedy_casks.presence)
upgrade_greedy_casks.split
else
[]
end
outdated_casks = if casks.empty?
Caskroom.casks(config: Config.from_args(args)).select do |cask|
cask_greedy = greedy || greedy_casks.include?(cask.token)
cask.outdated?(greedy: cask_greedy, greedy_latest:,
greedy_auto_updates:)
end
else
casks.select do |cask|
raise CaskNotInstalledError, cask if !cask.installed? && !force
if cask.outdated?(greedy: true)
true
elsif cask.version.latest?
opoo "Not upgrading #{cask.token}, the downloaded artifact has not changed" unless quiet
false
else
opoo "Not upgrading #{cask.token}, the latest version is already installed" unless quiet
false
end
end
end
manual_installer_casks = outdated_casks.select do |cask|
cask.artifacts.any? do |artifact|
artifact.is_a?(Artifact::Installer) && artifact.manual_install
end
end
if manual_installer_casks.present?
count = manual_installer_casks.count
ofail "Not upgrading #{count} `installer manual` #{::Utils.pluralize("cask", count)}."
puts manual_installer_casks.map(&:to_s)
outdated_casks -= manual_installer_casks
end
return false if outdated_casks.empty?
if casks.empty? && !greedy && greedy_casks.empty?
if !greedy_auto_updates && !greedy_latest
ohai "Casks with 'auto_updates true' or 'version :latest' " \
"will not be upgraded; pass `--greedy` to upgrade them."
end
if greedy_auto_updates && !greedy_latest
ohai "Casks with 'version :latest' will not be upgraded; pass `--greedy-latest` to upgrade them."
end
if !greedy_auto_updates && greedy_latest
ohai "Casks with 'auto_updates true' will not be upgraded; pass `--greedy-auto-updates` to upgrade them."
end
end
upgradable_casks = outdated_casks.map do |c|
unless c.installed?
odie <<~EOS
The cask '#{c.token}' was affected by a bug and cannot be upgraded as-is. To fix this, run:
brew reinstall --cask --force #{c.token}
EOS
end
[CaskLoader.load(c.installed_caskfile), c]
end
return false if upgradable_casks.empty?
if !dry_run && Homebrew::EnvConfig.download_concurrency > 1
download_queue = Homebrew::DownloadQueue.new(pour: true)
fetchable_casks = upgradable_casks.map(&:last)
fetchable_cask_installers = fetchable_casks.map do |cask|
# This is significantly easier given the weird difference in Sorbet signatures here.
# rubocop:disable Style/DoubleNegation
Installer.new(cask, binaries: !!binaries, verbose: !!verbose, force: !!force,
skip_cask_deps: !!skip_cask_deps, require_sha: !!require_sha,
upgrade: true, quarantine:, download_queue:)
# rubocop:enable Style/DoubleNegation
end
fetchable_cask_installers.each(&:prelude)
fetchable_casks_sentence = fetchable_casks.map { |cask| Formatter.identifier(cask.full_name) }.to_sentence
oh1 "Fetching downloads for: #{fetchable_casks_sentence}", truncate: false
fetchable_cask_installers.each(&:enqueue_downloads)
download_queue.fetch
end
verb = dry_run ? "Would upgrade" : "Upgrading"
oh1 "#{verb} #{upgradable_casks.count} outdated #{::Utils.pluralize("package", upgradable_casks.count)}:"
caught_exceptions = []
puts upgradable_casks
.map { |(old_cask, new_cask)| "#{new_cask.full_name} #{old_cask.version} -> #{new_cask.version}" }
.join("\n")
return true if dry_run
upgradable_casks.each do |(old_cask, new_cask)|
upgrade_cask(
old_cask, new_cask,
binaries:, force:, skip_cask_deps:, verbose:,
quarantine:, require_sha:, download_queue:
)
rescue => e
new_exception = e.exception("#{new_cask.full_name}: #{e}")
new_exception.set_backtrace(e.backtrace)
caught_exceptions << new_exception
next
end
return true if caught_exceptions.empty?
raise MultipleCaskErrors, caught_exceptions if caught_exceptions.count > 1
raise caught_exceptions.fetch(0) if caught_exceptions.one?
false
end
sig {
params(
old_cask: Cask,
new_cask: Cask,
binaries: T.nilable(T::Boolean),
force: T.nilable(T::Boolean),
quarantine: T.nilable(T::Boolean),
require_sha: T.nilable(T::Boolean),
skip_cask_deps: T.nilable(T::Boolean),
verbose: T.nilable(T::Boolean),
download_queue: T.nilable(Homebrew::DownloadQueue),
).void
}
def self.upgrade_cask(
old_cask, new_cask,
binaries:, force:, quarantine:, require_sha:, skip_cask_deps:, verbose:, download_queue:
)
require "cask/installer"
start_time = Time.now
odebug "Started upgrade process for Cask #{old_cask}"
old_config = old_cask.config
old_options = {
binaries:,
verbose:,
force:,
upgrade: true,
}.compact
old_cask_installer =
Installer.new(old_cask, **old_options)
new_cask.config = new_cask.default_config.merge(old_config)
new_options = {
binaries:,
verbose:,
force:,
skip_cask_deps:,
require_sha:,
upgrade: true,
quarantine:,
download_queue:,
}.compact
new_cask_installer =
Installer.new(new_cask, **new_options)
started_upgrade = false
new_artifacts_installed = false
begin
oh1 "Upgrading #{Formatter.identifier(old_cask)}"
# Start new cask's installation steps
new_cask_installer.check_conflicts
if (caveats = new_cask_installer.caveats)
puts caveats
end
new_cask_installer.fetch
# Move the old cask's artifacts back to staging
old_cask_installer.start_upgrade(successor: new_cask)
# And flag it so in case of error
started_upgrade = true
# Install the new cask
new_cask_installer.stage
new_cask_installer.install_artifacts(predecessor: old_cask)
new_artifacts_installed = true
# If successful, wipe the old cask from staging.
old_cask_installer.finalize_upgrade
rescue => e
new_cask_installer.uninstall_artifacts(successor: old_cask) if new_artifacts_installed
new_cask_installer.purge_versioned_files
old_cask_installer.revert_upgrade(predecessor: new_cask) if started_upgrade
raise e
end
end_time = Time.now
Homebrew.messages.package_installed(new_cask.token, end_time - start_time)
end
end
end