Merge pull request #15892 from apainintheneck/rework-recursive-dependency-resolution

dependency_helpers: rework recursive dependency resolution
This commit is contained in:
Mike McQuaid 2023-08-30 08:29:24 +01:00 committed by GitHub
commit 851df262a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 79 deletions

View File

@ -15,7 +15,10 @@ module Homebrew
description <<~EOS description <<~EOS
Show dependencies for <formula>. Additional options specific to <formula> Show dependencies for <formula>. Additional options specific to <formula>
may be appended to the command. When given multiple formula arguments, 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 EOS
switch "-n", "--topological", switch "-n", "--topological",
description: "Sort dependencies in topological order." description: "Sort dependencies in topological order."
@ -90,7 +93,8 @@ module Homebrew
!args.include_build? && !args.include_build? &&
!args.include_test? && !args.include_test? &&
!args.include_optional? && !args.include_optional? &&
!args.skip_recommended? !args.skip_recommended? &&
!args.missing?
if args.tree? || args.graph? if args.tree? || args.graph?
dependents = if args.named.present? dependents = if args.named.present?
@ -197,8 +201,8 @@ module Homebrew
deps ||= recursive_includes(Dependency, dependency, includes, ignores) deps ||= recursive_includes(Dependency, dependency, includes, ignores)
reqs = recursive_includes(Requirement, dependency, includes, ignores) reqs = recursive_includes(Requirement, dependency, includes, ignores)
else else
deps ||= reject_ignores(dependency.deps, ignores, includes) deps ||= select_includes(dependency.deps, ignores, includes)
reqs = reject_ignores(dependency.requirements, ignores, includes) reqs = select_includes(dependency.requirements, ignores, includes)
end end
deps + reqs.to_a deps + reqs.to_a
@ -269,8 +273,8 @@ module Homebrew
def self.dependables(formula, args:) def self.dependables(formula, args:)
includes, ignores = args_includes_ignores(args) includes, ignores = args_includes_ignores(args)
deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps
deps = reject_ignores(deps, ignores, includes) deps = select_includes(deps, ignores, includes)
reqs = reject_ignores(formula.requirements, ignores, includes) if args.include_requirements? reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements?
reqs ||= [] reqs ||= []
reqs + deps reqs + deps
end end

View File

@ -22,6 +22,8 @@ module Homebrew
of <formula>. When given multiple formula arguments, show the intersection of <formula>. When given multiple formula arguments, show the intersection
of formulae that use <formula>. By default, `uses` shows all formulae and casks that 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. specify <formula> as a required or recommended dependency for their stable builds.
Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.
EOS EOS
switch "--recursive", switch "--recursive",
description: "Resolve more than one level of dependencies." description: "Resolve more than one level of dependencies."
@ -33,13 +35,13 @@ module Homebrew
description: "Evaluate all available formulae and casks, whether installed or not, to show " \ description: "Evaluate all available formulae and casks, whether installed or not, to show " \
"their dependents." "their dependents."
switch "--include-build", 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", 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", 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", 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", switch "--formula", "--formulae",
description: "Include only formulae." description: "Include only formulae."
switch "--cask", "--casks", switch "--cask", "--casks",
@ -120,6 +122,18 @@ module Homebrew
deps += args.installed? ? Cask::Caskroom.casks : Cask::Cask.all deps += args.installed? ? Cask::Caskroom.casks : Cask::Cask.all
end 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) select_used_dependents(dependents(deps), used_formulae, recursive, includes, ignores)
end end
end end
@ -129,7 +143,7 @@ module Homebrew
deps = if recursive deps = if recursive
recursive_includes(Dependency, d, includes, ignores) recursive_includes(Dependency, d, includes, ignores)
else else
reject_ignores(d.deps, ignores, includes) select_includes(d.deps, ignores, includes)
end end
used_formulae.all? do |ff| used_formulae.all? do |ff|

View File

@ -8,29 +8,14 @@ require "cask_dependent"
# @api private # @api private
module DependenciesHelpers module DependenciesHelpers
def args_includes_ignores(args) def args_includes_ignores(args)
includes = [] includes = [:required?, :recommended?] # included by default
includes << :build? if args.include_build?
includes << :test? if args.include_test?
includes << :optional? if args.include_optional?
ignores = [] ignores = []
ignores << :recommended? if args.skip_recommended?
if args.include_build? ignores << :satisfied? if args.missing?
includes << "build?"
else
ignores << "build?"
end
if args.include_test?
includes << "test?"
else
ignores << "test?"
end
if args.include_optional?
includes << "optional?"
else
ignores << "optional?"
end
ignores << "recommended?" if args.skip_recommended?
ignores << "satisfied?" if args.missing?
[includes, ignores] [includes, ignores]
end end
@ -41,17 +26,12 @@ module DependenciesHelpers
cache_key = "recursive_includes_#{includes}_#{ignores}" cache_key = "recursive_includes_#{includes}_#{ignores}"
klass.expand(root_dependent, cache_key: cache_key) do |dependent, dep| klass.expand(root_dependent, cache_key: cache_key) do |dependent, dep|
if dep.recommended? klass.prune if ignores.any? { |ignore| dep.public_send(ignore) }
klass.prune if ignores.include?("recommended?") || dependent.build.without?(dep) klass.prune if includes.none? do |include|
elsif dep.optional? # Ignore indirect test dependencies
klass.prune if includes.exclude?("optional?") && !dependent.build.with?(dep) next if include == :test? && dependent != root_dependent
elsif dep.build? || dep.test?
keep = false dep.public_send(include)
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?")
end end
# If a tap isn't installed, we can't find the dependencies of one of # If a tap isn't installed, we can't find the dependencies of one of
@ -60,11 +40,11 @@ module DependenciesHelpers
end end
end end
def reject_ignores(dependables, ignores, includes) def select_includes(dependables, ignores, includes)
dependables.reject do |dep| dependables.select do |dep|
next false unless ignores.any? { |ignore| dep.send(ignore) } 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
end end

View File

@ -6,16 +6,34 @@ describe "brew deps" do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "outputs all of a Formula's dependencies and their dependencies on separate lines", :integration_test do 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 "bar"
setup_test_formula "foo"
setup_test_formula "test"
# Excluded from output
setup_test_formula "baz", <<~RUBY setup_test_formula "baz", <<~RUBY
url "https://brew.sh/baz-1.0" url "https://brew.sh/baz-1.0"
depends_on "bar" 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 RUBY
setup_test_formula "build"
setup_test_formula "optional"
setup_test_formula "recommended_test"
setup_test_formula "installed"
expect { brew "deps", "baz" } # 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", "--include-test", "--missing", "--skip-recommended" }
.to be_a_success .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 .and not_to_output.to_stderr
end end
end end

View File

@ -6,15 +6,37 @@ describe "brew uses" do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
it "prints the Formulae a given Formula is used by", :integration_test do 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 "bar"
setup_test_formula "baz", <<~RUBY setup_test_formula "optional", <<~RUBY
url "https://brew.sh/baz-1.0" url "https://brew.sh/optional-1.0"
depends_on "bar" depends_on "bar" => :optional
RUBY RUBY
expect { brew "uses", "--eval-all", "--recursive", "foo" } # Excluded from output
.to output(/(bar\nbaz|baz\nbar)/).to_stdout 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", "foo", "--eval-all", "--include-optional", "--missing", "--recursive" }
.to output(/^(bar\noptional|optional\nbar)$/).to_stdout
.and not_to_output.to_stderr .and not_to_output.to_stderr
.and be_a_success .and be_a_success
end end

View File

@ -166,10 +166,6 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
# something here # something here
RUBY RUBY
when "foo", "gnupg"
content = <<~RUBY
url "https://brew.sh/#{name}-1.0"
RUBY
when "bar" when "bar"
content = <<~RUBY content = <<~RUBY
url "https://brew.sh/#{name}-1.0" url "https://brew.sh/#{name}-1.0"
@ -180,6 +176,10 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
url "https://brew.sh/#patchelf-1.0" url "https://brew.sh/#patchelf-1.0"
license "0BSD" license "0BSD"
RUBY RUBY
else
content ||= <<~RUBY
url "https://brew.sh/#{name}-1.0"
RUBY
end end
Formulary.core_path(name).tap do |formula_path| Formulary.core_path(name).tap do |formula_path|

View File

@ -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 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 formula -d 'Include only formulae'
__fish_brew_complete_arg 'uses' -l help -d 'Show this message' __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-build -d 'Include formulae that specify formula as a `:build` 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-optional -d 'Include formulae that specify formula as an `:optional` 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-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 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 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 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 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' -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)' __fish_brew_complete_arg 'uses; and not __fish_seen_argument -l cask -l casks' -a '(__fish_brew_suggest_formulae_all)'

View File

@ -2092,14 +2092,14 @@ _brew_uses() {
'--debug[Display any debugging information]' \ '--debug[Display any debugging information]' \
'--eval-all[Evaluate all available formulae and casks, whether installed or not, to show their dependents]' \ '--eval-all[Evaluate all available formulae and casks, whether installed or not, to show their dependents]' \
'--help[Show this message]' \ '--help[Show this message]' \
'--include-build[Include all formulae that specify formula as `:build` type dependency]' \ '--include-build[Include formulae that specify formula as a `:build` dependency]' \
'--include-optional[Include all formulae that specify formula as `:optional` type dependency]' \ '--include-optional[Include formulae that specify formula as an `:optional` dependency]' \
'--include-test[Include all formulae that specify formula as `:test` type dependency]' \ '--include-test[Include formulae that specify formula as a `:test` dependency]' \
'(--all --missing)--installed[Only list formulae and casks that are currently installed]' \ '(--all --missing)--installed[Only list formulae and casks that are currently installed]' \
'(--installed)--missing[Only list formulae and casks that are not currently installed]' \ '(--installed)--missing[Only list formulae and casks that are not currently installed]' \
'--quiet[Make some output more quiet]' \ '--quiet[Make some output more quiet]' \
'--recursive[Resolve more than one level of dependencies]' \ '--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]' \ '--verbose[Make some output more verbose]' \
- formula \ - formula \
'(--cask)--formula[Include only formulae]' \ '(--cask)--formula[Include only formulae]' \

View File

@ -139,7 +139,10 @@ a bug report, you will be required to provide this information.
Show dependencies for *`formula`*. Additional options specific to *`formula`* Show dependencies for *`formula`*. Additional options specific to *`formula`*
may be appended to the command. When given multiple formula arguments, 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`: * `-n`, `--topological`:
Sort dependencies in topological order. Sort dependencies in topological order.
@ -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 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. specify *`formula`* as a required or recommended dependency for their stable builds.
Note: `--missing` and `--skip-recommended` have precedence over `--include-*`.
* `--recursive`: * `--recursive`:
Resolve more than one level of dependencies. Resolve more than one level of dependencies.
* `--installed`: * `--installed`:
@ -828,13 +833,13 @@ specify *`formula`* as a required or recommended dependency for their stable bui
* `--eval-all`: * `--eval-all`:
Evaluate all available formulae and casks, whether installed or not, to show their dependents. Evaluate all available formulae and casks, whether installed or not, to show their dependents.
* `--include-build`: * `--include-build`:
Include all formulae that specify *`formula`* as `:build` type dependency. Include formulae that specify *`formula`* as a `:build` dependency.
* `--include-test`: * `--include-test`:
Include all formulae that specify *`formula`* as `:test` type dependency. Include formulae that specify *`formula`* as a `:test` dependency.
* `--include-optional`: * `--include-optional`:
Include all formulae that specify *`formula`* as `:optional` type dependency. Include formulae that specify *`formula`* as an `:optional` dependency.
* `--skip-recommended`: * `--skip-recommended`:
Skip all formulae that specify *`formula`* as `:recommended` type dependency. Skip all formulae that specify *`formula`* as a `:recommended` dependency.
* `--formula`: * `--formula`:
Include only formulae. Include only formulae.
* `--cask`: * `--cask`:

View File

@ -155,7 +155,10 @@ Control whether Homebrew automatically links external tap shell completion files
Show Homebrew and system configuration info useful for debugging\. If you file a bug report, you will be required to provide this information\. Show Homebrew and system configuration info useful for debugging\. If you file a bug report, you will be required to provide this information\.
. .
.SS "\fBdeps\fR [\fIoptions\fR] [\fIformula\fR|\fIcask\fR \.\.\.]" .SS "\fBdeps\fR [\fIoptions\fR] [\fIformula\fR|\fIcask\fR \.\.\.]"
Show dependencies for \fIformula\fR\. Additional options specific to \fIformula\fR may be appended to the command\. When given multiple formula arguments, show the intersection of dependencies for each formula\. Show dependencies for \fIformula\fR\. Additional options specific to \fIformula\fR may be appended to the command\. When given multiple formula arguments, show the intersection of dependencies for each formula\. By default, \fBdeps\fR shows all required and recommended dependencies\.
.
.P
Note: \fB\-\-missing\fR and \fB\-\-skip\-recommended\fR have precedence over \fB\-\-include\-*\fR\.
. .
.TP .TP
\fB\-n\fR, \fB\-\-topological\fR \fB\-n\fR, \fB\-\-topological\fR
@ -1149,6 +1152,9 @@ Disable/enable quarantining of downloads (default: enabled)\.
.SS "\fBuses\fR [\fIoptions\fR] \fIformula\fR [\.\.\.]" .SS "\fBuses\fR [\fIoptions\fR] \fIformula\fR [\.\.\.]"
Show formulae and casks that specify \fIformula\fR as a dependency; that is, show dependents of \fIformula\fR\. When given multiple formula arguments, show the intersection of formulae that use \fIformula\fR\. By default, \fBuses\fR shows all formulae and casks that specify \fIformula\fR as a required or recommended dependency for their stable builds\. Show formulae and casks that specify \fIformula\fR as a dependency; that is, show dependents of \fIformula\fR\. When given multiple formula arguments, show the intersection of formulae that use \fIformula\fR\. By default, \fBuses\fR shows all formulae and casks that specify \fIformula\fR as a required or recommended dependency for their stable builds\.
. .
.P
Note: \fB\-\-missing\fR and \fB\-\-skip\-recommended\fR have precedence over \fB\-\-include\-*\fR\.
.
.TP .TP
\fB\-\-recursive\fR \fB\-\-recursive\fR
Resolve more than one level of dependencies\. Resolve more than one level of dependencies\.
@ -1167,19 +1173,19 @@ Evaluate all available formulae and casks, whether installed or not, to show the
. .
.TP .TP
\fB\-\-include\-build\fR \fB\-\-include\-build\fR
Include all formulae that specify \fIformula\fR as \fB:build\fR type dependency\. Include formulae that specify \fIformula\fR as a \fB:build\fR dependency\.
. .
.TP .TP
\fB\-\-include\-test\fR \fB\-\-include\-test\fR
Include all formulae that specify \fIformula\fR as \fB:test\fR type dependency\. Include formulae that specify \fIformula\fR as a \fB:test\fR dependency\.
. .
.TP .TP
\fB\-\-include\-optional\fR \fB\-\-include\-optional\fR
Include all formulae that specify \fIformula\fR as \fB:optional\fR type dependency\. Include formulae that specify \fIformula\fR as an \fB:optional\fR dependency\.
. .
.TP .TP
\fB\-\-skip\-recommended\fR \fB\-\-skip\-recommended\fR
Skip all formulae that specify \fIformula\fR as \fB:recommended\fR type dependency\. Skip all formulae that specify \fIformula\fR as a \fB:recommended\fR dependency\.
. .
.TP .TP
\fB\-\-formula\fR \fB\-\-formula\fR