molinillo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +9 -0
- data/README.md +45 -0
- data/lib/molinillo.rb +5 -0
- data/lib/molinillo/dependency_graph.rb +243 -0
- data/lib/molinillo/errors.rb +69 -0
- data/lib/molinillo/gem_metadata.rb +3 -0
- data/lib/molinillo/modules/specification_provider.rb +90 -0
- data/lib/molinillo/modules/ui.rb +63 -0
- data/lib/molinillo/resolution.rb +323 -0
- data/lib/molinillo/resolver.rb +43 -0
- data/lib/molinillo/state.rb +43 -0
- data/spec/dependency_graph_spec.rb +79 -0
- data/spec/resolver_spec.rb +113 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/spec_helper/index.rb +59 -0
- data/spec/spec_helper/specification.rb +22 -0
- data/spec/spec_helper/ui.rb +9 -0
- data/spec/state_spec.rb +33 -0
- metadata +98 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Molinillo
|
2
|
+
# Conveys information about the resolution process to a user.
|
3
|
+
module UI
|
4
|
+
# The {IO} object that should be used to print output. `STDOUT`, by default.
|
5
|
+
#
|
6
|
+
# @return [IO]
|
7
|
+
def output
|
8
|
+
STDOUT
|
9
|
+
end
|
10
|
+
|
11
|
+
# Called roughly every {#progress_rate}, this method should convey progress
|
12
|
+
# to the user.
|
13
|
+
#
|
14
|
+
# @return [void]
|
15
|
+
def indicate_progress
|
16
|
+
output.print '.' unless debug?
|
17
|
+
end
|
18
|
+
|
19
|
+
# How often progress should be conveyed to the user via
|
20
|
+
# {#indicate_progress}, in seconds. A third of a second, by default.
|
21
|
+
#
|
22
|
+
# @return [Float]
|
23
|
+
def progress_rate
|
24
|
+
0.33
|
25
|
+
end
|
26
|
+
|
27
|
+
# Called before resolution begins.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def before_resolution
|
31
|
+
output.print 'Resolving dependencies...'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Called after resolution ends (either successfully or with an error).
|
35
|
+
# By default, prints a newline.
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def after_resolution
|
39
|
+
output.puts
|
40
|
+
end
|
41
|
+
|
42
|
+
# Conveys debug information to the user.
|
43
|
+
# By default, prints to `STDERR` instead of {#output}.
|
44
|
+
#
|
45
|
+
# @param [Integer] depth the current depth of the resolution process.
|
46
|
+
# @return [void]
|
47
|
+
def debug(depth = 0)
|
48
|
+
if debug?
|
49
|
+
debug_info = yield
|
50
|
+
debug_info = debug_info.inspect unless debug_info.is_a?(String)
|
51
|
+
STDERR.puts debug_info.split("\n").map { |s| ' ' * depth + s }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Whether or not debug messages should be printed.
|
56
|
+
# By default, whether or not the `CP_RESOLVER` environment variable is set.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def debug?
|
60
|
+
@debug_mode ||= ENV['CP_RESOLVER']
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
module Molinillo
|
2
|
+
class Resolver
|
3
|
+
# A specific resolution from a given {Resolver}
|
4
|
+
class Resolution
|
5
|
+
# A conflict that the resolution process encountered
|
6
|
+
# @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict
|
7
|
+
# @attr [Object, nil] existing the existing spec that was in conflict with
|
8
|
+
# the {#possibility}
|
9
|
+
# @attr [Object] possibility the spec that was unable to be activated due
|
10
|
+
# to a conflict
|
11
|
+
Conflict = Struct.new(
|
12
|
+
:requirements,
|
13
|
+
:existing,
|
14
|
+
:possibility
|
15
|
+
)
|
16
|
+
|
17
|
+
# @return [SpecificationProvider] the provider that knows about
|
18
|
+
# dependencies, requirements, specifications, versions, etc.
|
19
|
+
attr_reader :specification_provider
|
20
|
+
|
21
|
+
# @return [UI] the UI that knows how to communicate feedback about the
|
22
|
+
# resolution process back to the user
|
23
|
+
attr_reader :resolver_ui
|
24
|
+
|
25
|
+
# @return [DependencyGraph] the base dependency graph to which
|
26
|
+
# dependencies should be 'locked'
|
27
|
+
attr_reader :base
|
28
|
+
|
29
|
+
# @return [Array] the dependencies that were explicitly required
|
30
|
+
attr_reader :original_requested
|
31
|
+
|
32
|
+
# @param [SpecificationProvider] specification_provider
|
33
|
+
# see {#specification_provider}
|
34
|
+
# @param [UI] resolver_ui see {#resolver_ui}
|
35
|
+
# @param [Array] requested see {#original_requested}
|
36
|
+
# @param [DependencyGraph] base see {#base}
|
37
|
+
def initialize(specification_provider, resolver_ui, requested, base)
|
38
|
+
@specification_provider = specification_provider
|
39
|
+
@resolver_ui = resolver_ui
|
40
|
+
@original_requested = requested
|
41
|
+
@base = base
|
42
|
+
@states = []
|
43
|
+
@iteration_counter = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
# Resolves the {#original_requested} dependencies into a full dependency
|
47
|
+
# graph
|
48
|
+
# @raise [ResolverError] if successful resolution is impossible
|
49
|
+
# @return [DependencyGraph] the dependency graph of successfully resolved
|
50
|
+
# dependencies
|
51
|
+
def resolve
|
52
|
+
start_resolution
|
53
|
+
|
54
|
+
while state
|
55
|
+
break unless state.requirements.any? || state.requirement
|
56
|
+
indicate_progress
|
57
|
+
if state.respond_to?(:pop_possibility_state) # DependencyState
|
58
|
+
debug(depth) { "creating possibility state (#{possibilities.count} remaining)" }
|
59
|
+
state.pop_possibility_state.tap { |s| states.push(s) if s }
|
60
|
+
end
|
61
|
+
process_topmost_state
|
62
|
+
end
|
63
|
+
|
64
|
+
activated.freeze
|
65
|
+
ensure
|
66
|
+
end_resolution
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Sets up the resolution process
|
72
|
+
# @return [void]
|
73
|
+
def start_resolution
|
74
|
+
@started_at = Time.now
|
75
|
+
|
76
|
+
states.push(initial_state)
|
77
|
+
|
78
|
+
debug { "starting resolution (#{@started_at})" }
|
79
|
+
resolver_ui.before_resolution
|
80
|
+
end
|
81
|
+
|
82
|
+
# Ends the resolution process
|
83
|
+
# @return [void]
|
84
|
+
def end_resolution
|
85
|
+
resolver_ui.after_resolution
|
86
|
+
debug do
|
87
|
+
"finished resolution (#{@iteration_counter} steps) " \
|
88
|
+
"(took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})"
|
89
|
+
end
|
90
|
+
debug { 'unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') }
|
91
|
+
debug { 'activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') }
|
92
|
+
end
|
93
|
+
|
94
|
+
require 'molinillo/state'
|
95
|
+
require 'molinillo/modules/specification_provider'
|
96
|
+
|
97
|
+
# @return [Integer] the number of resolver iterations in between calls to
|
98
|
+
# {#resolver_ui}'s {UI#indicate_progress} method
|
99
|
+
attr_accessor :iteration_rate
|
100
|
+
|
101
|
+
# @return [Time] the time at which resolution began
|
102
|
+
attr_accessor :started_at
|
103
|
+
|
104
|
+
# @return [Array<ResolutionState>] the stack of states for the resolution
|
105
|
+
attr_accessor :states
|
106
|
+
|
107
|
+
ResolutionState.new.members.each do |member|
|
108
|
+
define_method member do |*args, &block|
|
109
|
+
state.send(member, *args, &block)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
SpecificationProvider.instance_methods(false).each do |instance_method|
|
114
|
+
define_method instance_method do |*args, &block|
|
115
|
+
begin
|
116
|
+
specification_provider.send(instance_method, *args, &block)
|
117
|
+
rescue NoSuchDependencyError => error
|
118
|
+
vertex = activated.vertex_named(name_for error.dependency)
|
119
|
+
error.required_by += vertex.incoming_edges.map { |e| e.origin.name }
|
120
|
+
error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty?
|
121
|
+
raise
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Processes the topmost available {RequirementState} on the stack
|
127
|
+
# @return [void]
|
128
|
+
def process_topmost_state
|
129
|
+
if possibility
|
130
|
+
attempt_to_activate
|
131
|
+
else
|
132
|
+
create_conflict if state.is_a? PossibilityState
|
133
|
+
unwind_for_conflict until possibility && state.is_a?(DependencyState)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Object] the current possibility that the resolution is trying
|
138
|
+
# to activate
|
139
|
+
def possibility
|
140
|
+
possibilities.last
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [RequirementState] the current state the resolution is
|
144
|
+
# operating upon
|
145
|
+
def state
|
146
|
+
states.last
|
147
|
+
end
|
148
|
+
|
149
|
+
# Creates the initial state for the resolution, based upon the
|
150
|
+
# {#requested} dependencies
|
151
|
+
# @return [DependencyState] the initial state for the resolution
|
152
|
+
def initial_state
|
153
|
+
graph = DependencyGraph.new.tap do |dg|
|
154
|
+
original_requested.each { |r| dg.add_root_vertex(name_for(r), nil).tap { |v| v.explicit_requirements << r } }
|
155
|
+
end
|
156
|
+
|
157
|
+
requirements = sort_dependencies(original_requested, graph, {})
|
158
|
+
initial_requirement = requirements.shift
|
159
|
+
DependencyState.new(
|
160
|
+
initial_requirement && name_for(initial_requirement),
|
161
|
+
requirements,
|
162
|
+
graph,
|
163
|
+
initial_requirement,
|
164
|
+
initial_requirement && search_for(initial_requirement),
|
165
|
+
0,
|
166
|
+
{}
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Unwinds the states stack because a conflict has been encountered
|
171
|
+
# @return [void]
|
172
|
+
def unwind_for_conflict
|
173
|
+
if depth > 0
|
174
|
+
debug(depth) { 'Unwinding from level ' + state.depth.to_s }
|
175
|
+
conflicts.tap do |c|
|
176
|
+
states.pop
|
177
|
+
state.conflicts = c
|
178
|
+
end
|
179
|
+
else
|
180
|
+
raise VersionConflict.new(conflicts)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# @return [Conflict] a {Conflict} that reflects the failure to activate
|
185
|
+
# the {#possibility} in conjunction with the current {#state}
|
186
|
+
def create_conflict
|
187
|
+
vertex = activated.vertex_named(name)
|
188
|
+
existing = vertex.payload
|
189
|
+
requirements = {
|
190
|
+
name_for_explicit_dependency_source => vertex.explicit_requirements,
|
191
|
+
name_for_locking_dependency_source => Array(locked_requirement_named(name)),
|
192
|
+
}
|
193
|
+
vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(*edge.requirements) }
|
194
|
+
conflicts[name] = Conflict.new(
|
195
|
+
requirements,
|
196
|
+
existing,
|
197
|
+
possibility
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Indicates progress roughly once every second
|
202
|
+
# @return [void]
|
203
|
+
def indicate_progress
|
204
|
+
@iteration_counter += 1
|
205
|
+
@progress_rate ||= resolver_ui.progress_rate
|
206
|
+
if iteration_rate.nil?
|
207
|
+
if Time.now - started_at >= @progress_rate
|
208
|
+
self.iteration_rate = @iteration_counter
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
if iteration_rate && (@iteration_counter % iteration_rate) == 0
|
213
|
+
resolver_ui.indicate_progress
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Calls the {#resolver_ui}'s {UI#debug} method
|
218
|
+
# @param [Integer] depth the depth of the {#states} stack
|
219
|
+
# @param [Proc] block a block that yields a {#to_s}
|
220
|
+
# @return [void]
|
221
|
+
def debug(depth = 0, &block)
|
222
|
+
resolver_ui.debug(depth, &block)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Attempts to activate the current {#possibility}
|
226
|
+
# @return [void]
|
227
|
+
def attempt_to_activate
|
228
|
+
debug(depth) { 'attempting to activate ' + possibility.to_s }
|
229
|
+
existing_node = activated.vertex_named(name)
|
230
|
+
if existing_node && existing_node.payload
|
231
|
+
attempt_to_activate_existing_spec(existing_node)
|
232
|
+
else
|
233
|
+
attempt_to_activate_new_spec
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Attempts to activate the current {#possibility} (given that it has
|
238
|
+
# already been activated)
|
239
|
+
# @return [void]
|
240
|
+
def attempt_to_activate_existing_spec(existing_node)
|
241
|
+
existing_spec = existing_node.payload
|
242
|
+
if requirement_satisfied_by?(requirement, activated, existing_spec)
|
243
|
+
new_requirements = requirements.dup
|
244
|
+
push_state_for_requirements(new_requirements)
|
245
|
+
else
|
246
|
+
create_conflict
|
247
|
+
debug(depth) { 'Unsatisfied by existing spec' }
|
248
|
+
unwind_for_conflict
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Attempts to activate the current {#possibility} (given that it hasn't
|
253
|
+
# already been activated)
|
254
|
+
# @return [void]
|
255
|
+
def attempt_to_activate_new_spec
|
256
|
+
satisfied = begin
|
257
|
+
locked_requirement = locked_requirement_named(name)
|
258
|
+
requested_spec_satisfied = requirement_satisfied_by?(requirement, activated, possibility)
|
259
|
+
locked_spec_satisfied = !locked_requirement ||
|
260
|
+
requirement_satisfied_by?(locked_requirement, activated, possibility)
|
261
|
+
debug(depth) { 'Unsatisfied by requested spec' } unless requested_spec_satisfied
|
262
|
+
debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied
|
263
|
+
requested_spec_satisfied && locked_spec_satisfied
|
264
|
+
end
|
265
|
+
if satisfied
|
266
|
+
activate_spec
|
267
|
+
else
|
268
|
+
create_conflict
|
269
|
+
unwind_for_conflict
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# @param [String] requirement_name the spec name to search for
|
274
|
+
# @return [Object] the locked spec named `requirement_name`, if one
|
275
|
+
# is found on {#base}
|
276
|
+
def locked_requirement_named(requirement_name)
|
277
|
+
vertex = base.vertex_named(requirement_name)
|
278
|
+
vertex && vertex.payload
|
279
|
+
end
|
280
|
+
|
281
|
+
# Add the current {#possibility} to the dependency graph of the current
|
282
|
+
# {#state}
|
283
|
+
# @return [void]
|
284
|
+
def activate_spec
|
285
|
+
conflicts.delete(name)
|
286
|
+
debug(depth) { 'activated ' + name + ' at ' + possibility.to_s }
|
287
|
+
vertex = activated.vertex_named(name)
|
288
|
+
vertex.payload = possibility
|
289
|
+
require_nested_dependencies_for(possibility)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Requires the dependencies that the recently activated spec has
|
293
|
+
# @param [Object] activated_spec the specification that has just been
|
294
|
+
# activated
|
295
|
+
# @return [void]
|
296
|
+
def require_nested_dependencies_for(activated_spec)
|
297
|
+
nested_dependencies = dependencies_for(activated_spec)
|
298
|
+
debug(depth) { "requiring nested dependencies (#{nested_dependencies.map(&:to_s).join(', ')})" }
|
299
|
+
nested_dependencies.each { |d| activated.add_child_vertex name_for(d), nil, [name_for(activated_spec)], d }
|
300
|
+
|
301
|
+
push_state_for_requirements(requirements + nested_dependencies)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Pushes a new {DependencyState} that encapsulates both existing and new
|
305
|
+
# requirements
|
306
|
+
# @param [Array] new_requirements
|
307
|
+
# @return [void]
|
308
|
+
def push_state_for_requirements(new_requirements)
|
309
|
+
new_requirements = sort_dependencies(new_requirements, activated, conflicts)
|
310
|
+
new_requirement = new_requirements.shift
|
311
|
+
states.push DependencyState.new(
|
312
|
+
new_requirement ? name_for(new_requirement) : '',
|
313
|
+
new_requirements,
|
314
|
+
activated.dup,
|
315
|
+
new_requirement,
|
316
|
+
new_requirement ? search_for(new_requirement) : [],
|
317
|
+
depth,
|
318
|
+
conflicts.dup
|
319
|
+
)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'molinillo/dependency_graph'
|
2
|
+
|
3
|
+
module Molinillo
|
4
|
+
# This class encapsulates a dependency resolver.
|
5
|
+
# The resolver is responsible for determining which set of dependencies to
|
6
|
+
# activate, with feedback from the the {#specification_provider}
|
7
|
+
#
|
8
|
+
#
|
9
|
+
class Resolver
|
10
|
+
require 'molinillo/resolution'
|
11
|
+
|
12
|
+
# @return [SpecificationProvider] the specification provider used
|
13
|
+
# in the resolution process
|
14
|
+
attr_reader :specification_provider
|
15
|
+
|
16
|
+
# @return [UI] the UI module used to communicate back to the user
|
17
|
+
# during the resolution process
|
18
|
+
attr_reader :resolver_ui
|
19
|
+
|
20
|
+
# @param [SpecificationProvider] specification_provider
|
21
|
+
# see {#specification_provider}
|
22
|
+
# @param [UI] resolver_ui
|
23
|
+
# see {#resolver_ui}
|
24
|
+
def initialize(specification_provider, resolver_ui)
|
25
|
+
@specification_provider = specification_provider
|
26
|
+
@resolver_ui = resolver_ui
|
27
|
+
end
|
28
|
+
|
29
|
+
# Resolves the requested dependencies into a {DependencyGraph},
|
30
|
+
# locking to the base dependency graph (if specified)
|
31
|
+
# @param [Array] requested an array of 'requested' dependencies that the
|
32
|
+
# {#specification_provider} can understand
|
33
|
+
# @param [DependencyGraph,nil] base the base dependency graph to which
|
34
|
+
# dependencies should be 'locked'
|
35
|
+
def resolve(requested, base = DependencyGraph.new)
|
36
|
+
Resolution.new(specification_provider,
|
37
|
+
resolver_ui,
|
38
|
+
requested,
|
39
|
+
base).
|
40
|
+
resolve
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Molinillo
|
2
|
+
# A state that a {Resolution} can be in
|
3
|
+
# @attr [String] name
|
4
|
+
# @attr [Array<Object>] requirements
|
5
|
+
# @attr [DependencyGraph] activated
|
6
|
+
# @attr [Object] requirement
|
7
|
+
# @attr [Object] possibility
|
8
|
+
# @attr [Integer] depth
|
9
|
+
# @attr [Set<Object>] conflicts
|
10
|
+
ResolutionState = Struct.new(
|
11
|
+
:name,
|
12
|
+
:requirements,
|
13
|
+
:activated,
|
14
|
+
:requirement,
|
15
|
+
:possibilities,
|
16
|
+
:depth,
|
17
|
+
:conflicts
|
18
|
+
)
|
19
|
+
|
20
|
+
# A state that encapsulates a set of {#requirements} with an {Array} of
|
21
|
+
# possibilities
|
22
|
+
class DependencyState < ResolutionState
|
23
|
+
# Removes a possibility from `self`
|
24
|
+
# @return [PossibilityState] a state with a single possibility,
|
25
|
+
# the possibility that was removed from `self`
|
26
|
+
def pop_possibility_state
|
27
|
+
PossibilityState.new(
|
28
|
+
name,
|
29
|
+
requirements.dup,
|
30
|
+
activated.dup,
|
31
|
+
requirement,
|
32
|
+
[possibilities.pop],
|
33
|
+
depth + 1,
|
34
|
+
conflicts.dup
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# A state that encapsulates a single possibility to fulfill the given
|
40
|
+
# {#requirement}
|
41
|
+
class PossibilityState < ResolutionState
|
42
|
+
end
|
43
|
+
end
|