Skip to content

Commit

Permalink
Merge pull request Homebrew#15892 from apainintheneck/rework-recursiv…
Browse files Browse the repository at this point in the history
…e-dependency-resolution

dependency_helpers: rework recursive dependency resolution
  • Loading branch information
MikeMcQuaid committed Aug 30, 2023
2 parents 9809473 + b2b8f0e commit 851df26
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 79 deletions.
16 changes: 10 additions & 6 deletions Library/Homebrew/cmd/deps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ def self.deps_args
description <<~EOS
Show dependencies for <formula>. Additional options specific to <formula>
may be appended to the command. When given multiple formula arguments,
show the intersection of dependencies for each formula.
show the intersection of dependencies for each formula. By default, `deps`
shows all required and recommended dependencies.
Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.
EOS
switch "-n", "--topological",
description: "Sort dependencies in topological order."
Expand Down Expand Up @@ -90,7 +93,8 @@ def self.deps
!args.include_build? &&
!args.include_test? &&
!args.include_optional? &&
!args.skip_recommended?
!args.skip_recommended? &&
!args.missing?

if args.tree? || args.graph?
dependents = if args.named.present?
Expand Down Expand Up @@ -197,8 +201,8 @@ def self.deps_for_dependent(dependency, args:, recursive: false)
deps ||= recursive_includes(Dependency, dependency, includes, ignores)
reqs = recursive_includes(Requirement, dependency, includes, ignores)
else
deps ||= reject_ignores(dependency.deps, ignores, includes)
reqs = reject_ignores(dependency.requirements, ignores, includes)
deps ||= select_includes(dependency.deps, ignores, includes)
reqs = select_includes(dependency.requirements, ignores, includes)
end

deps + reqs.to_a
Expand Down Expand Up @@ -269,8 +273,8 @@ def self.puts_deps_tree(dependents, args:, recursive: false)
def self.dependables(formula, args:)
includes, ignores = args_includes_ignores(args)
deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps
deps = reject_ignores(deps, ignores, includes)
reqs = reject_ignores(formula.requirements, ignores, includes) if args.include_requirements?
deps = select_includes(deps, ignores, includes)
reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements?
reqs ||= []
reqs + deps
end
Expand Down
24 changes: 19 additions & 5 deletions Library/Homebrew/cmd/uses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def self.uses_args
of <formula>. When given multiple formula arguments, show the intersection
of formulae that use <formula>. By default, `uses` shows all formulae and casks that
specify <formula> as a required or recommended dependency for their stable builds.
Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.
EOS
switch "--recursive",
description: "Resolve more than one level of dependencies."
Expand All @@ -33,13 +35,13 @@ def self.uses_args
description: "Evaluate all available formulae and casks, whether installed or not, to show " \
"their dependents."
switch "--include-build",
description: "Include all formulae that specify <formula> as `:build` type dependency."
description: "Include formulae that specify <formula> as a `:build` dependency."
switch "--include-test",
description: "Include all formulae that specify <formula> as `:test` type dependency."
description: "Include formulae that specify <formula> as a `:test` dependency."
switch "--include-optional",
description: "Include all formulae that specify <formula> as `:optional` type dependency."
description: "Include formulae that specify <formula> as an `:optional` dependency."
switch "--skip-recommended",
description: "Skip all formulae that specify <formula> as `:recommended` type dependency."
description: "Skip all formulae that specify <formula> as a `:recommended` dependency."
switch "--formula", "--formulae",
description: "Include only formulae."
switch "--cask", "--casks",
Expand Down Expand Up @@ -120,6 +122,18 @@ def self.intersection_of_dependents(use_runtime_dependents, used_formulae, args:
deps += args.installed? ? Cask::Caskroom.casks : Cask::Cask.all
end

if args.missing?
deps.reject! do |dep|
case dep
when Formula
dep.any_version_installed?
when Cask::Cask
dep.installed?
end
end
ignores.delete(:satisfied?)
end

select_used_dependents(dependents(deps), used_formulae, recursive, includes, ignores)
end
end
Expand All @@ -129,7 +143,7 @@ def self.select_used_dependents(dependents, used_formulae, recursive, includes,
deps = if recursive
recursive_includes(Dependency, d, includes, ignores)
else
reject_ignores(d.deps, ignores, includes)
select_includes(d.deps, ignores, includes)
end

used_formulae.all? do |ff|
Expand Down
54 changes: 17 additions & 37 deletions Library/Homebrew/dependencies_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,14 @@
# @api private
module DependenciesHelpers
def args_includes_ignores(args)
includes = []
ignores = []

if args.include_build?
includes << "build?"
else
ignores << "build?"
end

if args.include_test?
includes << "test?"
else
ignores << "test?"
end
includes = [:required?, :recommended?] # included by default
includes << :build? if args.include_build?
includes << :test? if args.include_test?
includes << :optional? if args.include_optional?

if args.include_optional?
includes << "optional?"
else
ignores << "optional?"
end

ignores << "recommended?" if args.skip_recommended?
ignores << "satisfied?" if args.missing?
ignores = []
ignores << :recommended? if args.skip_recommended?
ignores << :satisfied? if args.missing?

[includes, ignores]
end
Expand All @@ -41,17 +26,12 @@ def recursive_includes(klass, root_dependent, includes, ignores)
cache_key = "recursive_includes_#{includes}_#{ignores}"

klass.expand(root_dependent, cache_key: cache_key) do |dependent, dep|
if dep.recommended?
klass.prune if ignores.include?("recommended?") || dependent.build.without?(dep)
elsif dep.optional?
klass.prune if includes.exclude?("optional?") && !dependent.build.with?(dep)
elsif dep.build? || dep.test?
keep = false
keep ||= dep.test? && includes.include?("test?") && dependent == root_dependent
keep ||= dep.build? && includes.include?("build?")
klass.prune unless keep
elsif dep.satisfied?
klass.prune if ignores.include?("satisfied?")
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

dep.public_send(include)
end

# If a tap isn't installed, we can't find the dependencies of one of
Expand All @@ -60,11 +40,11 @@ def recursive_includes(klass, root_dependent, includes, ignores)
end
end

def reject_ignores(dependables, ignores, includes)
dependables.reject do |dep|
next false unless ignores.any? { |ignore| dep.send(ignore) }
def select_includes(dependables, ignores, includes)
dependables.select do |dep|
next false if ignores.any? { |ignore| dep.public_send(ignore) }

includes.none? { |include| dep.send(include) }
includes.any? { |include| dep.public_send(include) }
end
end

Expand Down
24 changes: 21 additions & 3 deletions Library/Homebrew/test/cmd/deps_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,34 @@
it_behaves_like "parseable arguments"

it "outputs all of a Formula's dependencies and their dependencies on separate lines", :integration_test do
setup_test_formula "foo"
# Included in output
setup_test_formula "bar"
setup_test_formula "foo"
setup_test_formula "test"

# Excluded from output
setup_test_formula "baz", <<~RUBY
url "https://brew.sh/baz-1.0"
depends_on "bar"
depends_on "build" => :build
depends_on "test" => :test
depends_on "optional" => :optional
depends_on "recommended_test" => [:recommended, :test]
depends_on "installed"
RUBY
setup_test_formula "build"
setup_test_formula "optional"
setup_test_formula "recommended_test"
setup_test_formula "installed"

# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory
keg_dir = HOMEBREW_CELLAR/"installed"/"1.0"
keg_dir.mkpath
touch keg_dir/Tab::FILENAME

expect { brew "deps", "baz" }
expect { brew "deps", "baz", "--include-test", "--missing", "--skip-recommended" }
.to be_a_success
.and output("bar\nfoo\n").to_stdout
.and output("bar\nfoo\ntest\n").to_stdout
.and not_to_output.to_stderr
end
end
34 changes: 28 additions & 6 deletions Library/Homebrew/test/cmd/uses_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,37 @@
it_behaves_like "parseable arguments"

it "prints the Formulae a given Formula is used by", :integration_test do
setup_test_formula "foo"
# Included in output
setup_test_formula "bar"
setup_test_formula "baz", <<~RUBY
url "https://brew.sh/baz-1.0"
depends_on "bar"
setup_test_formula "optional", <<~RUBY
url "https://brew.sh/optional-1.0"
depends_on "bar" => :optional
RUBY

# Excluded from output
setup_test_formula "foo"
setup_test_formula "test", <<~RUBY
url "https://brew.sh/test-1.0"
depends_on "foo" => :test
RUBY
setup_test_formula "build", <<~RUBY
url "https://brew.sh/build-1.0"
depends_on "foo" => :build
RUBY
setup_test_formula "installed", <<~RUBY
url "https://brew.sh/installed-1.0"
depends_on "foo"
RUBY

# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory
%w[foo installed].each do |formula_name|
keg_dir = HOMEBREW_CELLAR/formula_name/"1.0"
keg_dir.mkpath
touch keg_dir/Tab::FILENAME
end

expect { brew "uses", "--eval-all", "--recursive", "foo" }
.to output(/(bar\nbaz|baz\nbar)/).to_stdout
expect { brew "uses", "foo", "--eval-all", "--include-optional", "--missing", "--recursive" }
.to output(/^(bar\noptional|optional\nbar)$/).to_stdout
.and not_to_output.to_stderr
.and be_a_success
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,6 @@ def install
# something here
RUBY
when "foo", "gnupg"
content = <<~RUBY
url "https://brew.sh/#{name}-1.0"
RUBY
when "bar"
content = <<~RUBY
url "https://brew.sh/#{name}-1.0"
Expand All @@ -180,6 +176,10 @@ def install
url "https://brew.sh/#patchelf-1.0"
license "0BSD"
RUBY
else
content ||= <<~RUBY
url "https://brew.sh/#{name}-1.0"
RUBY
end

Formulary.core_path(name).tap do |formula_path|
Expand Down
8 changes: 4 additions & 4 deletions completions/fish/brew.fish
Original file line number Diff line number Diff line change
Expand Up @@ -1700,14 +1700,14 @@ __fish_brew_complete_arg 'uses' -l debug -d 'Display any debugging information'
__fish_brew_complete_arg 'uses' -l eval-all -d 'Evaluate all available formulae and casks, whether installed or not, to show their dependents'
__fish_brew_complete_arg 'uses' -l formula -d 'Include only formulae'
__fish_brew_complete_arg 'uses' -l help -d 'Show this message'
__fish_brew_complete_arg 'uses' -l include-build -d 'Include all formulae that specify formula as `:build` type dependency'
__fish_brew_complete_arg 'uses' -l include-optional -d 'Include all formulae that specify formula as `:optional` type dependency'
__fish_brew_complete_arg 'uses' -l include-test -d 'Include all formulae that specify formula as `:test` type dependency'
__fish_brew_complete_arg 'uses' -l include-build -d 'Include formulae that specify formula as a `:build` dependency'
__fish_brew_complete_arg 'uses' -l include-optional -d 'Include formulae that specify formula as an `:optional` dependency'
__fish_brew_complete_arg 'uses' -l include-test -d 'Include formulae that specify formula as a `:test` dependency'
__fish_brew_complete_arg 'uses' -l installed -d 'Only list formulae and casks that are currently installed'
__fish_brew_complete_arg 'uses' -l missing -d 'Only list formulae and casks that are not currently installed'
__fish_brew_complete_arg 'uses' -l quiet -d 'Make some output more quiet'
__fish_brew_complete_arg 'uses' -l recursive -d 'Resolve more than one level of dependencies'
__fish_brew_complete_arg 'uses' -l skip-recommended -d 'Skip all formulae that specify formula as `:recommended` type dependency'
__fish_brew_complete_arg 'uses' -l skip-recommended -d 'Skip all formulae that specify formula as a `:recommended` dependency'
__fish_brew_complete_arg 'uses' -l verbose -d 'Make some output more verbose'
__fish_brew_complete_arg 'uses; and not __fish_seen_argument -l cask -l casks' -a '(__fish_brew_suggest_formulae_all)'

Expand Down
8 changes: 4 additions & 4 deletions completions/zsh/_brew
Original file line number Diff line number Diff line change
Expand Up @@ -2092,14 +2092,14 @@ _brew_uses() {
'--debug[Display any debugging information]' \
'--eval-all[Evaluate all available formulae and casks, whether installed or not, to show their dependents]' \
'--help[Show this message]' \
'--include-build[Include all formulae that specify formula as `:build` type dependency]' \
'--include-optional[Include all formulae that specify formula as `:optional` type dependency]' \
'--include-test[Include all formulae that specify formula as `:test` type dependency]' \
'--include-build[Include formulae that specify formula as a `:build` dependency]' \
'--include-optional[Include formulae that specify formula as an `:optional` dependency]' \
'--include-test[Include formulae that specify formula as a `:test` dependency]' \
'(--all --missing)--installed[Only list formulae and casks that are currently installed]' \
'(--installed)--missing[Only list formulae and casks that are not currently installed]' \
'--quiet[Make some output more quiet]' \
'--recursive[Resolve more than one level of dependencies]' \
'--skip-recommended[Skip all formulae that specify formula as `:recommended` type dependency]' \
'--skip-recommended[Skip all formulae that specify formula as a `:recommended` dependency]' \
'--verbose[Make some output more verbose]' \
- formula \
'(--cask)--formula[Include only formulae]' \
Expand Down
15 changes: 10 additions & 5 deletions docs/Manpage.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ a bug report, you will be required to provide this information.

Show dependencies for *`formula`*. Additional options specific to *`formula`*
may be appended to the command. When given multiple formula arguments,
show the intersection of dependencies for each formula.
show the intersection of dependencies for each formula. By default, `deps`
shows all required and recommended dependencies.

Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.

* `-n`, `--topological`:
Sort dependencies in topological order.
Expand Down Expand Up @@ -819,6 +822,8 @@ of *`formula`*. When given multiple formula arguments, show the intersection
of formulae that use *`formula`*. By default, `uses` shows all formulae and casks that
specify *`formula`* as a required or recommended dependency for their stable builds.

Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.

* `--recursive`:
Resolve more than one level of dependencies.
* `--installed`:
Expand All @@ -828,13 +833,13 @@ specify *`formula`* as a required or recommended dependency for their stable bui
* `--eval-all`:
Evaluate all available formulae and casks, whether installed or not, to show their dependents.
* `--include-build`:
Include all formulae that specify *`formula`* as `:build` type dependency.
Include formulae that specify *`formula`* as a `:build` dependency.
* `--include-test`:
Include all formulae that specify *`formula`* as `:test` type dependency.
Include formulae that specify *`formula`* as a `:test` dependency.
* `--include-optional`:
Include all formulae that specify *`formula`* as `:optional` type dependency.
Include formulae that specify *`formula`* as an `:optional` dependency.
* `--skip-recommended`:
Skip all formulae that specify *`formula`* as `:recommended` type dependency.
Skip all formulae that specify *`formula`* as a `:recommended` dependency.
* `--formula`:
Include only formulae.
* `--cask`:
Expand Down
Loading

0 comments on commit 851df26

Please sign in to comment.