
This came up recently where an outdated formula definition caused the program to crash with an ambiguous message when a user wanted to upgrade a cask instead. Catching these errors allows them to get handled later on improving error messages and defaults. Now if the only formula with the given name is invalid it will default to using the cask unless --formula is specified. - https://github.com/Homebrew/brew/issues/16123
438 lines
15 KiB
Ruby
438 lines
15 KiB
Ruby
# typed: true
|
|
# frozen_string_literal: true
|
|
|
|
require "delegate"
|
|
require "api"
|
|
require "cli/args"
|
|
|
|
module Homebrew
|
|
module CLI
|
|
# Helper class for loading formulae/casks from named arguments.
|
|
#
|
|
# @api private
|
|
class NamedArgs < Array
|
|
sig {
|
|
params(
|
|
args: String,
|
|
parent: Args,
|
|
override_spec: Symbol,
|
|
force_bottle: T::Boolean,
|
|
flags: T::Array[String],
|
|
cask_options: T::Boolean,
|
|
without_api: T::Boolean,
|
|
).void
|
|
}
|
|
def initialize(
|
|
*args,
|
|
parent: Args.new,
|
|
override_spec: T.unsafe(nil),
|
|
force_bottle: T.unsafe(nil),
|
|
flags: T.unsafe(nil),
|
|
cask_options: false,
|
|
without_api: false
|
|
)
|
|
require "cask/cask"
|
|
require "cask/cask_loader"
|
|
require "formulary"
|
|
require "keg"
|
|
require "missing_formula"
|
|
|
|
@args = args
|
|
@override_spec = override_spec
|
|
@force_bottle = force_bottle
|
|
@flags = flags
|
|
@cask_options = cask_options
|
|
@without_api = without_api
|
|
@parent = parent
|
|
|
|
super(@args)
|
|
end
|
|
|
|
attr_reader :parent
|
|
|
|
def to_casks
|
|
@to_casks ||= to_formulae_and_casks(only: :cask).freeze
|
|
end
|
|
|
|
def to_formulae
|
|
@to_formulae ||= to_formulae_and_casks(only: :formula).freeze
|
|
end
|
|
|
|
# Convert named arguments to {Formula} or {Cask} objects.
|
|
# If both a formula and cask with the same name exist, returns
|
|
# the formula and prints a warning unless `only` is specified.
|
|
sig {
|
|
params(
|
|
only: T.nilable(Symbol),
|
|
ignore_unavailable: T.nilable(T::Boolean),
|
|
method: T.nilable(Symbol),
|
|
uniq: T::Boolean,
|
|
warn: T::Boolean,
|
|
).returns(T::Array[T.any(Formula, Keg, Cask::Cask)])
|
|
}
|
|
def to_formulae_and_casks(
|
|
only: parent&.only_formula_or_cask,
|
|
ignore_unavailable: nil,
|
|
method: T.unsafe(nil),
|
|
uniq: true,
|
|
warn: T.unsafe(nil)
|
|
)
|
|
@to_formulae_and_casks ||= {}
|
|
@to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name|
|
|
options = { warn: warn }.compact
|
|
load_formula_or_cask(name, only: only, method: method, **options)
|
|
rescue FormulaUnreadableError, FormulaClassUnavailableError,
|
|
TapFormulaUnreadableError, TapFormulaClassUnavailableError,
|
|
Cask::CaskUnreadableError
|
|
# Need to rescue before `*UnavailableError` (superclass of this)
|
|
# The formula/cask was found, but there's a problem with its implementation
|
|
raise
|
|
rescue NoSuchKegError, FormulaUnavailableError, Cask::CaskUnavailableError, FormulaOrCaskUnavailableError
|
|
ignore_unavailable ? [] : raise
|
|
end.freeze
|
|
|
|
if uniq
|
|
@to_formulae_and_casks[only].uniq.freeze
|
|
else
|
|
@to_formulae_and_casks[only]
|
|
end
|
|
end
|
|
|
|
def to_formulae_to_casks(only: parent&.only_formula_or_cask, method: nil)
|
|
@to_formulae_to_casks ||= {}
|
|
@to_formulae_to_casks[[method, only]] = to_formulae_and_casks(only: only, method: method)
|
|
.partition { |o| o.is_a?(Formula) || o.is_a?(Keg) }
|
|
.map(&:freeze).freeze
|
|
end
|
|
|
|
def to_formulae_and_casks_and_unavailable(only: parent&.only_formula_or_cask, method: nil)
|
|
@to_formulae_casks_unknowns ||= {}
|
|
@to_formulae_casks_unknowns[method] = downcased_unique_named.map do |name|
|
|
load_formula_or_cask(name, only: only, method: method)
|
|
rescue FormulaOrCaskUnavailableError => e
|
|
e
|
|
end.uniq.freeze
|
|
end
|
|
|
|
def load_formula_or_cask(name, only: nil, method: nil, warn: 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,
|
|
FormulaSpecificationError => 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
|
|
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
|
|
end
|
|
private :load_formula_or_cask
|
|
|
|
def resolve_formula(name)
|
|
Formulary.resolve(name, **{ spec: @override_spec, force_bottle: @force_bottle, flags: @flags }.compact)
|
|
end
|
|
private :resolve_formula
|
|
|
|
sig { params(uniq: T::Boolean).returns(T::Array[Formula]) }
|
|
def to_resolved_formulae(uniq: true)
|
|
@to_resolved_formulae ||= to_formulae_and_casks(only: :formula, method: :resolve, uniq: uniq)
|
|
.freeze
|
|
end
|
|
|
|
def to_resolved_formulae_to_casks(only: parent&.only_formula_or_cask)
|
|
to_formulae_to_casks(only: only, method: :resolve)
|
|
end
|
|
|
|
LOCAL_PATH_REGEX = %r{^/|[.]|/$}.freeze
|
|
TAP_NAME_REGEX = %r{^[^./]+/[^./]+$}.freeze
|
|
private_constant :LOCAL_PATH_REGEX, :TAP_NAME_REGEX
|
|
|
|
# Keep existing paths and try to convert others to tap, formula or cask paths.
|
|
# If a cask and formula with the same name exist, includes both their paths
|
|
# unless `only` is specified.
|
|
sig { params(only: T.nilable(Symbol), recurse_tap: T::Boolean).returns(T::Array[Pathname]) }
|
|
def to_paths(only: parent&.only_formula_or_cask, recurse_tap: false)
|
|
@to_paths ||= {}
|
|
@to_paths[only] ||= Homebrew.with_no_api_env_if_needed(@without_api) do
|
|
downcased_unique_named.flat_map do |name|
|
|
path = Pathname(name).expand_path
|
|
if only.nil? && name.match?(LOCAL_PATH_REGEX) && path.exist?
|
|
path
|
|
elsif name.match?(TAP_NAME_REGEX)
|
|
tap = Tap.fetch(name)
|
|
|
|
if recurse_tap
|
|
next tap.formula_files if only == :formula
|
|
next tap.cask_files if only == :cask
|
|
end
|
|
|
|
tap.path
|
|
else
|
|
next Formulary.path(name) if only == :formula
|
|
next Cask::CaskLoader.path(name) if only == :cask
|
|
|
|
formula_path = Formulary.path(name)
|
|
cask_path = Cask::CaskLoader.path(name)
|
|
|
|
paths = []
|
|
|
|
if formula_path.exist? ||
|
|
(!CoreTap.instance.installed? && Homebrew::API::Formula.all_formulae.key?(path.basename))
|
|
paths << formula_path
|
|
end
|
|
if cask_path.exist? ||
|
|
(!CoreCaskTap.instance.installed? && Homebrew::API::Cask.all_casks.key?(path.basename))
|
|
paths << cask_path
|
|
end
|
|
|
|
paths.empty? ? path : paths
|
|
end
|
|
end.uniq.freeze
|
|
end
|
|
end
|
|
|
|
sig { returns(T::Array[Keg]) }
|
|
def to_default_kegs
|
|
@to_default_kegs ||= begin
|
|
to_formulae_and_casks(only: :formula, method: :default_kegs).freeze
|
|
rescue NoSuchKegError => e
|
|
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
|
|
$stderr.puts reason
|
|
end
|
|
raise e
|
|
end
|
|
end
|
|
|
|
sig { returns(T::Array[Keg]) }
|
|
def to_latest_kegs
|
|
@to_latest_kegs ||= begin
|
|
to_formulae_and_casks(only: :formula, method: :latest_kegs).freeze
|
|
rescue NoSuchKegError => e
|
|
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
|
|
$stderr.puts reason
|
|
end
|
|
raise e
|
|
end
|
|
end
|
|
|
|
sig { returns(T::Array[Keg]) }
|
|
def to_kegs
|
|
@to_kegs ||= begin
|
|
to_formulae_and_casks(only: :formula, method: :kegs).freeze
|
|
rescue NoSuchKegError => e
|
|
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
|
|
$stderr.puts reason
|
|
end
|
|
raise e
|
|
end
|
|
end
|
|
|
|
sig {
|
|
params(only: T.nilable(Symbol), ignore_unavailable: T.nilable(T::Boolean), all_kegs: T.nilable(T::Boolean))
|
|
.returns([T::Array[Keg], T::Array[Cask::Cask]])
|
|
}
|
|
def to_kegs_to_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, all_kegs: nil)
|
|
method = all_kegs ? :kegs : :default_kegs
|
|
@to_kegs_to_casks ||= {}
|
|
@to_kegs_to_casks[method] ||=
|
|
to_formulae_and_casks(only: only, ignore_unavailable: ignore_unavailable, method: method)
|
|
.partition { |o| o.is_a?(Keg) }
|
|
.map(&:freeze).freeze
|
|
end
|
|
|
|
sig { returns(T::Array[Tap]) }
|
|
def to_taps
|
|
@to_taps ||= downcased_unique_named.map { |name| Tap.fetch name }.uniq.freeze
|
|
end
|
|
|
|
sig { returns(T::Array[Tap]) }
|
|
def to_installed_taps
|
|
@to_installed_taps ||= to_taps.each do |tap|
|
|
raise TapUnavailableError, tap.name unless tap.installed?
|
|
end.uniq.freeze
|
|
end
|
|
|
|
sig { returns(T::Array[String]) }
|
|
def homebrew_tap_cask_names
|
|
downcased_unique_named.grep(HOMEBREW_CASK_TAP_CASK_REGEX)
|
|
end
|
|
|
|
private
|
|
|
|
sig { returns(T::Array[String]) }
|
|
def downcased_unique_named
|
|
# Only lowercase names, not paths, bottle filenames or URLs
|
|
map do |arg|
|
|
if arg.include?("/") || arg.end_with?(".tar.gz") || File.exist?(arg)
|
|
arg
|
|
else
|
|
arg.downcase
|
|
end
|
|
end.uniq
|
|
end
|
|
|
|
def resolve_kegs(name)
|
|
raise UsageError if name.blank?
|
|
|
|
require "keg"
|
|
|
|
rack = Formulary.to_rack(name.downcase)
|
|
|
|
kegs = rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : []
|
|
raise NoSuchKegError, name if kegs.none?
|
|
|
|
[rack, kegs]
|
|
end
|
|
|
|
def resolve_latest_keg(name)
|
|
_, kegs = resolve_kegs(name)
|
|
|
|
# Return keg if it is the only installed keg
|
|
return kegs if kegs.length == 1
|
|
|
|
stable_kegs = kegs.reject { |k| k.version.head? }
|
|
|
|
if stable_kegs.blank?
|
|
return kegs.max_by do |keg|
|
|
[Tab.for_keg(keg).source_modified_time, keg.version.revision]
|
|
end
|
|
end
|
|
|
|
stable_kegs.max_by(&:version)
|
|
end
|
|
|
|
def resolve_default_keg(name)
|
|
rack, kegs = resolve_kegs(name)
|
|
|
|
linked_keg_ref = HOMEBREW_LINKED_KEGS/rack.basename
|
|
opt_prefix = HOMEBREW_PREFIX/"opt/#{rack.basename}"
|
|
|
|
begin
|
|
return Keg.new(opt_prefix.resolved_path) if opt_prefix.symlink? && opt_prefix.directory?
|
|
return Keg.new(linked_keg_ref.resolved_path) if linked_keg_ref.symlink? && linked_keg_ref.directory?
|
|
return kegs.first if kegs.length == 1
|
|
|
|
f = if name.include?("/") || File.exist?(name)
|
|
Formulary.factory(name)
|
|
else
|
|
Formulary.from_rack(rack)
|
|
end
|
|
|
|
unless (prefix = f.latest_installed_prefix).directory?
|
|
raise MultipleVersionsInstalledError, <<~EOS
|
|
#{rack.basename} has multiple installed versions
|
|
Run `brew uninstall --force #{rack.basename}` to remove all versions.
|
|
EOS
|
|
end
|
|
|
|
Keg.new(prefix)
|
|
rescue FormulaUnavailableError
|
|
raise MultipleVersionsInstalledError, <<~EOS
|
|
Multiple kegs installed to #{rack}
|
|
However we don't know which one you refer to.
|
|
Please delete (with `rm -rf`!) all but one and then try again.
|
|
EOS
|
|
end
|
|
end
|
|
|
|
def warn_if_cask_conflicts(ref, loaded_type)
|
|
message = "Treating #{ref} as a #{loaded_type}."
|
|
begin
|
|
cask = Cask::CaskLoader.load ref
|
|
message += " For the cask, use #{cask.tap.name}/#{cask.token}" if cask.tap.present?
|
|
rescue Cask::CaskUnreadableError => e
|
|
# Need to rescue before `CaskUnavailableError` (superclass of this)
|
|
# The cask was found, but there's a problem with its implementation
|
|
onoe <<~EOS
|
|
Failed to load cask: #{ref}
|
|
#{e}
|
|
EOS
|
|
rescue Cask::CaskUnavailableError
|
|
# No ref conflict with a cask, do nothing
|
|
return
|
|
end
|
|
opoo message.freeze
|
|
end
|
|
end
|
|
end
|
|
end
|