diff --git a/Library/Homebrew/cask_dependent.rb b/Library/Homebrew/cask_dependent.rb index 8644e7776d..88c97c8df5 100644 --- a/Library/Homebrew/cask_dependent.rb +++ b/Library/Homebrew/cask_dependent.rb @@ -3,6 +3,8 @@ # An adapter for casks to provide dependency information in a formula-like interface. class CaskDependent + attr_reader :cask + def initialize(cask) @cask = cask end diff --git a/Library/Homebrew/cmd/deps.rb b/Library/Homebrew/cmd/deps.rb index 8a5c2997ab..e3d660addd 100644 --- a/Library/Homebrew/cmd/deps.rb +++ b/Library/Homebrew/cmd/deps.rb @@ -43,6 +43,11 @@ module Homebrew 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, optional, or recommended dependencies as "\ "such in the output." @@ -62,6 +67,7 @@ module Homebrew depends_on: "--installed", description: "Treat all named arguments as casks." + conflicts "--tree", "--graph" conflicts "--installed", "--all" conflicts "--formula", "--cask" formula_options @@ -80,12 +86,13 @@ module Homebrew @use_runtime_dependencies = installed && recursive && !args.tree? && + !args.graph? && !args.include_build? && !args.include_test? && !args.include_optional? && !args.skip_recommended? - if args.tree? + if args.tree? || args.graph? dependents = if args.named.present? sorted_dependents(args.named.to_formulae_and_casks) elsif args.installed? @@ -101,6 +108,16 @@ module Homebrew raise FormulaUnspecifiedError end + if args.graph? + dot_code = dot_code(dependents, recursive: recursive, args: args) + if args.dot? + puts dot_code + else + exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}" + end + return + end + puts_deps_tree dependents, recursive: recursive, args: args return elsif args.all? @@ -200,27 +217,68 @@ module Homebrew end end + def dot_code(dependents, recursive:, args:) + dep_graph = {} + dependents.each do |d| + graph_deps(d, dep_graph: dep_graph, recursive: recursive, args: 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" + end + comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any? + " \"#{d}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}" + end + end.flatten.join("\n") + "digraph {\n#{dot_code}\n}" + end + + def graph_deps(f, dep_graph:, recursive:, args:) + return if dep_graph.key?(f) + + dependables = dependables(f, args: args) + dep_graph[f] = dependables + return unless recursive + + dependables.each do |dep| + next unless dep.is_a? Dependency + + graph_deps(Formulary.factory(dep.name), + dep_graph: dep_graph, + recursive: true, + args: args) + end + end + def puts_deps_tree(dependents, args:, recursive: false) dependents.each do |d| puts d.full_name - @dep_stack = [] - recursive_deps_tree(d, "", recursive, args: args) + recursive_deps_tree(d, dep_stack: [], prefix: "", recursive: recursive, args: args) puts end end - def recursive_deps_tree(f, prefix, recursive, args:) + def dependables(f, args:) includes, ignores = args_includes_ignores(args) - dependables = @use_runtime_dependencies ? f.runtime_dependencies : f.deps - deps = reject_ignores(dependables, ignores, includes) - reqs = reject_ignores(f.requirements, ignores, includes) - dependables = reqs + deps + deps = @use_runtime_dependencies ? f.runtime_dependencies : f.deps + deps = reject_ignores(deps, ignores, includes) + reqs = reject_ignores(f.requirements, ignores, includes) if args.include_requirements? + reqs ||= [] + reqs + deps + end + def recursive_deps_tree(f, dep_stack:, prefix:, recursive:, args:) + dependables = dependables(f, args: args) max = dependables.length - 1 - @dep_stack.push f.name + dep_stack.push f.name dependables.each_with_index do |dep, i| - next if !args.include_requirements? && dep.is_a?(Requirement) - tree_lines = if i == max "└──" else @@ -228,7 +286,7 @@ module Homebrew end display_s = "#{tree_lines} #{dep_display_name(dep, args: args)}" - is_circular = @dep_stack.include?(dep.name) + is_circular = dep_stack.include?(dep.name) display_s = "#{display_s} (CIRCULAR DEPENDENCY)" if is_circular puts "#{prefix}#{display_s}" @@ -240,11 +298,15 @@ module Homebrew "│ " end - if dep.is_a? Dependency - recursive_deps_tree(Formulary.factory(dep.name), prefix + prefix_addition, true, args: args) - end + next unless dep.is_a? Dependency + + recursive_deps_tree(Formulary.factory(dep.name), + dep_stack: dep_stack, + prefix: prefix + prefix_addition, + recursive: true, + args: args) end - @dep_stack.pop + dep_stack.pop end end diff --git a/Library/Homebrew/utils/topological_hash.rb b/Library/Homebrew/utils/topological_hash.rb index c6a88b3268..cac774c857 100644 --- a/Library/Homebrew/utils/topological_hash.rb +++ b/Library/Homebrew/utils/topological_hash.rb @@ -20,7 +20,7 @@ module Utils packages = Array(packages) packages.each do |cask_or_formula| - next accumulator if accumulator.key?(cask_or_formula) + next if accumulator.key?(cask_or_formula) if cask_or_formula.is_a?(Cask::Cask) formula_deps = cask_or_formula.depends_on @@ -39,9 +39,7 @@ module Utils .map { |c| Cask::CaskLoader.load(c, config: nil) } end - accumulator[cask_or_formula] ||= [] - accumulator[cask_or_formula] += formula_deps - accumulator[cask_or_formula] += cask_deps + accumulator[cask_or_formula] = formula_deps + cask_deps graph_package_dependencies(formula_deps, accumulator) graph_package_dependencies(cask_deps, accumulator)