OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Attempts to resolve a set of version constraints for a package dependency | |
7 * graph and select an appropriate set of best specific versions for all | |
8 * dependent packages. It works iteratively and tries to reach a stable | |
9 * solution where the constraints of all dependencies are met. If it fails to | |
10 * reach a solution after a certain number of iterations, it assumes the | |
11 * dependency graph is unstable and reports and error. | |
12 * | |
13 * There are two fundamental operations in the process of iterating over the | |
14 * graph: | |
15 * | |
16 * 1. Changing the selected concrete version of some package. (This includes | |
17 * adding and removing a package too, which is considering changing the | |
18 * version to or from "none".) In other words, a node has changed. | |
19 * 2. Changing the version constraint that one package places on another. In | |
20 * other words, and edge has changed. | |
21 * | |
22 * Both of these events have a corresponding (potentional) async operation and | |
23 * roughly cycle back and forth between each other. When we change the version | |
24 * of package changes, we asynchronously load the pubspec for the new version. | |
25 * When that's done, we compare the dependencies of the new version versus the | |
26 * old one. For everything that differs, we change those constraints between | |
27 * this package and that dependency. | |
28 * | |
29 * When a constraint on a package changes, we re-calculate the overall | |
30 * constraint on that package. I.e. with a shared dependency, we intersect all | |
31 * of the constraints that its depending packages place on it. If that overall | |
32 * constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently | |
33 * picked version for that package may fall outside of the new constraint. If | |
34 * that happens, we find the new best version that meets the updated constraint | |
35 * and then the change the package to use that version. That cycles back up to | |
36 * the beginning again. | |
37 */ | |
38 #library('version_solver'); | |
39 | |
40 #import('package.dart'); | |
41 #import('pubspec.dart'); | |
42 #import('source.dart'); | |
43 #import('source_registry.dart'); | |
44 #import('utils.dart'); | |
45 #import('version.dart'); | |
46 | |
47 Future<Map<String, Version>> resolveVersions( | |
nweiz
2012/06/18 18:29:19
This could use some docs.
| |
48 SourceRegistry sources, Package root) { | |
49 return new VersionSolver(sources).solve(root); | |
50 } | |
51 | |
52 class VersionSolver { | |
53 final SourceRegistry _sources; | |
54 final PubspecCache _pubspecs; | |
55 final Map<String, Dependency> _packages; | |
56 final Queue<WorkItem> _work; | |
57 int _numIterations = 0; | |
58 | |
59 VersionSolver(SourceRegistry sources) | |
60 : _sources = sources, | |
61 _pubspecs = new PubspecCache(sources), | |
62 _packages = <Dependency>{}, | |
63 _work = new Queue<WorkItem>(); | |
64 | |
65 Future<Map<String, Version>> solve(Package root) { | |
66 // Kick off the work by adding the root package at its concrete version to | |
67 // the dependency graph. | |
68 _pubspecs.cache(root); | |
69 enqueue(new ChangeConstraint('(entrypoint)', root.name, root.version)); | |
nweiz
2012/06/18 18:29:19
What happens if some other package depends on the
Bob Nystrom
2012/06/20 01:40:04
There TODOs to add tests for that. Added tests. Th
| |
70 | |
71 Future processNextWorkItem(_) { | |
72 while (true) { | |
73 // Stop if we are done. | |
74 if (_work.isEmpty()) return new Future.immediate(buildResults()); | |
75 | |
76 // If we appear to be stuck in a loop, then we probably have an unstable | |
77 // graph, bail. We guess this based on a rough heuristic that it should | |
78 // only take a certain number of steps to solve a graph with a given | |
79 // number of connections. | |
80 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune | |
81 // when we have a better picture of real-world package topologies. | |
nweiz
2012/06/18 18:29:19
Are we planning to try to manually catch infinite
Bob Nystrom
2012/06/20 01:40:04
Right now, it's the sole method. Catching loops wi
nweiz
2012/06/20 21:08:48
It seems like a nice thing to do once we run out o
| |
82 _numIterations++; | |
83 if (_numIterations > Math.max(50, _packages.length * 5)) { | |
84 throw new CouldNotSolveException(); | |
85 } | |
86 | |
87 // Run the first work item. | |
88 var future = _work.removeFirst().process(this); | |
89 | |
90 // If we have an async operation to perform, chain the loop to resume | |
91 // when it's done. Otherwise, just loop synchronously. | |
nweiz
2012/06/18 18:29:19
Wouldn't it be possible to process other work item
Bob Nystrom
2012/06/20 01:40:04
In theory yes, but that adds a lot of complexity I
nweiz
2012/06/20 21:08:48
Probably not... that could also (potentially) make
| |
92 if (future != null) { | |
93 return future.chain(processNextWorkItem); | |
94 } | |
95 } | |
96 } | |
97 | |
98 return processNextWorkItem(null); | |
99 } | |
100 | |
101 void enqueue(WorkItem work) { | |
102 _work.add(work); | |
103 } | |
104 | |
105 Dependency getDependency(String package) { | |
106 // There can be unused dependencies in the graph, so just create an empty | |
107 // one if needed. | |
108 _packages.putIfAbsent(package, () => new Dependency()); | |
109 return _packages[package]; | |
110 } | |
111 | |
112 /** | |
113 * Sets the best selected version of [package] to [version]. | |
114 */ | |
115 void setVersion(String package, Version version) { | |
116 _packages[package].version = version; | |
117 } | |
118 | |
119 Map<String, Version> buildResults() { | |
120 var results = <Version>{}; | |
121 _packages.forEach((name, dependency) { | |
122 if (dependency.isDependedOn) { | |
123 results[name] = dependency.version; | |
124 } | |
125 }); | |
126 | |
127 return results; | |
128 } | |
129 } | |
130 | |
131 /** | |
132 * The constraint solver works by iteratively processing a queue work items. | |
nweiz
2012/06/18 18:29:19
"a queue of work items"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
133 * Each item is a single atomic change to the dependency graph. Handling them | |
134 * in a queue lets us handle asynchrony (resolving versions requires information | |
135 * from servers) as well as avoiding deeply nest recursion. | |
nweiz
2012/06/18 18:29:19
"avoid deeply nested recursion"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
136 */ | |
137 interface WorkItem { | |
138 /** | |
139 * Processes this work item. Returns a future that completes when the work is | |
140 * done. If `null` is returned, that means the work has completed | |
141 * synchronously and the next item can be started immediately. | |
142 */ | |
143 Future process(VersionSolver solver); | |
144 } | |
145 | |
146 /** | |
147 * The best selected version for [package] has changed to [version]. | |
148 * If the previous version of the package is `null`, that means the package is | |
149 * being added to the graph. If [version] is `null`, it is being removed. | |
150 */ | |
151 class ChangeVersion implements WorkItem { | |
152 /** | |
153 * The package whose version is changing. | |
154 */ | |
155 final String package; | |
156 | |
157 /** | |
158 * The new selected version. | |
159 */ | |
160 final Version version; | |
161 | |
162 ChangeVersion(this.package, this.version); | |
163 | |
164 Future process(VersionSolver solver) { | |
165 var oldVersion = solver.getDependency(package).version; | |
166 solver.setVersion(package, version); | |
167 | |
168 // The dependencies between the old and new version may be different. Walk | |
169 // them both and update any constraints that differ between the two. | |
170 return getDependencies(solver, oldVersion).chain((oldDependencies) { | |
171 return getDependencies(solver, version).transform((newDependencies) { | |
nweiz
2012/06/18 18:29:19
You could increase parallelism and reduce nesting
Bob Nystrom
2012/06/20 01:40:04
I think I had that at one point but it felt like o
nweiz
2012/06/20 21:08:48
I think it would be cleaner.
Bob Nystrom
2012/06/20 22:29:54
Done.
| |
172 for (var dependency in oldDependencies.getValues()) { | |
173 var constraint; | |
174 if (newDependencies.containsKey(dependency.name)) { | |
175 // The dependency is in both versions of this package, but its | |
176 // constraint may have changed. | |
177 constraint = newDependencies[dependency.name].version; | |
nweiz
2012/06/18 18:29:19
constraint = newDependencies.remove(dependency.nam
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
178 newDependencies.remove(dependency.name); | |
179 } else { | |
180 // The dependency is not in the new version of the package, so just | |
181 // remove its constraint. | |
182 constraint = null; | |
183 } | |
184 | |
185 solver.enqueue(new ChangeConstraint( | |
186 package, dependency.name, constraint)); | |
187 } | |
188 | |
189 // Everything that's left is a depdendency that's only in the new | |
190 // version of the package. | |
191 for (var dependency in newDependencies.getValues()) { | |
192 solver.enqueue(new ChangeConstraint( | |
193 package, dependency.name, dependency.version)); | |
194 } | |
195 }); | |
196 }); | |
197 } | |
198 | |
199 /** | |
200 * Get the dependencies that [package] has at [version]. | |
201 */ | |
202 Future<Map<String, PackageRef>> getDependencies(VersionSolver solver, | |
nweiz
2012/06/18 18:29:19
This name is confusing, since the method doesn't h
Bob Nystrom
2012/06/20 01:40:04
Done. What I wouldn't give for a shorter synonym f
nweiz
2012/06/20 21:08:48
You could always use "dep".
Bob Nystrom
2012/06/20 22:29:54
For some reason, that bugs me. Maybe I just don't
nweiz
2012/06/20 23:47:45
You used "ref" ;).
I don't actually care either w
| |
203 Version version) { | |
204 // If there is no version, it means no package, so no dependencies. | |
205 if (version == null) { | |
206 return new Future<Map<String, PackageRef>>.immediate(<PackageRef>{}); | |
207 } | |
208 | |
209 return solver._pubspecs.load(package, version).transform((pubspec) { | |
210 var dependencies = <PackageRef>{}; | |
211 for (var dependency in pubspec.dependencies) { | |
212 dependencies[dependency.name] = dependency; | |
213 } | |
214 return dependencies; | |
215 }); | |
216 } | |
217 } | |
218 | |
219 /** | |
220 * The [VersionConstraint] that [package] places on depdendency [dependent] has | |
221 * changed from [from] to [to]. The [from] constraint may be `null` which means | |
222 * this is the first time [package] has depended on [dependent]. The [to] | |
223 * constraint may be `null` which means [package] no longer depends on | |
224 * [dependent]. | |
nweiz
2012/06/18 18:29:19
This is inaccurate. "[package]" and "[dependent]"
Bob Nystrom
2012/06/20 01:40:04
Stale comment is stale. This went through a few it
| |
225 */ | |
226 class ChangeConstraint implements WorkItem { | |
227 /** | |
228 * The package that has the dependency. | |
229 */ | |
230 final String from; | |
231 | |
232 /** | |
233 * The package being depended on. | |
234 */ | |
235 final String to; | |
nweiz
2012/06/18 18:29:19
I'm finding "from" and "to" confusing when reading
Bob Nystrom
2012/06/20 01:40:04
Changed to "depender" (which kind of sounds like s
| |
236 | |
237 /** | |
238 * The constraint that [from] places on [to]'s version. | |
239 */ | |
240 final VersionConstraint version; | |
nweiz
2012/06/18 18:29:19
I'd call this "constraint", since it's a VersionCo
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
241 | |
242 ChangeConstraint(this.from, this.to, this.version); | |
243 | |
244 Future process(VersionSolver solver) { | |
245 var dependency = solver.getDependency(to); | |
246 var oldConstraint = dependency.constraint; | |
247 dependency.setConstraint(from, version); | |
248 var newConstraint = dependency.constraint; | |
249 | |
250 // If we the package is over-constrained, i.e. the packages depending | |
nweiz
2012/06/18 18:29:19
"If the package is"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
251 // have disjoint constraints, then stop. | |
252 if (newConstraint != null && newConstraint.isEmpty) { | |
253 throw new DisjointConstraintException(to); | |
254 } | |
255 | |
256 // If this constraint change didn't cause the overall constraint on the | |
257 // package to change, then we don't need to do any further work. | |
258 if (oldConstraint == newConstraint) return null; | |
259 | |
260 // If the dependency has been cut free from the graph, just remove it. | |
261 if (!dependency.isDependedOn) { | |
262 solver.enqueue(new ChangeVersion(to, null)); | |
263 return null; | |
264 } | |
265 | |
266 // The constraint has changed, so see what the best version of the package | |
267 // that meets the new constraint is. | |
268 // TODO(rnystrom): Should this always be the default source? | |
269 var source = solver._sources.defaultSource; | |
270 return source.findVersion(to, newConstraint).transform((best) { | |
271 // TODO(rnystrom): Better exception. | |
272 if (best == null) throw new NoVersionException(to, newConstraint); | |
273 | |
274 if (dependency.version != best) { | |
275 solver.enqueue(new ChangeVersion(to, best)); | |
276 } | |
277 }); | |
278 } | |
279 } | |
280 | |
281 // TODO(rnystrom): Instead of always pulling from the source (which will mean | |
282 // hitting a server, we should cache pubspecs of installed packages in the | |
283 // system cache. | |
nweiz
2012/06/18 18:29:19
Rather than making a separate cache of just pubspe
Bob Nystrom
2012/06/20 01:40:04
Agreed that we should do this.
| |
284 /** | |
285 * Maintains a synchronous cache of previously-loaded pubspecs. Used to | |
nweiz
2012/06/18 18:29:19
What do you mean by "synchronous"?
Bob Nystrom
2012/06/20 01:40:04
Stale comment. This used to have a synchronous "ge
| |
286 * avoid requesting the same pubspec from the server repeatedly. | |
287 */ | |
288 class PubspecCache { | |
289 final SourceRegistry _sources; | |
290 final Map<String, Map<Version, Pubspec>> _pubspecs; | |
291 | |
292 PubspecCache(this._sources) | |
293 : _pubspecs = <Map<Version, Pubspec>>{}; | |
294 | |
295 /** | |
296 * Adds the already loaded [package] to the cache. | |
297 */ | |
298 void cache(Package package) { | |
299 _pubspecs.putIfAbsent(package.name, () => new Map<Version, Pubspec>()); | |
300 _pubspecs[package.name][package.version] = package.pubspec; | |
301 } | |
302 | |
303 /** | |
304 * Loads the pubspec for [package] at [version]. | |
305 */ | |
306 Future<Pubspec> load(String package, Version version) { | |
307 // Complete immediately if it's already cached. | |
308 if (_pubspecs.containsKey(package) && | |
309 _pubspecs[package].containsKey(version)) { | |
310 return new Future<Pubspec>.immediate(_pubspecs[package][version]); | |
311 } | |
312 | |
313 // TODO(rnystrom): Should this always be the default source? | |
314 var source = _sources.defaultSource; | |
315 return source.describe(package, version).transform((pubspec) { | |
316 // Cache it. | |
317 _pubspecs.putIfAbsent(package, () => new Map<Version, Pubspec>()); | |
318 _pubspecs[package][version] = pubspec; | |
319 | |
320 return pubspec; | |
321 }); | |
322 } | |
323 } | |
324 | |
325 /** | |
326 * Describes one [Package] in the [DependencyGraph] and keeps track of which | |
327 * packages depend on it and what [VersionConstraint]s they place on it. | |
328 */ | |
329 class Dependency { | |
330 /** | |
331 * The currently selected best version for this dependency. | |
332 */ | |
333 Version version; | |
334 | |
335 /** | |
336 * The constraints that depending packages have placed on this one. | |
337 */ | |
338 final Map<String, VersionConstraint> _constraints; | |
339 | |
340 /** | |
341 * Gets whether or not any other packages are currently depending on this | |
342 * one. If `false`, then it means this package is not part of the dependency | |
343 * graph and should be omitted. | |
344 */ | |
345 bool get isDependedOn() => !_constraints.isEmpty(); | |
346 | |
347 /** | |
348 * Gets the overall constraint that all packages are placing on this one. | |
349 * If no packages have a constraint on this one (which can happen when this | |
350 * package is in the process of being added to the graph), returns `null`. | |
351 */ | |
352 VersionConstraint get constraint() { | |
353 if (_constraints.isEmpty()) return null; | |
354 return new VersionConstraint.intersect(_constraints.getValues()); | |
355 } | |
356 | |
357 Dependency() | |
358 : _constraints = <VersionConstraint>{}; | |
359 | |
360 /** | |
361 * Places [constraint] from [package] onto this. | |
362 */ | |
363 void setConstraint(String package, VersionConstraint constraint) { | |
nweiz
2012/06/18 18:29:19
"addConstraint" would be a clearer name. It's true
Bob Nystrom
2012/06/20 01:40:04
Changed to "placeConstraint". :)
| |
364 if (constraint == null) { | |
365 _constraints.remove(package); | |
366 } else { | |
367 _constraints[package] = constraint; | |
368 } | |
369 } | |
370 } | |
371 | |
372 // TODO(rnystrom): Report the last of depending packages and their constraints. | |
373 /** | |
374 * Exception thrown when the [VersionConstraint] used to match a package is | |
375 * valid (i.e. non-empty), but there are no released versions of the package | |
376 * that fit that constraint. | |
377 */ | |
378 class NoVersionException implements Exception { | |
379 final String package; | |
380 final VersionConstraint version; | |
nweiz
2012/06/18 18:29:19
s/version/constraint/.
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
381 | |
382 NoVersionException(this.package, this.version); | |
383 | |
384 String toString() => | |
385 "Package '$package' has no versions that match constraint $version."; | |
386 } | |
387 | |
388 // TODO(rnystrom): Report the last of depending packages and their constraints. | |
389 /** | |
390 * Exception thrown when the [VersionConstraint] used to match a package is | |
391 * the empty set: in other words, multiple packages depend on it and have | |
392 * conflicting constraints that have no overlap. | |
393 */ | |
394 class DisjointConstraintException implements Exception { | |
395 final String package; | |
396 | |
397 DisjointConstraintException(this.package); | |
398 | |
399 String toString() => | |
400 "Package '$package' has disjoint constraints."; | |
401 } | |
402 | |
403 /** | |
404 * Exception thrown when the [VersionSolver] fails to find a solution after a | |
405 * certain number of iterations. | |
406 */ | |
407 class CouldNotSolveException implements Exception { | |
408 CouldNotSolveException(); | |
409 | |
410 String toString() => | |
411 "Could not find a solution that met all version constraints."; | |
412 } | |
OLD | NEW |