diff --git a/Library/Homebrew/cmd/deps.rb b/Library/Homebrew/cmd/deps.rb index 2a664ebaa0..e8ae5c5a11 100644 --- a/Library/Homebrew/cmd/deps.rb +++ b/Library/Homebrew/cmd/deps.rb @@ -1,340 +1,337 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "formula" -require "cli/parser" require "cask/caskroom" require "dependencies_helpers" module Homebrew - extend DependenciesHelpers + module Cmd + class Deps < AbstractCommand + include DependenciesHelpers + cmd_args do + description <<~EOS + Show dependencies for . When given multiple formula arguments, + show the intersection of dependencies for each formula. By default, `deps` + shows all required and recommended dependencies. - sig { returns(CLI::Parser) } - def self.deps_args - Homebrew::CLI::Parser.new do - description <<~EOS - Show dependencies for . When given multiple formula arguments, - show the intersection of dependencies for each formula. By default, `deps` - shows all required and recommended dependencies. + If any version of each formula argument is installed and no other options + are passed, this command displays their actual runtime dependencies (similar + to `brew linkage`), which may differ from the current versions' stated + dependencies if the installed versions are outdated. - If any version of each formula argument is installed and no other options - are passed, this command displays their actual runtime dependencies (similar - to `brew linkage`), which may differ from the current versions' stated - dependencies if the installed versions are outdated. + *Note:* `--missing` and `--skip-recommended` have precedence over `--include-*`. + EOS + switch "-n", "--topological", + description: "Sort dependencies in topological order." + switch "-1", "--direct", "--declared", "--1", + description: "Show only the direct dependencies declared in the formula." + switch "--union", + description: "Show the union of dependencies for multiple , instead of the intersection." + switch "--full-name", + description: "List dependencies by their full name." + switch "--include-build", + description: "Include `:build` dependencies for ." + switch "--include-optional", + description: "Include `:optional` dependencies for ." + switch "--include-test", + description: "Include `:test` dependencies for (non-recursive)." + switch "--skip-recommended", + description: "Skip `:recommended` dependencies for ." + switch "--include-requirements", + description: "Include requirements in addition to dependencies for ." + switch "--tree", + description: "Show dependencies as a tree. When given multiple formula arguments, " \ + "show individual trees for each formula." + switch "--graph", + description: "Show dependencies as a directed graph." + switch "--dot", + depends_on: "--graph", + description: "Show text-based graph description in DOT format." + switch "--annotate", + description: "Mark any build, test, implicit, optional, or recommended dependencies as " \ + "such in the output." + switch "--installed", + description: "List dependencies for formulae that are currently installed. If is " \ + "specified, list only its dependencies that are currently installed." + switch "--missing", + description: "Show only missing dependencies." + switch "--eval-all", + description: "Evaluate all available formulae and casks, whether installed or not, to list " \ + "their dependencies." + switch "--for-each", + description: "Switch into the mode used by the `--eval-all` option, but only list dependencies " \ + "for each provided , one formula per line. This is used for " \ + "debugging the `--installed`/`--eval-all` display mode." + switch "--HEAD", + description: "Show dependencies for HEAD version instead of stable version." + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - *Note:* `--missing` and `--skip-recommended` have precedence over `--include-*`. - EOS - switch "-n", "--topological", - description: "Sort dependencies in topological order." - switch "-1", "--direct", "--declared", "--1", - description: "Show only the direct dependencies declared in the formula." - switch "--union", - description: "Show the union of dependencies for multiple , instead of the intersection." - switch "--full-name", - description: "List dependencies by their full name." - switch "--include-build", - description: "Include `:build` dependencies for ." - switch "--include-optional", - description: "Include `:optional` dependencies for ." - switch "--include-test", - description: "Include `:test` dependencies for (non-recursive)." - switch "--skip-recommended", - description: "Skip `:recommended` dependencies for ." - switch "--include-requirements", - description: "Include requirements in addition to dependencies for ." - switch "--tree", - description: "Show dependencies as a tree. When given multiple formula arguments, " \ - "show individual trees for each formula." - switch "--graph", - description: "Show dependencies as a directed graph." - switch "--dot", - depends_on: "--graph", - description: "Show text-based graph description in DOT format." - switch "--annotate", - description: "Mark any build, test, implicit, optional, or recommended dependencies as " \ - "such in the output." - switch "--installed", - description: "List dependencies for formulae that are currently installed. If is " \ - "specified, list only its dependencies that are currently installed." - switch "--missing", - description: "Show only missing dependencies." - switch "--eval-all", - description: "Evaluate all available formulae and casks, whether installed or not, to list " \ - "their dependencies." - switch "--for-each", - description: "Switch into the mode used by the `--eval-all` option, but only list dependencies " \ - "for each provided , one formula per line. This is used for " \ - "debugging the `--installed`/`--eval-all` display mode." - switch "--HEAD", - description: "Show dependencies for HEAD version instead of stable version." - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--tree", "--graph" + conflicts "--installed", "--missing" + conflicts "--installed", "--eval-all" + conflicts "--formula", "--cask" + formula_options - conflicts "--tree", "--graph" - conflicts "--installed", "--missing" - conflicts "--installed", "--eval-all" - conflicts "--formula", "--cask" - formula_options + named_args [:formula, :cask] + end - named_args [:formula, :cask] - end - end + sig { override.void } + def run + all = args.eval_all? - def self.deps - args = deps_args.parse + Formulary.enable_factory_cache! - all = args.eval_all? + recursive = !args.direct? + installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?) - Formulary.enable_factory_cache! + @use_runtime_dependencies = installed && recursive && + !args.tree? && + !args.graph? && + !args.HEAD? && + !args.include_build? && + !args.include_test? && + !args.include_optional? && + !args.skip_recommended? && + !args.missing? - recursive = !args.direct? - installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?) + if args.tree? || args.graph? + dependents = if args.named.present? + sorted_dependents(args.named.to_formulae_and_casks) + elsif args.installed? + case args.only_formula_or_cask + when :formula + sorted_dependents(Formula.installed) + when :cask + sorted_dependents(Cask::Caskroom.casks) + else + sorted_dependents(Formula.installed + Cask::Caskroom.casks) + end + else + raise FormulaUnspecifiedError + end - @use_runtime_dependencies = installed && recursive && - !args.tree? && - !args.graph? && - !args.HEAD? && - !args.include_build? && - !args.include_test? && - !args.include_optional? && - !args.skip_recommended? && - !args.missing? + if args.graph? + dot_code = dot_code(dependents, recursive:) + if args.dot? + puts dot_code + else + exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}" + end + return + end - if args.tree? || args.graph? - dependents = if args.named.present? - sorted_dependents(args.named.to_formulae_and_casks) - elsif args.installed? - case args.only_formula_or_cask - when :formula - sorted_dependents(Formula.installed) - when :cask - sorted_dependents(Cask::Caskroom.casks) + puts_deps_tree(dependents, recursive:) + return + elsif all + puts_deps(sorted_dependents( + Formula.all(eval_all: args.eval_all?) + Cask::Cask.all(eval_all: args.eval_all?), + ), recursive:) + return + elsif !args.no_named? && args.for_each? + puts_deps(sorted_dependents(args.named.to_formulae_and_casks), recursive:) + return + end + + if args.no_named? + raise FormulaUnspecifiedError unless args.installed? + + sorted_dependents_formulae_and_casks = case args.only_formula_or_cask + when :formula + sorted_dependents(Formula.installed) + when :cask + sorted_dependents(Cask::Caskroom.casks) + else + sorted_dependents(Formula.installed + Cask::Caskroom.casks) + end + puts_deps(sorted_dependents_formulae_and_casks, recursive:) + return + end + + dependents = dependents(args.named.to_formulae_and_casks) + check_head_spec(dependents) if args.HEAD? + + all_deps = deps_for_dependents(dependents, recursive:, &(args.union? ? :| : :&)) + condense_requirements(all_deps) + all_deps.map! { |d| dep_display_name(d) } + all_deps.uniq! + all_deps.sort! unless args.topological? + puts all_deps + end + + def sorted_dependents(formulae_or_casks) + dependents(formulae_or_casks).sort_by(&:name) + end + + def condense_requirements(deps) + deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements? + deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed? + end + + def dep_display_name(dep) + str = if dep.is_a? Requirement + if args.include_requirements? + ":#{dep.display_s}" + else + # This shouldn't happen, but we'll put something here to help debugging + "::#{dep.name}" + end + elsif args.full_name? + dep.to_formula.full_name else - sorted_dependents(Formula.installed + Cask::Caskroom.casks) + dep.name end - else - raise FormulaUnspecifiedError + + if args.annotate? + str = "#{str} " if args.tree? + str = "#{str} [build]" if dep.build? + str = "#{str} [test]" if dep.test? + str = "#{str} [optional]" if dep.optional? + str = "#{str} [recommended]" if dep.recommended? + str = "#{str} [implicit]" if dep.implicit? + end + + str end - if args.graph? - dot_code = dot_code(dependents, recursive:, args:) - if args.dot? - puts dot_code + def deps_for_dependent(dependency, recursive: false) + includes, ignores = args_includes_ignores(args) + + deps = dependency.runtime_dependencies if @use_runtime_dependencies + + if recursive + deps ||= recursive_includes(Dependency, dependency, includes, ignores) + reqs = recursive_includes(Requirement, dependency, includes, ignores) else - exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}" + deps ||= select_includes(dependency.deps, ignores, includes) + reqs = select_includes(dependency.requirements, ignores, includes) end - return + + deps + reqs.to_a end - puts_deps_tree(dependents, recursive:, args:) - return - elsif all - puts_deps(sorted_dependents( - Formula.all(eval_all: args.eval_all?) + Cask::Cask.all(eval_all: args.eval_all?), - ), recursive:, args:) - return - elsif !args.no_named? && args.for_each? - puts_deps(sorted_dependents(args.named.to_formulae_and_casks), recursive:, args:) - return - end - - if args.no_named? - raise FormulaUnspecifiedError unless args.installed? - - sorted_dependents_formulae_and_casks = case args.only_formula_or_cask - when :formula - sorted_dependents(Formula.installed) - when :cask - sorted_dependents(Cask::Caskroom.casks) - else - sorted_dependents(Formula.installed + Cask::Caskroom.casks) + def deps_for_dependents(dependents, recursive: false, &block) + dependents.map { |d| deps_for_dependent(d, recursive:) }.reduce(&block) end - puts_deps(sorted_dependents_formulae_and_casks, recursive:, args:) - return - end - dependents = dependents(args.named.to_formulae_and_casks) - check_head_spec(dependents) if args.HEAD? - - all_deps = deps_for_dependents(dependents, recursive:, args:, &(args.union? ? :| : :&)) - condense_requirements(all_deps, args:) - all_deps.map! { |d| dep_display_name(d, args:) } - all_deps.uniq! - all_deps.sort! unless args.topological? - puts all_deps - end - - def self.sorted_dependents(formulae_or_casks) - dependents(formulae_or_casks).sort_by(&:name) - end - - def self.condense_requirements(deps, args:) - deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements? - deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed? - end - - def self.dep_display_name(dep, args:) - str = if dep.is_a? Requirement - if args.include_requirements? - ":#{dep.display_s}" - else - # This shouldn't happen, but we'll put something here to help debugging - "::#{dep.name}" + def check_head_spec(dependents) + headless = dependents.select { |d| d.is_a?(Formula) && d.active_spec_sym != :head } + .to_sentence two_words_connector: " or ", last_word_connector: " or " + opoo "No head spec for #{headless}, using stable spec instead" unless headless.empty? end - elsif args.full_name? - dep.to_formula.full_name - else - dep.name - end - if args.annotate? - str = "#{str} " if args.tree? - str = "#{str} [build]" if dep.build? - str = "#{str} [test]" if dep.test? - str = "#{str} [optional]" if dep.optional? - str = "#{str} [recommended]" if dep.recommended? - str = "#{str} [implicit]" if dep.implicit? - end - - str - end - - def self.deps_for_dependent(dependency, args:, recursive: false) - includes, ignores = args_includes_ignores(args) - - deps = dependency.runtime_dependencies if @use_runtime_dependencies - - if recursive - deps ||= recursive_includes(Dependency, dependency, includes, ignores) - reqs = recursive_includes(Requirement, dependency, includes, ignores) - else - deps ||= select_includes(dependency.deps, ignores, includes) - reqs = select_includes(dependency.requirements, ignores, includes) - end - - deps + reqs.to_a - end - - def self.deps_for_dependents(dependents, args:, recursive: false, &block) - dependents.map { |d| deps_for_dependent(d, recursive:, args:) }.reduce(&block) - end - - def self.check_head_spec(dependents) - headless = dependents.select { |d| d.is_a?(Formula) && d.active_spec_sym != :head } - .to_sentence two_words_connector: " or ", last_word_connector: " or " - opoo "No head spec for #{headless}, using stable spec instead" unless headless.empty? - end - - def self.puts_deps(dependents, args:, recursive: false) - check_head_spec(dependents) if args.HEAD? - dependents.each do |dependent| - deps = deps_for_dependent(dependent, recursive:, args:) - condense_requirements(deps, args:) - deps.sort_by!(&:name) - deps.map! { |d| dep_display_name(d, args:) } - puts "#{dependent.full_name}: #{deps.join(" ")}" - end - end - - def self.dot_code(dependents, recursive:, args:) - dep_graph = {} - dependents.each do |d| - graph_deps(d, dep_graph:, recursive:, args:) - end - - dot_code = dep_graph.map do |d, deps| - deps.map do |dep| - attributes = [] - attributes << "style = dotted" if dep.build? - attributes << "arrowhead = empty" if dep.test? - if dep.optional? - attributes << "color = red" - elsif dep.recommended? - attributes << "color = green" + def puts_deps(dependents, recursive: false) + check_head_spec(dependents) if args.HEAD? + dependents.each do |dependent| + deps = deps_for_dependent(dependent, recursive:) + condense_requirements(deps) + deps.sort_by!(&:name) + deps.map! { |d| dep_display_name(d) } + puts "#{dependent.full_name}: #{deps.join(" ")}" end - comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any? - " \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}" end - end.flatten.join("\n") - "digraph {\n#{dot_code}\n}" - end - def self.graph_deps(formula, dep_graph:, recursive:, args:) - return if dep_graph.key?(formula) + def dot_code(dependents, recursive:) + dep_graph = {} + dependents.each do |d| + graph_deps(d, dep_graph:, recursive:) + end - dependables = dependables(formula, args:) - dep_graph[formula] = dependables - return unless recursive + dot_code = dep_graph.map do |d, deps| + deps.map do |dep| + attributes = [] + attributes << "style = dotted" if dep.build? + attributes << "arrowhead = empty" if dep.test? + if dep.optional? + attributes << "color = red" + elsif dep.recommended? + attributes << "color = green" + end + comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any? + " \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}" + end + end.flatten.join("\n") + "digraph {\n#{dot_code}\n}" + end - dependables.each do |dep| - next unless dep.is_a? Dependency + def graph_deps(formula, dep_graph:, recursive:) + return if dep_graph.key?(formula) - graph_deps(Formulary.factory(dep.name), - dep_graph:, - recursive: true, - args:) + dependables = dependables(formula) + dep_graph[formula] = dependables + return unless recursive + + dependables.each do |dep| + next unless dep.is_a? Dependency + + graph_deps(Formulary.factory(dep.name), + dep_graph:, + recursive: true) + end + end + + def puts_deps_tree(dependents, recursive: false) + check_head_spec(dependents) if args.HEAD? + dependents.each do |d| + puts d.full_name + recursive_deps_tree(d, dep_stack: [], prefix: "", recursive:) + puts + end + end + + def dependables(formula) + includes, ignores = args_includes_ignores(args) + deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps + deps = select_includes(deps, ignores, includes) + reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements? + reqs ||= [] + reqs + deps + end + + def recursive_deps_tree(formula, dep_stack:, prefix:, recursive:) + dependables = dependables(formula) + max = dependables.length - 1 + dep_stack.push formula.name + dependables.each_with_index do |dep, i| + tree_lines = if i == max + "└──" + else + "├──" + end + + display_s = "#{tree_lines} #{dep_display_name(dep)}" + + # Detect circular dependencies and consider them a failure if present. + is_circular = dep_stack.include?(dep.name) + if is_circular + display_s = "#{display_s} (CIRCULAR DEPENDENCY)" + Homebrew.failed = true + end + + puts "#{prefix}#{display_s}" + + next if !recursive || is_circular + + prefix_addition = if i == max + " " + else + "│ " + end + + next unless dep.is_a? Dependency + + recursive_deps_tree(Formulary.factory(dep.name), + dep_stack:, + prefix: prefix + prefix_addition, + recursive: true) + end + + dep_stack.pop + end end end - - def self.puts_deps_tree(dependents, args:, recursive: false) - check_head_spec(dependents) if args.HEAD? - dependents.each do |d| - puts d.full_name - recursive_deps_tree(d, dep_stack: [], prefix: "", recursive:, args:) - puts - end - end - - def self.dependables(formula, args:) - includes, ignores = args_includes_ignores(args) - deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps - deps = select_includes(deps, ignores, includes) - reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements? - reqs ||= [] - reqs + deps - end - - def self.recursive_deps_tree(formula, dep_stack:, prefix:, recursive:, args:) - dependables = dependables(formula, args:) - max = dependables.length - 1 - dep_stack.push formula.name - dependables.each_with_index do |dep, i| - tree_lines = if i == max - "└──" - else - "├──" - end - - display_s = "#{tree_lines} #{dep_display_name(dep, args:)}" - - # Detect circular dependencies and consider them a failure if present. - is_circular = dep_stack.include?(dep.name) - if is_circular - display_s = "#{display_s} (CIRCULAR DEPENDENCY)" - Homebrew.failed = true - end - - puts "#{prefix}#{display_s}" - - next if !recursive || is_circular - - prefix_addition = if i == max - " " - else - "│ " - end - - next unless dep.is_a? Dependency - - recursive_deps_tree(Formulary.factory(dep.name), - dep_stack:, - prefix: prefix + prefix_addition, - recursive: true, - args:) - end - - dep_stack.pop - end end diff --git a/Library/Homebrew/test/cmd/deps_spec.rb b/Library/Homebrew/test/cmd/deps_spec.rb index aa6277196b..7171ba0303 100644 --- a/Library/Homebrew/test/cmd/deps_spec.rb +++ b/Library/Homebrew/test/cmd/deps_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/deps" require "cmd/shared_examples/args_parse" -RSpec.describe "brew deps" do +RSpec.describe Homebrew::Cmd::Deps do it_behaves_like "parseable arguments" it "outputs all of a Formula's dependencies and their dependencies on separate lines", :integration_test do