Skip to content

Commit f510630

Browse files
committed
Add support for optional dependencies
1 parent d429ddf commit f510630

File tree

7 files changed

+129
-16
lines changed

7 files changed

+129
-16
lines changed

lib/molinillo/delegates/specification_provider.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ def allow_missing?(dependency)
6060
end
6161
end
6262

63+
# (see Molinillo::SpecificationProvider#optional_dependency?)
64+
def optional_dependency?(dependency)
65+
with_no_such_dependency_error_handling do
66+
specification_provider.optional_dependency?(dependency)
67+
end
68+
end
69+
6370
private
6471

6572
# Ensures any raised {NoSuchDependencyError} has its

lib/molinillo/modules/specification_provider.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,18 @@ def sort_dependencies(dependencies, activated, conflicts)
9696
def allow_missing?(dependency)
9797
false
9898
end
99+
100+
# Returns whether this dependency is considered optional.
101+
#
102+
# If the only dependencies for a specification name are optional, that
103+
# specification will not be activated. If there is a strong dependency as
104+
# well, than the requirements imposed by optional dependencies will be taken
105+
# into account.
106+
#
107+
# @param [Object] dependency
108+
# @return [Boolean] whether this dependency is optional.
109+
def optional_dependency?(dependency)
110+
false
111+
end
99112
end
100113
end

lib/molinillo/resolution.rb

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def initial_state
163163
end
164164

165165
requirements = sort_dependencies(original_requested, graph, {})
166+
requirements.reject! { |r| optional_dependency?(r) }
166167
initial_requirement = requirements.shift
167168
DependencyState.new(
168169
initial_requirement && name_for(initial_requirement),
@@ -382,13 +383,26 @@ def attempt_to_activate_new_spec
382383
# @return [Boolean] whether the current spec is satisfied as a new
383384
# possibility.
384385
def new_spec_satisfied?
386+
unless requirement_satisfied_by?(requirement, activated, possibility)
387+
debug(depth) { 'Requirement unsatisfied by requested spec' }
388+
return false
389+
end
390+
391+
vertex = activated.vertex_named(name)
392+
vertex.requirements.each do |d|
393+
next unless optional_dependency?(d) # TODO: investigate getting rid of this
394+
next if requirement_satisfied_by?(d, activated, possibility)
395+
debug(depth) { "Vertex requirement #{d} unsatisfied by the possibility" }
396+
return false
397+
end
398+
385399
locked_requirement = locked_requirement_named(name)
386-
requested_spec_satisfied = requirement_satisfied_by?(requirement, activated, possibility)
387-
locked_spec_satisfied = !locked_requirement ||
388-
requirement_satisfied_by?(locked_requirement, activated, possibility)
389-
debug(depth) { 'Unsatisfied by requested spec' } unless requested_spec_satisfied
390-
debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied
391-
requested_spec_satisfied && locked_spec_satisfied
400+
if locked_requirement && !requirement_satisfied_by?(locked_requirement, activated, possibility)
401+
debug(depth) { "Possibility unsatisfied by locked spec #{locked_requirement}" }
402+
return false
403+
end
404+
405+
true
392406
end
393407

394408
# @param [String] requirement_name the spec name to search for
@@ -416,9 +430,14 @@ def activate_spec
416430
def require_nested_dependencies_for(activated_spec)
417431
nested_dependencies = dependencies_for(activated_spec)
418432
debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
419-
nested_dependencies.each do |d|
420-
activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d)
433+
nested_dependencies = nested_dependencies.reject do |d|
434+
name = name_for(d)
435+
activated.add_child_vertex(name, nil, [name_for(activated_spec)], d)
421436
@parent_of[d] = requirement
437+
next false unless optional_dependency?(d)
438+
next true unless existing = activated.vertex_named(name).payload
439+
next true if requirement_satisfied_by?(d, activated, existing)
440+
false
422441
end
423442

424443
push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?)

spec/dependency_graph_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module Molinillo
5757
@graph.detach_vertex_named(root.name)
5858
expect(@graph.vertex_named(root.name)).to be_nil
5959
expect(@graph.vertex_named(child.name)).to eq(child)
60-
expect(child.predecessors).to eq([root2])
60+
expect(child.predecessors).to contain_exactly(root2)
6161
expect(@graph.vertices.count).to eq(2)
6262
end
6363

spec/resolver_spec.rb

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def initialize(fixture_path)
1919
File.open(fixture_path) do |fixture|
2020
JSON.load(fixture).tap do |test_case|
2121
self.name = test_case['name']
22-
self.index = TestIndex.new(test_case['index'] || 'awesome')
22+
self.index = TestIndex.from_fixture(test_case['index'] || 'awesome')
2323
self.requested = test_case['requested'].map do |(name, reqs)|
2424
VersionKit::Dependency.new name, reqs.split(',').map(&:chomp)
2525
end
@@ -89,8 +89,9 @@ def initialize(fixture_path)
8989
end
9090

9191
describe 'in general' do
92+
let(:index) { TestIndex.from_fixture('awesome') }
9293
before do
93-
@resolver = described_class.new(TestIndex.new('awesome'), TestUI.new)
94+
@resolver = described_class.new(index, TestUI.new)
9495
end
9596

9697
it 'can resolve a list of 0 requirements' do
@@ -121,6 +122,61 @@ def initialize(fixture_path)
121122
expect(resolved.map(&:payload).map(&:to_s)).to eq(['actionpack (1.2.3)'])
122123
end
123124

125+
describe 'optional dependencies' do
126+
let(:index) do
127+
TestIndex.new(
128+
'no_deps' => [
129+
TestSpecification.new('name' => 'no_deps', 'version' => '1.0'),
130+
TestSpecification.new('name' => 'no_deps', 'version' => '2.0'),
131+
TestSpecification.new('name' => 'no_deps', 'version' => '3.0'),
132+
],
133+
'strong_dep' => [
134+
TestSpecification.new('name' => 'strong_dep', 'version' => '1.0',
135+
'dependencies' => { 'no_deps' => '< 3' }),
136+
],
137+
'weak_dep' => [
138+
TestSpecification.new('name' => 'weak_dep', 'version' => '1.0',
139+
'dependencies' => { 'no_deps' => '< 2 optional' }),
140+
]
141+
)
142+
end
143+
144+
let(:no_deps) { VersionKit::Dependency.new('no_deps', '> 1') }
145+
let(:weak_dep) { VersionKit::Dependency.new('weak_dep', '>= 0') }
146+
let(:strong_dep) { VersionKit::Dependency.new('strong_dep', '>= 0') }
147+
148+
it 'ignores optional explicit dependencies' do
149+
no_deps.optional = true
150+
expect(@resolver.resolve([no_deps], DependencyGraph.new).map(&:payload).compact).to be_empty
151+
end
152+
153+
it 'ignores nested optional dependencies' do
154+
expect(@resolver.resolve([weak_dep], DependencyGraph.new).map(&:payload).compact.map(&:to_s)).to eq [
155+
'weak_dep (1.0.0)',
156+
'no_deps (1.0.0)',
157+
]
158+
end
159+
160+
it 'uses nested optional dependencies' do
161+
expect(@resolver.resolve([weak_dep, strong_dep], DependencyGraph.new).map(&:payload).compact.map(&:to_s)).
162+
to eq [
163+
'weak_dep (1.0.0)',
164+
'strong_dep (1.0.0)',
165+
'no_deps (1.0.0)',
166+
]
167+
end
168+
169+
it 'raises when an optional dependency conflicts' do
170+
resolve = proc { @resolver.resolve([weak_dep, no_deps], DependencyGraph.new) }
171+
expect(&resolve).to raise_error(Molinillo::VersionConflict, <<-EOS.strip)
172+
Unable to satisfy the following requirements:
173+
174+
- `no_deps (> 1.0.0)` required by `user-specified dependency`
175+
- `no_deps (< 2.0.0)` required by `weak_dep (1.0.0)`
176+
EOS
177+
end
178+
end
179+
124180
# Regression test. See: https://github.com/CocoaPods/Molinillo/pull/38
125181
it 'can resolve when swapping children with successors' do
126182
swap_child_with_successors_index = Class.new(TestIndex) do
@@ -144,7 +200,7 @@ def versions_of(dependency_name)
144200
end
145201
end
146202

147-
index = swap_child_with_successors_index.new('swap_child_with_successors')
203+
index = swap_child_with_successors_index.from_fixture('swap_child_with_successors')
148204
@resolver = described_class.new(index, TestUI.new)
149205
demands = [
150206
VersionKit::Dependency.new('build-essential', '>= 0.0.0'),

spec/spec_helper/index.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ class TestIndex
44
attr_accessor :specs
55
include SpecificationProvider
66

7-
def initialize(fixture_name)
7+
def self.from_fixture(fixture_name)
88
File.open(FIXTURE_INDEX_DIR + (fixture_name + '.json'), 'r') do |fixture|
9-
self.specs = JSON.load(fixture).reduce(Hash.new([])) do |specs_by_name, (name, versions)|
9+
sorted_specs = JSON.load(fixture).reduce(Hash.new([])) do |specs_by_name, (name, versions)|
1010
specs_by_name.tap do |specs|
1111
specs[name] = versions.map { |s| TestSpecification.new s }.sort_by(&:version)
1212
end
1313
end
14+
return new(sorted_specs)
1415
end
1516
end
1617

18+
def initialize(specs_by_name)
19+
self.specs = specs_by_name
20+
end
21+
1722
def requirement_satisfied_by?(requirement, _activated, spec)
1823
case requirement
1924
when TestSpecification
@@ -53,6 +58,10 @@ def sort_dependencies(dependencies, activated, conflicts)
5358
end
5459
end
5560

61+
def optional_dependency?(dependency)
62+
dependency.optional?
63+
end
64+
5665
private
5766

5867
def dependency_pre_release?(dependency)

spec/spec_helper/specification.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
# frozen_string_literal: true
2+
module VersionKit
3+
class Dependency
4+
attr_accessor :optional
5+
alias optional? optional
6+
end
7+
end
8+
29
module Molinillo
310
class TestSpecification
411
attr_accessor :name, :version, :dependencies
512
def initialize(hash)
613
self.name = hash['name']
714
self.version = VersionKit::Version.new(hash['version'])
8-
self.dependencies = hash['dependencies'].map do |(name, requirement)|
9-
VersionKit::Dependency.new(name, requirement.split(',').map(&:chomp))
15+
self.dependencies = hash.fetch('dependencies') { Hash.new }.map do |(name, requirement)|
16+
requirements = requirement.split(',').map(&:chomp)
17+
optional = requirements.delete('optional')
18+
VersionKit::Dependency.new(name, requirements).tap { |d| d.optional = optional }
1019
end
1120
end
1221

0 commit comments

Comments
 (0)