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.
@@ -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