forked from Homebrew/brew
/
named_args.rb
352 lines (298 loc) 路 11.8 KB
/
named_args.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# typed: false
# frozen_string_literal: true
require "delegate"
require "cli/args"
module Homebrew
module CLI
# Helper class for loading formulae/casks from named arguments.
#
# @api private
class NamedArgs < Array
extend T::Sig
def initialize(*args, parent: Args.new, override_spec: nil, force_bottle: false, flags: [], cask_options: 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
@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,
).returns(T::Array[T.any(Formula, Keg, Cask::Cask)])
}
def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true)
@to_formulae_and_casks ||= {}
@to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name|
load_formula_or_cask(name, only: only, method: method)
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) }
.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)
unreadable_error = nil
if only != :cask
begin
formula = case method
when nil, :factory
Formulary.factory(name, *spec, force_bottle: @force_bottle, flags: @flags)
when :resolve
resolve_formula(name)
# odeprecated "`method: :keg`"
when :keg, :default_kegs
resolve_default_keg(name)
when :kegs
_, kegs = resolve_kegs(name)
kegs
else
raise
end
warn_if_cask_conflicts(name, "formula") unless 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
begin
config = Cask::Config.from_args(@parent) if @cask_options
cask = Cask::CaskLoader.load(name, config: config)
if unreadable_error.present?
onoe <<~EOS
Failed to load formula: #{name}
#{unreadable_error}
EOS
opoo "Treating #{name} as a cask."
end
return cask
rescue Cask::CaskUnreadableError => e
# 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 FormulaOrCaskUnavailableError, name
end
private :load_formula_or_cask
def resolve_formula(name)
Formulary.resolve(name, spec: spec, force_bottle: @force_bottle, flags: @flags)
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
def to_formulae_paths
to_paths(only: :formula)
end
# 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] ||= downcased_unique_named.flat_map do |name|
if File.exist?(name)
Pathname(name)
elsif name.count("/") == 1 && !name.start_with?("./", "/")
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 = []
paths << formula_path if formula_path.exist?
paths << cask_path if cask_path.exist?
paths.empty? ? Pathname(name) : paths
end
end.uniq.freeze
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_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 spec
@override_spec
end
private :spec
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, rack.basename if kegs.none?
[rack, kegs]
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