From 89d0309b9cbfd3330e97ba01fe49b5185943d543 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Wed, 2 Apr 2025 17:15:32 +0100 Subject: [PATCH] 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. --- Library/Homebrew/bundle.rb | 36 +++++++++++++++---- Library/Homebrew/bundle/brew_installer.rb | 8 +++-- Library/Homebrew/bundle/checker.rb | 2 +- Library/Homebrew/bundle/commands/cleanup.rb | 2 +- .../bundle/vscode_extension_installer.rb | 2 +- Library/Homebrew/cmd/bundle.rb | 6 +++- .../sorbet/rbi/dsl/homebrew/cmd/bundle.rbi | 6 ++++ .../test/bundle/commands/cleanup_spec.rb | 2 +- completions/bash/brew | 1 + completions/fish/brew.fish | 3 +- completions/zsh/_brew | 3 +- docs/Manpage.md | 5 +++ manpages/brew.1 | 5 ++- 13 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Library/Homebrew/bundle.rb b/Library/Homebrew/bundle.rb index 04a581c832..6184a80397 100644 --- a/Library/Homebrew/bundle.rb +++ b/Library/Homebrew/bundle.rb @@ -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 diff --git a/Library/Homebrew/bundle/brew_installer.rb b/Library/Homebrew/bundle/brew_installer.rb index 0d5ae35f7c..e1d2789f3b 100644 --- a/Library/Homebrew/bundle/brew_installer.rb +++ b/Library/Homebrew/bundle/brew_installer.rb @@ -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) diff --git a/Library/Homebrew/bundle/checker.rb b/Library/Homebrew/bundle/checker.rb index 9c6d9e00d9..411e0d8c0b 100644 --- a/Library/Homebrew/bundle/checker.rb +++ b/Library/Homebrew/bundle/checker.rb @@ -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." diff --git a/Library/Homebrew/bundle/commands/cleanup.rb b/Library/Homebrew/bundle/commands/cleanup.rb index 319388d904..a42f07d980 100644 --- a/Library/Homebrew/bundle/commands/cleanup.rb +++ b/Library/Homebrew/bundle/commands/cleanup.rb @@ -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 diff --git a/Library/Homebrew/bundle/vscode_extension_installer.rb b/Library/Homebrew/bundle/vscode_extension_installer.rb index 14e4ef02da..22e10be431 100644 --- a/Library/Homebrew/bundle/vscode_extension_installer.rb +++ b/Library/Homebrew/bundle/vscode_extension_installer.rb @@ -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 diff --git a/Library/Homebrew/cmd/bundle.rb b/Library/Homebrew/cmd/bundle.rb index 71df929e75..d4b71ee956 100755 --- a/Library/Homebrew/cmd/bundle.rb +++ b/Library/Homebrew/cmd/bundle.rb @@ -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? diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi index 43c5e5fe61..b9fe56b358 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/bundle.rbi @@ -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 diff --git a/Library/Homebrew/test/bundle/commands/cleanup_spec.rb b/Library/Homebrew/test/bundle/commands/cleanup_spec.rb index 2291120f26..d234f07b1e 100644 --- a/Library/Homebrew/test/bundle/commands/cleanup_spec.rb +++ b/Library/Homebrew/test/bundle/commands/cleanup_spec.rb @@ -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 diff --git a/completions/bash/brew b/completions/bash/brew index c57dd09cf4..2a71586da3 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -667,6 +667,7 @@ _brew_bundle() { --services --tap --upgrade + --upgrade-formulae --verbose --vscode --whalebrew diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index 5e53345443..df9158b520 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -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' diff --git a/completions/zsh/_brew b/completions/zsh/_brew index b250776f5d..3ed2d825b3 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -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]' \ diff --git a/docs/Manpage.md b/docs/Manpage.md index ad1f0f591b..dd959193cb 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -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`. diff --git a/manpages/brew.1 b/manpages/brew.1 index fdf4c98a05..dde94aff4f 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -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