Add brew bundle --upgrade-formulae

This flag allows you to specify formulae to upgrade, even if
`$HOMEBREW_BUNDLE_NO_UPGRADE` is set.

This is useful for upgrading specific formulae without upgrading all
formulae.

While we're here, let's add Sorbet signatures to the `Bundle` module
because I needed to add a new method there anyway.
This commit is contained in:
Mike McQuaid 2025-04-02 17:15:32 +01:00
parent e42c792fe3
commit 89d0309b9c
No known key found for this signature in database
13 changed files with 64 additions and 17 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "English"
@ -6,11 +6,22 @@ require "English"
module Homebrew
module Bundle
class << self
sig { params(args_upgrade_formula: T.nilable(String)).void }
def upgrade_formulae=(args_upgrade_formula)
@upgrade_formulae = args_upgrade_formula.to_s.split(",")
end
sig { returns(T::Array[String]) }
def upgrade_formulae
@upgrade_formulae || []
end
sig { params(cmd: T.any(String, Pathname), args: T.anything, verbose: T::Boolean).returns(T::Boolean) }
def system(cmd, *args, verbose: false)
return super cmd, *args if verbose
logs = []
success = T.let(nil, T.nilable(T::Boolean))
success = T.let(false, T::Boolean)
IO.popen([cmd, *args], err: [:child, :out]) do |pipe|
while (buf = pipe.gets)
logs << buf
@ -23,18 +34,22 @@ module Homebrew
success
end
sig { params(args: T.anything, verbose: T::Boolean).returns(T::Boolean) }
def brew(*args, verbose: false)
system(HOMEBREW_BREW_FILE, *args, verbose:)
end
sig { returns(T::Boolean) }
def mas_installed?
@mas_installed ||= which_formula("mas")
end
sig { returns(T::Boolean) }
def vscode_installed?
@vscode_installed ||= which_vscode.present?
end
sig { returns(T.nilable(Pathname)) }
def which_vscode
@which_vscode ||= which("code", ORIGINAL_PATHS)
@which_vscode ||= which("codium", ORIGINAL_PATHS)
@ -42,22 +57,26 @@ module Homebrew
@which_vscode ||= which("code-insiders", ORIGINAL_PATHS)
end
sig { returns(T::Boolean) }
def whalebrew_installed?
@whalebrew_installed ||= which_formula("whalebrew")
end
sig { returns(T::Boolean) }
def cask_installed?
@cask_installed ||= File.directory?("#{HOMEBREW_PREFIX}/Caskroom") &&
(File.directory?("#{HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask") ||
!Homebrew::EnvConfig.no_install_from_api?)
end
sig { params(name: String).returns(T::Boolean) }
def which_formula(name)
formula = Formulary.factory(name)
ENV["PATH"] = "#{formula.opt_bin}:#{ENV.fetch("PATH", nil)}" if formula.any_version_installed?
which(name).present?
end
sig { params(block: T.proc.returns(T.anything)).returns(T.untyped) }
def exchange_uid_if_needed!(&block)
euid = Process.euid
uid = Process.uid
@ -83,6 +102,7 @@ module Homebrew
return_value
end
sig { returns(T::Hash[String, String]) }
def formula_versions_from_env
@formula_versions_from_env ||= begin
formula_versions = {}
@ -106,11 +126,13 @@ module Homebrew
sig { void }
def reset!
@mas_installed = nil
@vscode_installed = nil
@whalebrew_installed = nil
@cask_installed = nil
@formula_versions_from_env = nil
@mas_installed = T.let(nil, T.nilable(T::Boolean))
@vscode_installed = T.let(nil, T.nilable(T::Boolean))
@which_vscode = T.let(nil, T.nilable(String))
@whalebrew_installed = T.let(nil, T.nilable(T::Boolean))
@cask_installed = T.let(nil, T.nilable(T::Boolean))
@formula_versions_from_env = T.let(nil, T.nilable(T::Hash[String, String]))
@upgrade_formulae = T.let(nil, T.nilable(T::Array[String]))
end
end
end

View File

@ -32,7 +32,7 @@ module Homebrew
end
def preinstall(no_upgrade: false, verbose: false)
if installed? && (no_upgrade || !upgradable?)
if installed? && (self.class.no_upgrade_with_args?(no_upgrade, @name) || !upgradable?)
puts "Skipping install of #{@name} formula. It is already installed." if verbose
@changed = nil
return false
@ -166,11 +166,15 @@ module Homebrew
def self.formula_installed_and_up_to_date?(formula, no_upgrade: false)
return false unless formula_installed?(formula)
return true if no_upgrade
return true if no_upgrade_with_args?(no_upgrade, formula)
!formula_upgradable?(formula)
end
def self.no_upgrade_with_args?(no_upgrade, formula_name)
no_upgrade && Bundle.upgrade_formulae.exclude?(formula_name)
end
def self.formula_in_array?(formula, array)
return true if array.include?(formula)
return true if array.include?(formula.split("/").last)

View File

@ -18,7 +18,7 @@ module Homebrew
end
def failure_reason(name, no_upgrade:)
reason = if no_upgrade
reason = if no_upgrade && Bundle.upgrade_formulae.exclude?(name)
"needs to be installed."
else
"needs to be installed or updated."

View File

@ -48,7 +48,7 @@ module Homebrew
Bundle.exchange_uid_if_needed! do
vscode_extensions.each do |extension|
Kernel.system(Bundle.which_vscode, "--uninstall-extension", extension)
Kernel.system(T.must(Bundle.which_vscode).to_s, "--uninstall-extension", extension)
end
end

View File

@ -31,7 +31,7 @@ module Homebrew
puts "Installing #{name} VSCode extension. It is not currently installed." if verbose
return false unless Bundle.exchange_uid_if_needed! do
Bundle.system(Bundle.which_vscode, "--install-extension", name, verbose:)
Bundle.system(T.must(Bundle.which_vscode), "--install-extension", name, verbose:)
end
installed_extensions << name

View File

@ -80,7 +80,10 @@ module Homebrew
"This is enabled by default if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set."
switch "--upgrade",
description: "`install` runs `brew upgrade` on outdated dependencies, " \
"even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set. "
"even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set."
flag "--upgrade-formulae=", "--upgrade-formula=",
description: "`install` runs `brew upgrade` on any of these comma-separated formulae, " \
"even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set."
switch "--install",
description: "Run `install` before continuing to other operations e.g. `exec`."
switch "--services",
@ -150,6 +153,7 @@ module Homebrew
verbose = args.verbose?
force = args.force?
zap = args.zap?
Homebrew::Bundle.upgrade_formulae = args.upgrade_formulae
no_type_args = !args.brews? && !args.casks? && !args.taps? && !args.mas? && !args.whalebrew? && !args.vscode?

View File

@ -71,6 +71,12 @@ class Homebrew::Cmd::Bundle::Args < Homebrew::CLI::Args
sig { returns(T::Boolean) }
def upgrade?; end
sig { returns(T.nilable(String)) }
def upgrade_formula; end
sig { returns(T.nilable(String)) }
def upgrade_formulae; end
sig { returns(T::Boolean) }
def vscode?; end

View File

@ -219,7 +219,7 @@ RSpec.describe Homebrew::Bundle::Commands::Cleanup do
end
it "uninstalls extensions" do
expect(Kernel).to receive(:system).with(Pathname("code"), "--uninstall-extension", "GitHub.codespaces")
expect(Kernel).to receive(:system).with("code", "--uninstall-extension", "GitHub.codespaces")
expect(described_class).to receive(:system_output_no_stderr).and_return("")
described_class.run(force: true)
end

View File

@ -667,6 +667,7 @@ _brew_bundle() {
--services
--tap
--upgrade
--upgrade-formulae
--verbose
--vscode
--whalebrew

View File

@ -521,7 +521,8 @@ __fish_brew_complete_arg 'bundle' -l no-vscode -d '`dump` without VSCode (and fo
__fish_brew_complete_arg 'bundle' -l quiet -d 'Make some output more quiet'
__fish_brew_complete_arg 'bundle' -l services -d 'Temporarily start services while running the `exec` or `sh` command'
__fish_brew_complete_arg 'bundle' -l tap -d '`list` or `dump` Homebrew tap dependencies'
__fish_brew_complete_arg 'bundle' -l upgrade -d '`install` runs `brew upgrade` on outdated dependencies, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set. '
__fish_brew_complete_arg 'bundle' -l upgrade -d '`install` runs `brew upgrade` on outdated dependencies, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set'
__fish_brew_complete_arg 'bundle' -l upgrade-formulae -d '`install` runs `brew upgrade` on any of these comma-separated formulae, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set'
__fish_brew_complete_arg 'bundle' -l verbose -d '`install` prints output from commands as they are run. `check` lists all missing dependencies'
__fish_brew_complete_arg 'bundle' -l vscode -d '`list` or `dump` VSCode (and forks/variants) extensions'
__fish_brew_complete_arg 'bundle' -l whalebrew -d '`list` or `dump` Whalebrew dependencies'

View File

@ -664,7 +664,8 @@ _brew_bundle() {
'--quiet[Make some output more quiet]' \
'--services[Temporarily start services while running the `exec` or `sh` command]' \
'--tap[`list` or `dump` Homebrew tap dependencies]' \
'(--install)--upgrade[`install` runs `brew upgrade` on outdated dependencies, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set. ]' \
'(--install)--upgrade[`install` runs `brew upgrade` on outdated dependencies, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set]' \
'--upgrade-formulae[`install` runs `brew upgrade` on any of these comma-separated formulae, even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set]' \
'--verbose[`install` prints output from commands as they are run. `check` lists all missing dependencies]' \
'(--no-vscode)--vscode[`list` or `dump` VSCode (and forks/variants) extensions]' \
'--whalebrew[`list` or `dump` Whalebrew dependencies]' \

View File

@ -247,6 +247,11 @@ flags which will help with finding keg-only dependencies like `openssl`,
: `install` runs `brew upgrade` on outdated dependencies, even if
`$HOMEBREW_BUNDLE_NO_UPGRADE` is set.
`--upgrade-formulae`
: `install` runs `brew upgrade` on any of these comma-separated formulae, even
if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set.
`--install`
: Run `install` before continuing to other operations e.g. `exec`.

View File

@ -1,5 +1,5 @@
.\" generated by kramdown
.TH "BREW" "1" "March 2025" "Homebrew"
.TH "BREW" "1" "April 2025" "Homebrew"
.SH NAME
brew \- The Missing Package Manager for macOS (or Linux)
.SH "SYNOPSIS"
@ -150,6 +150,9 @@ Read from or write to the \fBBrewfile\fP from \fB$HOMEBREW_BUNDLE_FILE_GLOBAL\fP
\fB\-\-upgrade\fP
\fBinstall\fP runs \fBbrew upgrade\fP on outdated dependencies, even if \fB$HOMEBREW_BUNDLE_NO_UPGRADE\fP is set\.
.TP
\fB\-\-upgrade\-formulae\fP
\fBinstall\fP runs \fBbrew upgrade\fP on any of these comma\-separated formulae, even if \fB$HOMEBREW_BUNDLE_NO_UPGRADE\fP is set\.
.TP
\fB\-\-install\fP
Run \fBinstall\fP before continuing to other operations e\.g\. \fBexec\fP\&\.
.TP