Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/deps: add args to filter output #17129

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 32 additions & 11 deletions Library/Homebrew/cmd/deps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,30 @@
description: "List dependencies by their full name."
switch "--include-build",
description: "Include `:build` dependencies for <formula>."
switch "--direct-build",
depends_on: "--include-build",
description: "Only include `:build` dependencies that are direct dependencies declared " \
"in the formula."
switch "--include-optional",
description: "Include `:optional` dependencies for <formula>."
switch "--include-test",
description: "Include `:test` dependencies for <formula> (non-recursive)."
description: "Include `:test` dependencies for <formula> (non-recursive for flat output)."
switch "--direct-test",
depends_on: "--include-test",
description: "Only include `:test` dependencies that are direct dependencies declared " \
"in the formula when showing non-flat output like `--tree` or `--graph`."
Comment on lines 42 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we'd be better off just making the --direct-test behavior the default for the --include-test option instead. It's always been a bit weird that the output is different depending on whether you're using the tree, graph or flat output.

switch "--skip-recommended",
description: "Skip `:recommended` dependencies for <formula>."
comma_array "--skip",
description: "Skip specified dependencies in the output."
switch "--include-requirements",
description: "Include requirements in addition to dependencies for <formula>."
switch "--tree",
description: "Show dependencies as a tree. When given multiple formula arguments, " \
"show individual trees for each formula."
flag "--level=",
depends_on: "--tree",
description: "Limit depth of recursive dependencies in `--tree`."
switch "--graph",
description: "Show dependencies as a directed graph."
switch "--dot",
Expand Down Expand Up @@ -75,6 +88,7 @@
switch "--cask", "--casks",
description: "Treat all named arguments as casks."

conflicts "--direct", "--level"
conflicts "--tree", "--graph"
conflicts "--installed", "--missing"
conflicts "--installed", "--eval-all"
Expand All @@ -88,6 +102,9 @@
def run
raise UsageError, "`brew deps --os=all` is not supported" if args.os == "all"
raise UsageError, "`brew deps --arch=all` is not supported" if args.arch == "all"
if (level = args.level) && !level.match?(/^[1-9][0-9]*$/)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (level = args.level) && !level.match?(/^[1-9][0-9]*$/)
if (level = args.level) && level.to_i > 0

maybe with exception rescue required

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing some basic testing: it doesn't look like a rescue is required as invalid input will just end up being 0 from to_i.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using String#to_i is slightly more permissive of garbage input but probably fine for this use case.

irb(main):001:0> "skdfjsdklj100".to_i
=> 0
irb(main):002:0> "100alskdfsdkjfs".to_i
=> 100

raise UsageError, "`brew deps --level=<n>` needs a positive integer depth"
end

os, arch = T.must(args.os_arch_combinations.first)
all = args.eval_all?
Expand All @@ -107,6 +124,7 @@
!args.include_optional? &&
!args.skip_recommended? &&
!args.missing? &&
args.skip.blank? &&
args.os.nil? &&
args.arch.nil?

Expand Down Expand Up @@ -213,15 +231,15 @@
end

def deps_for_dependent(dependency, recursive: false)
includes, ignores = args_includes_ignores(args)
includes, ignores, recursive_ignores = args_includes_ignores(args)

deps = dependency.runtime_dependencies if @use_runtime_dependencies

if recursive
deps ||= recursive_includes(Dependency, dependency, includes, ignores)
deps ||= recursive_includes(Dependency, dependency, includes, ignores, recursive_ignores:, skip: args.skip)
reqs = recursive_includes(Requirement, dependency, includes, ignores)
else
deps ||= select_includes(dependency.deps, ignores, includes)
deps ||= select_includes(dependency.deps, ignores, includes, skip: args.skip)
reqs = select_includes(dependency.requirements, ignores, includes)
end

Expand Down Expand Up @@ -272,10 +290,10 @@
"digraph {\n#{dot_code}\n}"
end

def graph_deps(formula, dep_graph:, recursive:)
def graph_deps(formula, dep_graph:, recursive:, recursing: false)
return if dep_graph.key?(formula)

dependables = dependables(formula)
dependables = dependables(formula, recursing:)

Check warning on line 296 in Library/Homebrew/cmd/deps.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/deps.rb#L296

Added line #L296 was not covered by tests
dep_graph[formula] = dependables
return unless recursive

Expand All @@ -284,7 +302,8 @@

graph_deps(Formulary.factory(dep.name),
dep_graph:,
recursive: true)
recursive: true,
recursing: true)
end
end

Expand All @@ -297,17 +316,18 @@
end
end

def dependables(formula)
includes, ignores = args_includes_ignores(args)
def dependables(formula, recursing: false)
includes, ignores, recursive_ignores = args_includes_ignores(args)

Check warning on line 320 in Library/Homebrew/cmd/deps.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/deps.rb#L320

Added line #L320 was not covered by tests
includes -= recursive_ignores if recursing
deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps
deps = select_includes(deps, ignores, includes)
deps = select_includes(deps, ignores, includes, skip: args.skip)

Check warning on line 323 in Library/Homebrew/cmd/deps.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/deps.rb#L323

Added line #L323 was not covered by tests
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)
dependables = dependables(formula, recursing: dep_stack.length.positive?)

Check warning on line 330 in Library/Homebrew/cmd/deps.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/deps.rb#L330

Added line #L330 was not covered by tests
max = dependables.length - 1
dep_stack.push formula.name
dependables.each_with_index do |dep, i|
Expand All @@ -329,6 +349,7 @@
puts "#{prefix}#{display_s}"

next if !recursive || is_circular
next if dep_stack.length == args.level.to_i

prefix_addition = if i == max
" "
Expand Down
20 changes: 15 additions & 5 deletions Library/Homebrew/dependencies_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,28 @@ def args_includes_ignores(args)
ignores << :recommended? if args.skip_recommended?
ignores << :satisfied? if args.missing?

[includes, ignores]
recursive_ignores = []
recursive_ignores << :build? if args.respond_to?(:direct_build?) && args.direct_build?
recursive_ignores << :test? if args.respond_to?(:direct_test?) && args.direct_test?

[includes, ignores, recursive_ignores]
end

def recursive_includes(klass, root_dependent, includes, ignores)
def recursive_includes(klass, root_dependent, includes, ignores, recursive_ignores: nil, skip: nil)
raise ArgumentError, "Invalid class argument: #{klass}" if klass != Dependency && klass != Requirement

cache_key = "recursive_includes_#{includes}_#{ignores}"
cache_key = "#{cache_key}_#{recursive_ignores}" if recursive_ignores.present?
cache_key = "#{cache_key}_#{skip}" if skip.present?
apainintheneck marked this conversation as resolved.
Show resolved Hide resolved
recursive_ignores = Array(recursive_ignores)
# Ignore indirect test dependencies
recursive_ignores << :test? if recursive_ignores.exclude?(:test?)

klass.expand(root_dependent, cache_key:) do |dependent, dep|
klass.prune if skip&.include?(dep.name)
klass.prune if ignores.any? { |ignore| dep.public_send(ignore) }
klass.prune if includes.none? do |include|
# Ignore indirect test dependencies
next if include == :test? && dependent != root_dependent
next if recursive_ignores.include?(include) && dependent != root_dependent

dep.public_send(include)
end
Expand All @@ -40,8 +49,9 @@ def recursive_includes(klass, root_dependent, includes, ignores)
end
end

def select_includes(dependables, ignores, includes)
def select_includes(dependables, ignores, includes, skip: nil)
dependables.select do |dep|
next false if skip&.include?(dep.name)
next false if ignores.any? { |ignore| dep.public_send(ignore) }

includes.any? { |include| dep.public_send(include) }
Expand Down
12 changes: 12 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/deps.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 130 additions & 0 deletions Library/Homebrew/test/dependencies_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,136 @@
require "dependencies_helpers"

RSpec.describe DependenciesHelpers do
let(:default_includes) { [:required?, :recommended?] }
let(:includes_with_build) { default_includes + [:build?] }

describe "#args_includes_ignores" do
include described_class

let(:args) { Homebrew::CLI::Args.new }

it "returns default includes when no extra args are provided" do
expect(args_includes_ignores(args)).to eq [default_includes, [], []]
end

it "returns includes and ignores matching the provided args" do
args[:include_build?] = true
args[:skip_recommended?] = true
args[:direct_build?] = true
expect(args_includes_ignores(args)).to eq [includes_with_build, [:recommended?], [:build?]]
end
end

describe "#recursive_includes" do
include described_class

let(:dependency_formulae) do
deps = (0..7).map do |i|
formula "f#{i}" do
url "f#{i}-1.0"
end
end
deps << formula("d1") do
url "d1-1.0"
depends_on "f0" => :build
depends_on "f1" => :test
depends_on "f2"
end
deps << formula("d2") do
url "d2-1.0"
depends_on "f3" => :build
depends_on "f4"
end
deps << formula("d3") do
url "d3-1.0"
depends_on "f5" => :test
depends_on "f6"
end
deps
end

let(:test_formula) do
formula("test_formula") do
url "test_formula-1.0"
depends_on "d1" => :build
depends_on "d2" => [:recommended, :test]
depends_on "d3"
depends_on "f7"
end
end

before do
dependency_formulae.each { |f| stub_formula_loader f }
stub_formula_loader test_formula
end

it "returns an empty array when provided empty `includes`" do
expect(recursive_includes(Dependency, test_formula, [], [])).to eq []
end

it "selects required and recommended dependencies using `includes`" do
deps = recursive_includes(Dependency, test_formula, default_includes, [])
expect(deps.map(&:name).sort).to eq %w[d2 d3 f4 f6 f7]
end

it "selects required, recommended and build dependencies using `includes`" do
deps = recursive_includes(Dependency, test_formula, includes_with_build, [])
expect(deps.map(&:name).sort).to eq %w[d1 d2 d3 f0 f2 f3 f4 f6 f7]
end

it "does not select recommended dependencies when recommended is in both `includes` and `ignores`" do
deps = recursive_includes(Dependency, test_formula, default_includes, [:recommended?])
expect(deps.map(&:name).sort).to eq %w[d3 f6 f7]
end

it "does not return any dependency in `skip`" do
deps = recursive_includes(Dependency, test_formula, default_includes, [], skip: %w[d2 f6])
expect(deps.map(&:name).sort).to eq %w[d3 f7]
end

it "can ignore recursive build dependencies using `recursive_ignores`" do
deps = recursive_includes(Dependency, test_formula, includes_with_build, [], recursive_ignores: [:build?])
expect(deps.map(&:name).sort).to eq %w[d1 d2 d3 f2 f4 f6 f7]
end
end

describe "#select_includes" do
include described_class

let(:runtime_dep) { Dependency.new("runtime_dep") }
let(:optional_dep) { Dependency.new("optional_dep", [:optional]) }
let(:build_dep) { Dependency.new("build_dep", [:build]) }
let(:recommended_dep) { Dependency.new("recommended_dep", [:recommended]) }
let(:build_test_dep) { Dependency.new("build_test_dep", [:build, :test]) }
let(:build_recommended_dep) { Dependency.new("build_recommended_dep", [:build, :recommended]) }
let(:dependables) do
[runtime_dep, optional_dep, build_dep, recommended_dep, build_test_dep, build_recommended_dep]
end

it "returns an empty array when provided empty `includes`" do
expect(select_includes(dependables, [], [])).to eq []
end

it "selects required and recommended dependencies using `includes`" do
expect(select_includes(dependables, [], default_includes))
.to eq [runtime_dep, recommended_dep, build_recommended_dep]
end

it "selects required, recommended and build dependencies using `includes`" do
expect(select_includes(dependables, [], includes_with_build))
.to eq [runtime_dep, build_dep, recommended_dep, build_test_dep, build_recommended_dep]
end

it "does not select recommended dependencies when recommended is in both `includes` and `ignores`" do
expect(select_includes(dependables, [:recommended?], default_includes)).to eq [runtime_dep]
end

it "does not return any dependency in `skip`" do
expect(select_includes(dependables, [], includes_with_build, skip: ["runtime_dep", "build_test_dep"]))
.to eq [build_dep, recommended_dep, build_recommended_dep]
end
end

specify "#dependents" do
foo = formula "foo" do
url "foo"
Expand Down
4 changes: 4 additions & 0 deletions completions/bash/brew
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ _brew_deps() {
--cask
--debug
--direct
--direct-build
--direct-test
--dot
--eval-all
--for-each
Expand All @@ -757,9 +759,11 @@ _brew_deps() {
--include-requirements
--include-test
--installed
--level
--missing
--os
--quiet
--skip
--skip-recommended
--topological
--tree
Expand Down
6 changes: 5 additions & 1 deletion completions/fish/brew.fish
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,8 @@ __fish_brew_complete_arg 'deps' -l arch -d 'Show dependencies for the given CPU
__fish_brew_complete_arg 'deps' -l cask -d 'Treat all named arguments as casks'
__fish_brew_complete_arg 'deps' -l debug -d 'Display any debugging information'
__fish_brew_complete_arg 'deps' -l direct -d 'Show only the direct dependencies declared in the formula'
__fish_brew_complete_arg 'deps' -l direct-build -d 'Only include `:build` dependencies that are direct dependencies declared in the formula'
__fish_brew_complete_arg 'deps' -l direct-test -d 'Only include `:test` dependencies that are direct dependencies declared in the formula when showing non-flat output like `--tree` or `--graph`'
__fish_brew_complete_arg 'deps' -l dot -d 'Show text-based graph description in DOT format'
__fish_brew_complete_arg 'deps' -l eval-all -d 'Evaluate all available formulae and casks, whether installed or not, to list their dependencies'
__fish_brew_complete_arg 'deps' -l for-each -d 'Switch into the mode used by the `--eval-all` option, but only list dependencies for each provided formula, one formula per line. This is used for debugging the `--installed`/`--eval-all` display mode'
Expand All @@ -580,11 +582,13 @@ __fish_brew_complete_arg 'deps' -l help -d 'Show this message'
__fish_brew_complete_arg 'deps' -l include-build -d 'Include `:build` dependencies for formula'
__fish_brew_complete_arg 'deps' -l include-optional -d 'Include `:optional` dependencies for formula'
__fish_brew_complete_arg 'deps' -l include-requirements -d 'Include requirements in addition to dependencies for formula'
__fish_brew_complete_arg 'deps' -l include-test -d 'Include `:test` dependencies for formula (non-recursive)'
__fish_brew_complete_arg 'deps' -l include-test -d 'Include `:test` dependencies for formula (non-recursive for flat output)'
__fish_brew_complete_arg 'deps' -l installed -d 'List dependencies for formulae that are currently installed. If formula is specified, list only its dependencies that are currently installed'
__fish_brew_complete_arg 'deps' -l level -d 'Limit depth of recursive dependencies in `--tree`'
__fish_brew_complete_arg 'deps' -l missing -d 'Show only missing dependencies'
__fish_brew_complete_arg 'deps' -l os -d 'Show dependencies for the given operating system'
__fish_brew_complete_arg 'deps' -l quiet -d 'Make some output more quiet'
__fish_brew_complete_arg 'deps' -l skip -d 'Skip specified dependencies in the output'
__fish_brew_complete_arg 'deps' -l skip-recommended -d 'Skip `:recommended` dependencies for formula'
__fish_brew_complete_arg 'deps' -l topological -d 'Sort dependencies in topological order'
__fish_brew_complete_arg 'deps' -l tree -d 'Show dependencies as a tree. When given multiple formula arguments, show individual trees for each formula'
Expand Down