molinillo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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