diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 2efe1bd7fe..3b13359f72 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -176,4 +176,20 @@ module Homebrew Tap.fetch(org, repo) end end + + # @api private + sig { params(block: T.proc.returns(T.untyped)).returns(T.untyped) } + def self.with_no_api_env(&block) + return yield if Homebrew::EnvConfig.no_install_from_api? + + with_env(HOMEBREW_NO_INSTALL_FROM_API: "1", HOMEBREW_AUTOMATICALLY_SET_NO_INSTALL_FROM_API: "1", &block) + end + + # @api private + sig { params(condition: T::Boolean, block: T.proc.returns(T.untyped)).returns(T.untyped) } + def self.with_no_api_env_if_needed(condition, &block) + return yield unless condition + + with_no_api_env(&block) + end end diff --git a/Library/Homebrew/cli/args.rb b/Library/Homebrew/cli/args.rb index 02e66905cf..b2226f1329 100644 --- a/Library/Homebrew/cli/args.rb +++ b/Library/Homebrew/cli/args.rb @@ -32,7 +32,7 @@ module Homebrew self[:remaining] = remaining_args.freeze end - def freeze_named_args!(named_args, cask_options:) + def freeze_named_args!(named_args, cask_options:, without_api:) options = {} options[:force_bottle] = true if self[:force_bottle?] options[:override_spec] = :head if self[:HEAD?] @@ -41,6 +41,7 @@ module Homebrew *named_args.freeze, parent: self, cask_options: cask_options, + without_api: without_api, **options, ) end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 3e68e848ce..0e92f1aefd 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -19,6 +19,7 @@ module Homebrew force_bottle: T::Boolean, flags: T::Array[String], cask_options: T::Boolean, + without_api: T::Boolean, ).void } def initialize( @@ -27,7 +28,8 @@ module Homebrew override_spec: T.unsafe(nil), force_bottle: T.unsafe(nil), flags: T.unsafe(nil), - cask_options: false + cask_options: false, + without_api: false ) require "cask/cask" require "cask/cask_loader" @@ -40,6 +42,7 @@ module Homebrew @force_bottle = force_bottle @flags = flags @cask_options = cask_options + @without_api = without_api @parent = parent super(@args) @@ -112,92 +115,94 @@ module Homebrew end def load_formula_or_cask(name, only: nil, method: nil, warn: nil) - unreadable_error = nil + Homebrew.with_no_api_env_if_needed(@without_api) do + unreadable_error = nil - if only != :cask - begin - formula = case method - when nil, :factory - options = { warn: warn, force_bottle: @force_bottle, flags: @flags }.compact - Formulary.factory(name, *@override_spec, **options) - when :resolve - resolve_formula(name) - when :latest_kegs - resolve_latest_keg(name) - when :default_kegs - resolve_default_keg(name) - when :kegs - _, kegs = resolve_kegs(name) - kegs - else - raise - end - - warn_if_cask_conflicts(name, "formula") if only != :formula - return formula - rescue FormulaUnreadableError, FormulaClassUnavailableError, - TapFormulaUnreadableError, TapFormulaClassUnavailableError => e - # Need to rescue before `FormulaUnavailableError` (superclass of this) - # The formula was found, but there's a problem with its implementation - unreadable_error ||= e - rescue NoSuchKegError, FormulaUnavailableError => e - raise e if only == :formula - end - end - - if only != :formula - want_keg_like_cask = [:latest_kegs, :default_kegs, :kegs].include?(method) - - begin - config = Cask::Config.from_args(@parent) if @cask_options - options = { warn: warn }.compact - cask = Cask::CaskLoader.load(name, config: config, **options) - - if unreadable_error.present? - onoe <<~EOS - Failed to load formula: #{name} - #{unreadable_error} - EOS - opoo "Treating #{name} as a cask." - end - - # If we're trying to get a keg-like Cask, do our best to use the same cask - # file that was used for installation, if possible. - if want_keg_like_cask && (installed_caskfile = cask.installed_caskfile) && installed_caskfile.exist? - cask = Cask::CaskLoader.load(installed_caskfile) - end - - return cask - rescue Cask::CaskUnreadableError, Cask::CaskInvalidError => e - # If we're trying to get a keg-like Cask, do our best to handle it - # not being readable and return something that can be used. - if want_keg_like_cask - cask_version = Cask::Cask.new(name, config: config).installed_version - cask = Cask::Cask.new(name, config: config) do - version cask_version if cask_version + if only != :cask + begin + formula = case method + when nil, :factory + options = { warn: warn, force_bottle: @force_bottle, flags: @flags }.compact + Formulary.factory(name, *@override_spec, **options) + when :resolve + resolve_formula(name) + when :latest_kegs + resolve_latest_keg(name) + when :default_kegs + resolve_default_keg(name) + when :kegs + _, kegs = resolve_kegs(name) + kegs + else + raise end - return cask + + warn_if_cask_conflicts(name, "formula") if only != :formula + return formula + rescue FormulaUnreadableError, FormulaClassUnavailableError, + TapFormulaUnreadableError, TapFormulaClassUnavailableError => e + # Need to rescue before `FormulaUnavailableError` (superclass of this) + # The formula was found, but there's a problem with its implementation + unreadable_error ||= e + rescue NoSuchKegError, FormulaUnavailableError => e + raise e if only == :formula end - - # Need to rescue before `CaskUnavailableError` (superclass of this) - # The cask was found, but there's a problem with its implementation - unreadable_error ||= e - rescue Cask::CaskUnavailableError => e - raise e if only == :cask end + + if only != :formula + want_keg_like_cask = [:latest_kegs, :default_kegs, :kegs].include?(method) + + begin + config = Cask::Config.from_args(@parent) if @cask_options + options = { warn: warn }.compact + cask = Cask::CaskLoader.load(name, config: config, **options) + + if unreadable_error.present? + onoe <<~EOS + Failed to load formula: #{name} + #{unreadable_error} + EOS + opoo "Treating #{name} as a cask." + end + + # If we're trying to get a keg-like Cask, do our best to use the same cask + # file that was used for installation, if possible. + if want_keg_like_cask && (installed_caskfile = cask.installed_caskfile) && installed_caskfile.exist? + cask = Cask::CaskLoader.load(installed_caskfile) + end + + return cask + rescue Cask::CaskUnreadableError, Cask::CaskInvalidError => e + # If we're trying to get a keg-like Cask, do our best to handle it + # not being readable and return something that can be used. + if want_keg_like_cask + cask_version = Cask::Cask.new(name, config: config).installed_version + cask = Cask::Cask.new(name, config: config) do + version cask_version if cask_version + end + return cask + end + + # Need to rescue before `CaskUnavailableError` (superclass of this) + # The cask was found, but there's a problem with its implementation + unreadable_error ||= e + rescue Cask::CaskUnavailableError => e + raise e if only == :cask + end + end + + raise unreadable_error if unreadable_error.present? + + user, repo, short_name = name.downcase.split("/", 3) + if repo.present? && short_name.present? + tap = Tap.fetch(user, repo) + raise TapFormulaOrCaskUnavailableError.new(tap, short_name) + end + + raise NoSuchKegError, name if resolve_formula(name) + + raise FormulaOrCaskUnavailableError, name end - - raise unreadable_error if unreadable_error.present? - - user, repo, short_name = name.downcase.split("/", 3) - if repo.present? && short_name.present? - tap = Tap.fetch(user, repo) - raise TapFormulaOrCaskUnavailableError.new(tap, short_name) - end - - raise NoSuchKegError, name if resolve_formula(name) - - raise FormulaOrCaskUnavailableError, name end private :load_formula_or_cask diff --git a/Library/Homebrew/cli/parser.rb b/Library/Homebrew/cli/parser.rb index 76e7dcc8b0..682dca701d 100644 --- a/Library/Homebrew/cli/parser.rb +++ b/Library/Homebrew/cli/parser.rb @@ -138,6 +138,7 @@ module Homebrew @named_args_type = nil @max_named_args = nil @min_named_args = nil + @named_args_without_api = false @description = nil @usage_banner = nil @hide_from_man_page = false @@ -346,7 +347,7 @@ module Homebrew check_named_args(named_args) end - @args.freeze_named_args!(named_args, cask_options: @cask_options) + @args.freeze_named_args!(named_args, cask_options: @cask_options, without_api: @named_args_without_api) @args.freeze_remaining_args!(non_options.empty? ? remaining : [*remaining, "--", non_options]) @args.freeze_processed_options!(@processed_options) @args.freeze @@ -392,13 +393,14 @@ module Homebrew sig { params( - type: T.any(NilClass, Symbol, T::Array[String], T::Array[Symbol]), - number: T.nilable(Integer), - min: T.nilable(Integer), - max: T.nilable(Integer), + type: T.any(NilClass, Symbol, T::Array[String], T::Array[Symbol]), + number: T.nilable(Integer), + min: T.nilable(Integer), + max: T.nilable(Integer), + without_api: T::Boolean, ).void } - def named_args(type = nil, number: nil, min: nil, max: nil) + def named_args(type = nil, number: nil, min: nil, max: nil, without_api: false) if number.present? && (min.present? || max.present?) raise ArgumentError, "Do not specify both `number` and `min` or `max`" end @@ -417,6 +419,8 @@ module Homebrew @min_named_args = min @max_named_args = max end + + @named_args_without_api = without_api end sig { void } diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index 73a529d874..664c18d545 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -123,7 +123,7 @@ module Homebrew ENV.activate_extensions! ENV.setup_build_environment - audit_formulae, audit_casks = without_api do # audit requires full Ruby source + audit_formulae, audit_casks = with_no_api_env do # audit requires full Ruby source if args.tap Tap.fetch(args.tap).then do |tap| [ @@ -217,7 +217,7 @@ module Homebrew # Audit requires full Ruby source so disable API. # We shouldn't do this for taps however so that we don't unnecessarily require a full Homebrew/core clone. fa = if f.core_formula? - without_api(&audit_proc) + with_no_api_env(&audit_proc) else audit_proc.call end @@ -347,10 +347,4 @@ module Homebrew "* #{location}#{message.chomp.gsub("\n", "\n ")}#{status}" end end - - def self.without_api(&block) - return yield if Homebrew::EnvConfig.no_install_from_api? - - with_env(HOMEBREW_NO_INSTALL_FROM_API: "1", HOMEBREW_AUTOMATICALLY_SET_NO_INSTALL_FROM_API: "1", &block) - end end diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 6ed3ef6092..3c88e79015 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -87,13 +87,19 @@ class FormulaOrCaskUnavailableError < RuntimeError super() @name = name + + # Store the state of these envs at the time the exception is thrown. + # This is so we do the fuzzy search for "did you mean" etc under that same mode, + # in case the list of formulae are different. + @without_api = Homebrew::EnvConfig.no_install_from_api? + @auto_without_api = Homebrew::EnvConfig.automatically_set_no_install_from_api? end sig { returns(String) } def did_you_mean require "formula" - similar_formula_names = Formula.fuzzy_search(name) + similar_formula_names = Homebrew.with_no_api_env_if_needed(@without_api) { Formula.fuzzy_search(name) } return "" if similar_formula_names.blank? "Did you mean #{similar_formula_names.to_sentence two_words_connector: " or ", last_word_connector: " or "}?" @@ -101,7 +107,11 @@ class FormulaOrCaskUnavailableError < RuntimeError sig { returns(String) } def to_s - "No available formula or cask with the name \"#{name}\". #{did_you_mean}".strip + s = "No available formula or cask with the name \"#{name}\". #{did_you_mean}".strip + if @auto_without_api && !CoreTap.instance.installed? + s += "\nA full git tap clone is required to use this command on core packages." + end + s end end