1# typed: strict
2# frozen_string_literal: true
3
4require "abstract_command"
5require "formula"
6require "cask/caskroom"
7require "dependencies_helpers"
8
9module Homebrew
10 module Cmd
11 class Deps < AbstractCommand
12 include DependenciesHelpers
13
14 class DepsCombineMode < T::Enum
15 enums do
16 # enum values are not mutable, and calling .freeze on them breaks Sorbet
17 # rubocop:disable Style/MutableConstant
18 Intersection = new
19 Union = new
20 # rubocop:enable Style/MutableConstant
21 end
22 end
23
24 cmd_args do
25 description <<~EOS
26 Show dependencies for <formula>. When given multiple formula arguments,
27 show the intersection of dependencies for each formula. By default, `deps`
28 shows all required and recommended dependencies.
…lines 29–95 unchanged (67 lines)
96 conflicts "--formula", "--cask"
97 formula_options
98
99 named_args [:formula, :cask]
100 end
101
102 sig { override.params(argv: T::Array[String]).void }
103 def initialize(argv = ARGV.freeze)
104 super
105 @use_runtime_dependencies = T.let(true, T::Boolean)
106 end
107
108 sig { override.void }
109 def run
110 raise UsageError, "`brew deps --os=all` is not supported." if args.os == "all"
111 raise UsageError, "`brew deps --arch=all` is not supported." if args.arch == "all"
112
113 os, arch = args.os_arch_combinations.fetch(0)
114 eval_all = args.eval_all?
115
116 Formulary.enable_factory_cache!
117
118 SimulateSystem.with(os:, arch:) do
119 installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?)
120 unless installed
121 not_using_runtime_dependencies_reason = if args.installed?
122 "not all the named formulae were installed"
123 else
124 "`--installed` was not passed"
125 end
…lines 126–206 unchanged (81 lines)
207 return
208 end
209
210 dependents = dependents(args.named.to_formulae_and_casks)
211 check_head_spec(dependents) if args.HEAD?
212
213 deps_combine_mode = args.union? ? DepsCombineMode::Union : DepsCombineMode::Intersection
214 all_deps = deps_for_dependents(dependents, deps_combine_mode:, recursive:)
215 condense_requirements(all_deps)
216 all_deps.map! { dep_display_name(it) }
217 all_deps.uniq!
218 all_deps.sort! unless args.topological?
219 puts all_deps
220 end
221 end
222
223 private
224
225 sig {
226 params(formulae_or_casks: T::Array[T.any(Formula, Keg, Cask::Cask)])
227 .returns(T::Array[T.any(Formula, CaskDependent)])
228 }
229 def sorted_dependents(formulae_or_casks)
230 dependents(formulae_or_casks).sort_by(&:name)
231 end
232
233 sig { params(deps: T::Array[T.any(Dependency, Requirement)]).void }
234 def condense_requirements(deps)
235 deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements?
236 deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed?
237 end
238
239 sig { params(dep: T.any(Requirement, Dependency)).returns(String) }
240 def dep_display_name(dep)
241 str = if dep.is_a? Requirement
242 if args.include_requirements?
243 ":#{dep.display_s}"
244 else
245 # This shouldn't happen, but we'll put something here to help debugging
…lines 246–259 unchanged (14 lines)
260 str = "#{str} [implicit]" if dep.implicit?
261 end
262
263 str
264 end
265
266 sig {
267 params(dependency: T.any(Formula, CaskDependent), recursive: T::Boolean)
268 .returns(T::Array[T.any(Dependency, Requirement)])
269 }
270 def deps_for_dependent(dependency, recursive: false)
271 includes, ignores = args_includes_ignores(args)
272
273 deps = dependency.runtime_dependencies if @use_runtime_dependencies
274
275 if recursive
…lines 276–279 unchanged (4 lines)
280 reqs = select_includes(dependency.requirements, ignores, includes)
281 end
282
283 deps + reqs.to_a
284 end
285
286 sig {
287 params(
288 dependents: T::Array[T.any(Formula, CaskDependent)],
289 deps_combine_mode: DepsCombineMode,
290 recursive: T::Boolean,
291 ).returns(T::Array[T.any(Dependency, Requirement)])
292 }
293 def deps_for_dependents(dependents, deps_combine_mode:, recursive:)
294 symbol = (deps_combine_mode == DepsCombineMode::Intersection) ? :& : :|
295 dependents.map { deps_for_dependent(it, recursive:) }.reduce(symbol)
296 end
297
298 sig { params(dependents: T::Array[T.any(Formula, CaskDependent)]).void }
299 def check_head_spec(dependents)
300 headless = dependents.select { it.is_a?(Formula) && it.active_spec_sym != :head }
301 .to_sentence two_words_connector: " or ", last_word_connector: " or "
302 opoo "No head spec for #{headless}, using stable spec instead" unless headless.empty?
303 end
304
305 sig { params(dependents: T::Array[T.any(Formula, CaskDependent)], recursive: T::Boolean).void }
306 def puts_deps(dependents, recursive: false)
307 check_head_spec(dependents) if args.HEAD?
308 dependents.each do |dependent|
309 deps = deps_for_dependent(dependent, recursive:)
310 condense_requirements(deps)
311 deps.sort_by!(&:name)
312 deps.map! { dep_display_name(it) }
313 puts "#{dependent.full_name}: #{deps.join(" ")}"
314 end
315 end
316
317 sig { params(dependents: T::Array[T.any(Formula, CaskDependent)], recursive: T::Boolean).returns(String) }
318 def dot_code(dependents, recursive:)
319 dep_graph = {}
320 dependents.each { graph_deps(it, dep_graph:, recursive:) }
321
322 dot_code = dep_graph.map do |d, deps|
323 deps.map do |dep|
324 attributes = []
325 attributes << "style = dotted" if dep.build?
326 attributes << "arrowhead = empty" if dep.test?
…lines 327–332 unchanged (6 lines)
333 " \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}"
334 end
335 end.flatten.join("\n")
336 "digraph {\n#{dot_code}\n}"
337 end
338
339 sig {
340 params(
341 formula: T.any(Formula, CaskDependent),
342 dep_graph: T::Hash[T.any(Formula, CaskDependent), T::Array[T.any(Dependency, Requirement)]],
343 recursive: T::Boolean,
344 ).void
345 }
346 def graph_deps(formula, dep_graph:, recursive:)
347 return if dep_graph.key?(formula)
348
349 dependables = dependables(formula)
350 dep_graph[formula] = dependables
351 return unless recursive
…lines 352–355 unchanged (4 lines)
356 graph_deps(Formulary.factory(dep.name),
357 dep_graph:,
358 recursive: true)
359 end
360 end
361
362 sig { params(dependents: T::Array[T.any(Formula, CaskDependent)], recursive: T::Boolean).void }
363 def puts_deps_tree(dependents, recursive: false)
364 check_head_spec(dependents) if args.HEAD?
365 dependents.each do |d|
366 puts d.full_name
367 recursive_deps_tree(d, deps_seen: {}, prefix: "", recursive:)
368 puts
369 end
370 end
371
372 sig { params(formula: T.any(Formula, CaskDependent)).returns(T::Array[T.any(Dependency, Requirement)]) }
373 def dependables(formula)
374 includes, ignores = args_includes_ignores(args)
375 deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps
376 deps = select_includes(deps, ignores, includes)
377 reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements?
378 reqs ||= []
379 reqs + deps
380 end
381
382 sig {
383 params(
384 formula: T.any(Formula, CaskDependent),
385 deps_seen: T::Hash[String, T::Boolean],
386 prefix: String, recursive: T::Boolean
387 ).void
388 }
389 def recursive_deps_tree(formula, deps_seen:, prefix:, recursive:)
390 dependables = dependables(formula)
391 max = dependables.length - 1
392 deps_seen[formula.name] = true
393 dependables.each_with_index do |dep, i|
394 tree_lines = if i == max
…lines 395–435 unchanged (41 lines)