diff --git a/Library/Homebrew/ast_constants.rb b/Library/Homebrew/ast_constants.rb index 18a80c4a00..fa31aef6ed 100644 --- a/Library/Homebrew/ast_constants.rb +++ b/Library/Homebrew/ast_constants.rb @@ -17,7 +17,8 @@ FORMULA_COMPONENT_PRECEDENCE_LIST = T.let([ [{ name: :head, type: :method_call }], [{ name: :stable, type: :block_call }], [{ name: :livecheck, type: :block_call }], - [{ name: :bottle, type: :block_call }], + [{ name: :no_autobump!, type: :method_call }], + [{ name: :bottle, type: :block_call }], [{ name: :pour_bottle?, type: :block_call }], [{ name: :head, type: :block_call }], [{ name: :bottle, type: :method_call }], diff --git a/Library/Homebrew/autobump_constants.rb b/Library/Homebrew/autobump_constants.rb new file mode 100644 index 0000000000..a88ccaa71e --- /dev/null +++ b/Library/Homebrew/autobump_constants.rb @@ -0,0 +1,7 @@ +# typed: strict +# frozen_string_literal: true + +# TODO: add more reasons here +NO_AUTOBUMP_REASONS_LIST = T.let({ + incompatible_version_format: "incompatible version format", +}.freeze, T::Hash[Symbol, String]) diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index a48af3acb2..4ce56d4011 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -371,6 +371,9 @@ module Cask "url" => url, "url_specs" => url_specs, "version" => version, + "autobump" => autobump?, + "no_autobump_message" => no_autobump_message, + "skip_livecheck" => livecheck.skip?, "installed" => installed_version, "installed_time" => install_time&.to_i, "bundle_version" => bundle_long_version, diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index cf9f3ee108..5363e58237 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -1,6 +1,7 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "autobump_constants" require "locale" require "lazy_object" require "livecheck" @@ -102,6 +103,9 @@ module Cask :livecheck, :livecheck_defined?, :livecheckable?, # TODO: remove once `#livecheckable?` is removed + :no_autobump!, + :autobump?, + :no_autobump_message, :on_system_blocks_exist?, :on_system_block_min_os, :depends_on_set_in_block?, @@ -112,7 +116,7 @@ module Cask include OnSystem::MacOSAndLinux - attr_reader :cask, :token, :artifacts, :deprecation_date, :deprecation_reason, + attr_reader :cask, :token, :no_autobump_message, :artifacts, :deprecation_date, :deprecation_reason, :deprecation_replacement_cask, :deprecation_replacement_formula, :disable_date, :disable_reason, :disable_replacement_cask, :disable_replacement_formula, :on_system_block_min_os @@ -147,6 +151,8 @@ module Cask @livecheck = T.let(Livecheck.new(cask), Livecheck) @livecheck_defined = T.let(false, T::Boolean) @name = T.let([], T::Array[String]) + @autobump = T.let(true, T::Boolean) + @no_autobump_defined = T.let(false, T::Boolean) @on_system_blocks_exist = T.let(false, T::Boolean) @os = T.let(nil, T.nilable(String)) @on_system_block_min_os = T.let(nil, T.nilable(MacOSVersion)) @@ -540,6 +546,37 @@ module Cask @livecheck_defined == true end + # Excludes the cask from autobump list. + # + # TODO: limit this method to the official taps only (f.e. raise + # an error if `!tap.official?`) + # + # @api public + sig { params(because: T.any(String, Symbol)).void } + def no_autobump!(because:) + if because.is_a?(Symbol) && !NO_AUTOBUMP_REASONS_LIST.key?(because) + raise ArgumentError, "'because' argument should use valid symbol or a string!" + end + + if !@cask.allow_reassignment && @no_autobump_defined + raise CaskInvalidError.new(cask, "'no_autobump_defined' stanza may only appear once.") + end + + @no_autobump_defined = true + @no_autobump_message = because + @autobump = false + end + + # Is the cask in autobump list? + def autobump? + @autobump == true + end + + # Is no_autobump! method defined? + def no_autobump_defined? + @no_autobump_defined == true + end + # Declare that a cask is no longer functional or supported. # # NOTE: A warning will be shown when trying to install this cask. diff --git a/Library/Homebrew/dev-cmd/bump-cask-pr.rb b/Library/Homebrew/dev-cmd/bump-cask-pr.rb index 361ad410c7..bcf3cb866a 100644 --- a/Library/Homebrew/dev-cmd/bump-cask-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-cask-pr.rb @@ -81,8 +81,8 @@ module Homebrew Whoops, the #{cask.token} cask has its version update pull requests automatically opened by BrewTestBot every ~3 hours! We'd still love your contributions, though, so try another one - that's not in the autobump list: - #{Formatter.url("#{cask.tap.remote}/blob/master/.github/autobump.txt")} + that is excluded from autobump list (i.e. it has 'no_autobump!' + method or 'livecheck' block with 'skip'.) EOS odie "You have too many PRs open: close or merge some first!" if GitHub.too_many_open_prs?(cask.tap) diff --git a/Library/Homebrew/dev-cmd/bump-formula-pr.rb b/Library/Homebrew/dev-cmd/bump-formula-pr.rb index 267c53a945..e585f75d8c 100644 --- a/Library/Homebrew/dev-cmd/bump-formula-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-formula-pr.rb @@ -117,8 +117,8 @@ module Homebrew Whoops, the #{formula.name} formula has its version update pull requests automatically opened by BrewTestBot every ~3 hours! We'd still love your contributions, though, so try another one - that's not in the autobump list: - #{Formatter.url("#{tap.remote}/blob/master/.github/autobump.txt")} + that is excluded from autobump list (i.e. it has 'no_autobump!' + method or 'livecheck' block with 'skip'.) EOS odie "You have too many PRs open: close or merge some first!" if GitHub.too_many_open_prs?(tap) diff --git a/Library/Homebrew/dev-cmd/bump.rb b/Library/Homebrew/dev-cmd/bump.rb index 1edf55f811..e9422d9e56 100644 --- a/Library/Homebrew/dev-cmd/bump.rb +++ b/Library/Homebrew/dev-cmd/bump.rb @@ -58,7 +58,7 @@ module Homebrew conflicts "--cask", "--formula" conflicts "--tap=", "--installed" - conflicts "--tap=", "--no-auto" + conflicts "--tap=", "--no-autobump" conflicts "--eval-all", "--installed" conflicts "--installed", "--auto" conflicts "--no-pull-requests", "--open-pr" diff --git a/Library/Homebrew/dev-cmd/livecheck.rb b/Library/Homebrew/dev-cmd/livecheck.rb index 84675af729..dee478de47 100644 --- a/Library/Homebrew/dev-cmd/livecheck.rb +++ b/Library/Homebrew/dev-cmd/livecheck.rb @@ -100,10 +100,7 @@ module Homebrew tap = formula_or_cask.tap next false if tap.nil? - autobump_lists[tap] ||= begin - autobump_path = tap.path/".github/autobump.txt" - autobump_path.exist? ? autobump_path.readlines.map(&:strip) : [] - end + autobump_lists[tap] ||= tap.autobump name = formula_or_cask.respond_to?(:token) ? formula_or_cask.token : formula_or_cask.name next unless autobump_lists[tap].include?(name) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index ab09573ae2..b2f7e9bc13 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1,6 +1,7 @@ # typed: strict # frozen_string_literal: true +require "autobump_constants" require "cache_store" require "did_you_mean" require "formula_support" @@ -213,6 +214,13 @@ class Formula sig { returns(T::Boolean) } attr_accessor :follow_installed_alias + # Message that explains why the formula was excluded from autobump list. + # Returns `nil` if no message is specified. + # + # @see .no_autobump! + sig { returns(T.nilable(T.any(String, Symbol))) } + attr_reader :no_autobump_message + alias follow_installed_alias? follow_installed_alias # Whether or not to force the use of a bottle. @@ -242,6 +250,9 @@ class Formula @head = T.let(nil, T.nilable(SoftwareSpec)) @stable = T.let(nil, T.nilable(SoftwareSpec)) + @autobump = T.let(true, T::Boolean) + @no_autobump_message = T.let(nil, T.nilable(T.any(String, Symbol))) + @force_bottle = T.let(force_bottle, T::Boolean) @tap = T.let(tap, T.nilable(Tap)) @@ -474,6 +485,23 @@ class Formula # @see .livecheckable? delegate livecheckable?: :"self.class" + # Exclude the formula from autobump list. + # @!method no_autobump! + # @see .no_autobump! + delegate no_autobump!: :"self.class" + + # Is the formula in autobump list? + # @!method autobump? + # @see .autobump? + delegate autobump?: :"self.class" + + # Is no_autobump! method defined? + # @!method no_autobump_defined? + # @see .no_autobump_defined? + delegate no_autobump_defined?: :"self.class" + + delegate no_autobump_message: :"self.class" + # Is a service specification defined for the software? # @!method service? # @see .service? @@ -2484,6 +2512,9 @@ class Formula "urls" => urls_hash, "revision" => revision, "version_scheme" => version_scheme, + "autobump" => autobump?, + "no_autobump_message" => no_autobump_message, + "skip_livecheck" => livecheck.skip?, "bottle" => {}, "pour_bottle_only_if" => self.class.pour_bottle_only_if&.to_s, "keg_only" => keg_only?, @@ -4182,6 +4213,40 @@ class Formula @livecheck.instance_eval(&block) end + # Method that excludes the formula from the autobump list. + # + # TODO: limit this method to the official taps only (f.e. raise + # an error if `!tap.official?`) + # + # @api public + sig { params(because: T.any(String, Symbol)).void } + def no_autobump!(because:) + if because.is_a?(Symbol) && !NO_AUTOBUMP_REASONS_LIST.key?(because) + raise ArgumentError, "'because' argument should use valid symbol or a string!" + end + + @no_autobump_defined = T.let(true, T.nilable(T::Boolean)) + @no_autobump_message = T.let(because, T.nilable(T.any(String, Symbol))) + @autobump = T.let(false, T.nilable(T::Boolean)) + end + + # Is the formula in autobump list? + sig { returns(T::Boolean) } + def autobump? + @autobump != false # @autobump may be `nil` + end + + # Is no_autobump! method defined? + sig { returns(T::Boolean) } + def no_autobump_defined? = @no_autobump_defined == true + + # Message that explains why the formula was excluded from autobump list. + # Returns `nil` if no message is specified. + # + # @see .no_autobump! + sig { returns(T.nilable(T.any(String, Symbol))) } + attr_reader :no_autobump_message + # Service can be used to define services. # This method evaluates the DSL specified in the service block of the # {Formula} (if it exists) and sets the instance variables of a Service diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 5dd9e2ec4c..2f191fb787 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -291,6 +291,10 @@ module Formulary end end + if (because = json_formula["no_autobump_msg"]) + no_autobump!(because:) + end + bottles_stable = json_formula["bottle"]["stable"].presence if bottles_stable diff --git a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi index efe31aafaa..385d97082d 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi @@ -30,6 +30,9 @@ class Cask::Cask sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def auto_updates(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def autobump?(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def bash_completion(*args, &block); end @@ -141,6 +144,9 @@ class Cask::Cask sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def name(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def no_autobump_message(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.nilable(MacOSVersion)) } def on_system_block_min_os(*args, &block); end diff --git a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi index d8b7825bdb..315e5a687a 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/formula.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/formula.rbi @@ -6,6 +6,9 @@ class Formula + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def autobump?(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def allow_network_access!(*args, &block); end diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 808544c4dd..05bb1d2d37 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -985,11 +985,25 @@ class Tap # Array with autobump names sig { returns(T::Array[String]) } def autobump - @autobump ||= if (autobump_file = path/HOMEBREW_TAP_AUTOBUMP_FILE).file? - autobump_file.readlines(chomp: true) + autobump_packages = if core_cask_tap? + Homebrew::API::Cask.all_casks else - [] + Homebrew::API::Formula.all_formulae end + + @autobump ||= autobump_packages.select do |_, p| + p["autobump"] == true && !p["skip_livecheck"] && !(p["deprecated"] || p["disabled"]) + end.keys + + if @autobump.empty? + @autobump = if (autobump_file = path/HOMEBREW_TAP_AUTOBUMP_FILE).file? + autobump_file.readlines(chomp: true) + else + [] + end + end + + @autobump end # Whether this {Tap} allows running bump commands on the given {Formula} or {Cask}. diff --git a/Library/Homebrew/test/cask/dsl_spec.rb b/Library/Homebrew/test/cask/dsl_spec.rb index b799279c02..c9b75cf482 100644 --- a/Library/Homebrew/test/cask/dsl_spec.rb +++ b/Library/Homebrew/test/cask/dsl_spec.rb @@ -155,6 +155,25 @@ RSpec.describe Cask::DSL, :cask do end end + describe "no_autobump! stanze" do + it "returns true if no_autobump! is not set" do + expect(cask.autobump?).to be(true) + end + + context "when no_autobump! is set" do + let(:cask) do + Cask::Cask.new("checksum-cask") do + no_autobump! because: "some reason" + end + end + + it "returns false" do + expect(cask.autobump?).to be(false) + expect(cask.no_autobump_message).to eq("some reason") + end + end + end + describe "language stanza" do context "when language is set explicitly" do subject(:cask) do diff --git a/Library/Homebrew/test/support/fixtures/cask/everything.json b/Library/Homebrew/test/support/fixtures/cask/everything.json index d3d981258f..08e27f614a 100644 --- a/Library/Homebrew/test/support/fixtures/cask/everything.json +++ b/Library/Homebrew/test/support/fixtures/cask/everything.json @@ -16,6 +16,9 @@ "user_agent": ":fake" }, "version": "1.2.3", + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": false, "installed": null, "installed_time": null, "bundle_version": null, diff --git a/completions/bash/brew b/completions/bash/brew index 2894cc704c..ff172835c2 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -652,6 +652,7 @@ _brew_bundle() { __brewcomp " --all --cask + --check --cleanup --debug --describe diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index 398951e1d8..ff6e37aa56 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -507,6 +507,7 @@ __fish_brew_complete_sub_cmd 'bundle' 'env' __fish_brew_complete_sub_cmd 'bundle' 'edit' __fish_brew_complete_arg 'bundle' -l all -d '`list` all dependencies' __fish_brew_complete_arg 'bundle' -l cask -d '`list` or `dump` Homebrew cask dependencies' +__fish_brew_complete_arg 'bundle' -l check -d 'Check that all dependencies in the Brewfile are installed before running `exec`, `sh`, or `env`' __fish_brew_complete_arg 'bundle' -l cleanup -d '`install` performs cleanup operation, same as running `cleanup --force`. This is enabled by default if `$HOMEBREW_BUNDLE_INSTALL_CLEANUP` is set and `--global` is passed' __fish_brew_complete_arg 'bundle' -l debug -d 'Display any debugging information' __fish_brew_complete_arg 'bundle' -l describe -d '`dump` adds a description comment above each line, unless the dependency does not have a description. This is enabled by default if `$HOMEBREW_BUNDLE_DUMP_DESCRIBE` is set' diff --git a/completions/zsh/_brew b/completions/zsh/_brew index 48bd1ac9d9..72453df3d6 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -540,14 +540,14 @@ _brew_bump() { '--full-name[Print formulae/casks with fully-qualified names]' \ '--help[Show this message]' \ '(--tap --eval-all --auto)--installed[Check formulae and casks that are currently installed]' \ - '--no-autobump[Ignore formulae/casks in autobump list (official repositories only)]' \ + '(--tap)--no-autobump[Ignore formulae/casks in autobump list (official repositories only)]' \ '--no-fork[Don'\''t try to fork the repository]' \ '(--open-pr)--no-pull-requests[Do not retrieve pull requests from GitHub]' \ '(--no-pull-requests)--open-pr[Open a pull request for the new version if none have been opened yet]' \ '--quiet[Make some output more quiet]' \ '--repology[Use Repology to check for outdated packages]' \ '--start-with[Letter or word that the list of package results should alphabetically follow]' \ - '(--installed --no-auto)--tap[Check formulae and casks within the given tap, specified as user`/`repo]' \ + '(--installed --no-autobump)--tap[Check formulae and casks within the given tap, specified as user`/`repo]' \ '--verbose[Make some output more verbose]' \ - formula \ '(--cask)--formula[Check only formulae]' \ @@ -650,6 +650,7 @@ _brew_bundle() { _arguments \ '(--no-vscode)--all[`list` all dependencies]' \ '--cask[`list` or `dump` Homebrew cask dependencies]' \ + '--check[Check that all dependencies in the Brewfile are installed before running `exec`, `sh`, or `env`]' \ '--cleanup[`install` performs cleanup operation, same as running `cleanup --force`. This is enabled by default if `$HOMEBREW_BUNDLE_INSTALL_CLEANUP` is set and `--global` is passed]' \ '--debug[Display any debugging information]' \ '--describe[`dump` adds a description comment above each line, unless the dependency does not have a description. This is enabled by default if `$HOMEBREW_BUNDLE_DUMP_DESCRIBE` is set]' \ diff --git a/docs/Manpage.md b/docs/Manpage.md index e5b1271006..cb6a546ff2 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -199,7 +199,7 @@ By default, only Homebrew formula dependencies are listed. of the corresponding type. Passing `--formula` also removes matches against formula aliases and old formula names. -`brew bundle exec` *`command`* +`brew bundle exec` \[--check\] *`command`* : Run an external command in an isolated build environment based on the `Brewfile` dependencies. @@ -210,11 +210,11 @@ commands like `bundle install`, `npm install`, etc. It will also add compiler flags which will help with finding keg-only dependencies like `openssl`, `icu4c`, etc. -`brew bundle sh` +`brew bundle sh` \[--check\] : Run your shell in a `brew bundle exec` environment. -`brew bundle env` +`brew bundle env` \[--check\] : Print the environment variables that would be set in a `brew bundle exec` environment. @@ -319,6 +319,11 @@ flags which will help with finding keg-only dependencies like `openssl`, : `cleanup` casks using the `zap` command instead of `uninstall`. +`--check` + +: Check that all dependencies in the Brewfile are installed before running + `exec`, `sh`, or `env`. + ### `casks` List all locally installable casks including short names. diff --git a/manpages/brew.1 b/manpages/brew.1 index fdb437dc3d..6c602fd777 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -124,15 +124,15 @@ Add entries to your \fBBrewfile\fP\&\. Adds formulae by default\. Use \fB\-\-cas \fBbrew bundle remove\fP \fIname\fP [\.\.\.] Remove entries that match \fBname\fP from your \fBBrewfile\fP\&\. Use \fB\-\-formula\fP, \fB\-\-cask\fP, \fB\-\-tap\fP, \fB\-\-mas\fP, \fB\-\-whalebrew\fP or \fB\-\-vscode\fP to remove only entries of the corresponding type\. Passing \fB\-\-formula\fP also removes matches against formula aliases and old formula names\. .TP -\fBbrew bundle exec\fP \fIcommand\fP +\fBbrew bundle exec\fP [\-\-check] \fIcommand\fP Run an external command in an isolated build environment based on the \fBBrewfile\fP dependencies\. .P This sanitized build environment ignores unrequested dependencies, which makes sure that things you didn\[u2019]t specify in your \fBBrewfile\fP won\[u2019]t get picked up by commands like \fBbundle install\fP, \fBnpm install\fP, etc\. It will also add compiler flags which will help with finding keg\-only dependencies like \fBopenssl\fP, \fBicu4c\fP, etc\. .TP -\fBbrew bundle sh\fP +\fBbrew bundle sh\fP [\-\-check] Run your shell in a \fBbrew bundle exec\fP environment\. .TP -\fBbrew bundle env\fP +\fBbrew bundle env\fP [\-\-check] Print the environment variables that would be set in a \fBbrew bundle exec\fP environment\. .TP \fB\-\-file\fP @@ -197,6 +197,9 @@ Temporarily start services while running the \fBexec\fP or \fBsh\fP command\. Th .TP \fB\-\-zap\fP \fBcleanup\fP casks using the \fBzap\fP command instead of \fBuninstall\fP\&\. +.TP +\fB\-\-check\fP +Check that all dependencies in the Brewfile are installed before running \fBexec\fP, \fBsh\fP, or \fBenv\fP\&\. .SS "\fBcasks\fP" List all locally installable casks including short names\. .SS "\fBcleanup\fP \fR[\fIoptions\fP] \fR[\fIformula\fP|\fIcask\fP \.\.\.]"