OLD | NEW |
---|---|
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// Attempts to resolve a set of version constraints for a package dependency | |
6 /// graph and select an appropriate set of best specific versions for all | |
7 /// dependent packages. It works iteratively and tries to reach a stable | |
8 /// solution where the constraints of all dependencies are met. If it fails to | |
9 /// reach a solution after a certain number of iterations, it assumes the | |
10 /// dependency graph is unstable and reports and error. | |
11 /// | |
12 /// There are two fundamental operations in the process of iterating over the | |
13 /// graph: | |
14 /// | |
15 /// 1. Changing the selected concrete version of some package. (This includes | |
16 /// adding and removing a package too, which is considering changing the | |
17 /// version to or from "none".) In other words, a node has changed. | |
18 /// 2. Changing the version constraint that one package places on another. In | |
19 /// other words, and edge has changed. | |
20 /// | |
21 /// Both of these events have a corresponding (potentional) async operation and | |
22 /// roughly cycle back and forth between each other. When we change the version | |
23 /// of package changes, we asynchronously load the pubspec for the new version. | |
24 /// When that's done, we compare the dependencies of the new version versus the | |
25 /// old one. For everything that differs, we change those constraints between | |
26 /// this package and that dependency. | |
27 /// | |
28 /// When a constraint on a package changes, we re-calculate the overall | |
29 /// constraint on that package. I.e. with a shared dependency, we intersect all | |
30 /// of the constraints that its depending packages place on it. If that overall | |
31 /// constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently | |
32 /// picked version for that package may fall outside of the new constraint. If | |
33 /// that happens, we find the new best version that meets the updated constraint | |
34 /// and then the change the package to use that version. That cycles back up to | |
35 /// the beginning again. | |
36 library version_solver; | 5 library version_solver; |
37 | 6 |
38 import 'dart:async'; | 7 import 'dart:async'; |
39 import 'dart:collection' show Queue; | 8 import 'dart:collection' show Queue; |
40 import 'dart:json' as json; | 9 import 'dart:json' as json; |
41 import 'dart:math'; | 10 import 'dart:math'; |
42 import 'lock_file.dart'; | 11 import 'lock_file.dart'; |
43 import 'log.dart' as log; | 12 import 'log.dart' as log; |
44 import 'package.dart'; | 13 import 'package.dart'; |
45 import 'pubspec.dart'; | 14 import 'pubspec.dart'; |
46 import 'source.dart'; | 15 import 'source.dart'; |
47 import 'source_registry.dart'; | 16 import 'source_registry.dart'; |
48 import 'utils.dart'; | 17 import 'utils.dart'; |
49 import 'version.dart'; | 18 import 'version.dart'; |
19 import 'version_solver1.dart' as v1; | |
20 import 'version_solver2.dart' as v2; | |
nweiz
2013/03/29 01:58:25
Name these after the type of version solver they c
Bob Nystrom
2013/03/30 00:15:55
Done.
| |
50 | 21 |
51 /// Attempts to select the best concrete versions for all of the transitive | 22 /// Attempts to select the best concrete versions for all of the transitive |
52 /// dependencies of [root] taking into account all of the [VersionConstraint]s | 23 /// dependencies of [root] taking into account all of the [VersionConstraint]s |
53 /// that those dependencies place on each other and the requirements imposed by | 24 /// that those dependencies place on each other and the requirements imposed by |
54 /// [lockFile]. If successful, completes to a [Map] that maps package names to | 25 /// [lockFile]. If successful, completes to a [Map] that maps package names to |
55 /// the selected version for that package. If it fails, the future will complete | 26 /// the selected version for that package. If it fails, the future will complete |
56 /// with a [NoVersionException], [DisjointConstraintException], or | 27 /// with a [NoVersionException], [DisjointConstraintException], or |
57 /// [CouldNotSolveException]. | 28 /// [CouldNotSolveException]. |
nweiz
2013/03/29 01:58:25
Document the new arguments.
Bob Nystrom
2013/03/30 00:15:55
Done.
| |
58 Future<List<PackageId>> resolveVersions(SourceRegistry sources, Package root, | 29 Future<List<PackageId>> resolveVersions(SourceRegistry sources, Package root, |
59 LockFile lockFile) { | 30 {int solver, LockFile lockFile, List<PackageRef> useLatest}) { |
nweiz
2013/03/29 01:58:25
Make solver an enum.
Bob Nystrom
2013/03/30 00:15:55
Changed to a bool.
| |
60 log.message('Resolving dependencies...'); | 31 log.message('Resolving dependencies...'); |
61 return new VersionSolver(sources, root, lockFile).solve(); | 32 |
33 if (solver == null) solver = 2; | |
nweiz
2013/03/29 01:58:25
Let's make the existing solver the default for now
Bob Nystrom
2013/03/30 00:15:55
I'll do that if you'll let me submit this patch be
nweiz
2013/04/03 00:28:43
I'm willing to LGTM without the backjumper, on the
Bob Nystrom
2013/04/08 22:12:59
OK, I'll default to the old solver for now. We'll
| |
34 if (lockFile == null) lockFile = new LockFile.empty(); | |
35 if (useLatest == null) useLatest = []; | |
nweiz
2013/03/29 01:58:25
Is our style to just avoid using default values ot
Bob Nystrom
2013/03/30 00:15:55
Yeah, I prefer to avoid them.
| |
36 | |
37 var versionSolver; | |
38 if (solver == 1) { | |
39 versionSolver = new v1.GreedyVersionSolver( | |
40 sources, root, lockFile, useLatest); | |
41 } else if (solver == 2) { | |
42 versionSolver = new v2.BacktrackingVersionSolver( | |
43 sources, root, lockFile, useLatest); | |
44 } else { | |
45 throw new ArgumentError("Argument 'solver' must be 1 or 2."); | |
46 } | |
47 | |
48 return versionSolver.solve(); | |
62 } | 49 } |
63 | 50 |
51 time(String name, Future operation) { | |
52 var stopwatch = new Stopwatch()..start(); | |
53 return operation.whenComplete(() { | |
54 log.message('$name took ${stopwatch.elapsed}'); | |
55 }); | |
56 } | |
nweiz
2013/03/29 01:58:25
Debugging function, remove it.
Bob Nystrom
2013/03/30 00:15:55
Done.
| |
57 | |
58 /// Base class for an implementation of the version constraint solver. | |
64 class VersionSolver { | 59 class VersionSolver { |
65 final SourceRegistry _sources; | 60 final SourceRegistry sources; |
66 final Package _root; | 61 final Package root; |
67 final LockFile lockFile; | 62 final LockFile lockFile; |
68 final PubspecCache _pubspecs; | 63 final PubspecCache cache; |
69 final Map<String, Dependency> _packages; | |
70 final Queue<WorkItem> _work; | |
71 int _numIterations = 0; | |
72 | 64 |
73 VersionSolver(SourceRegistry sources, this._root, this.lockFile) | 65 VersionSolver(SourceRegistry sources, this.root, this.lockFile, |
74 : _sources = sources, | 66 List<String> useLatest) |
75 _pubspecs = new PubspecCache(sources), | 67 : sources = sources, |
76 _packages = <String, Dependency>{}, | 68 cache = new PubspecCache(sources) { |
77 _work = new Queue<WorkItem>(); | 69 for (var package in useLatest) { |
78 | 70 forceLatestVersion(package); |
79 /// Tell the version solver to use the most recent version of [package] that | 71 lockFile.packages.remove(package); |
80 /// exists in whatever source it's installed from. If that version violates | 72 } |
81 /// constraints imposed by other dependencies, an error will be raised when | |
82 /// solving the versions, even if an earlier compatible version exists. | |
83 void useLatestVersion(String package) { | |
84 // TODO(nweiz): How do we want to detect and handle unknown dependencies | |
85 // here? | |
86 getDependency(package).useLatestVersion = true; | |
87 lockFile.packages.remove(package); | |
88 } | 73 } |
89 | 74 |
75 /// Force the solver to upgrade [package] to the latest available version. | |
76 void forceLatestVersion(String package); | |
77 | |
78 /// Run the solver. Completes with a list of specific package versions if | |
79 /// successful or an error if it failed to find a solution. | |
90 Future<List<PackageId>> solve() { | 80 Future<List<PackageId>> solve() { |
91 // Kick off the work by adding the root package at its concrete version to | 81 var stopwatch = new Stopwatch(); |
92 // the dependency graph. | 82 stopwatch.start(); |
93 var ref = new PackageRef.root(_root); | |
94 enqueue(new AddConstraint('(entrypoint)', ref)); | |
95 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); | |
96 | 83 |
97 Future processNextWorkItem(_) { | 84 // Gather some solving metrics. |
98 while (true) { | 85 return runSolver().whenComplete(() { |
99 // Stop if we are done. | 86 var buffer = new StringBuffer(); |
100 if (_work.isEmpty) return new Future.immediate(buildResults()); | 87 buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.'); |
101 | 88 buffer.writeln( |
102 // If we appear to be stuck in a loop, then we probably have an unstable | 89 '- Requested ${cache.versionCacheMisses} version lists'); |
103 // graph, bail. We guess this based on a rough heuristic that it should | 90 buffer.writeln( |
104 // only take a certain number of steps to solve a graph with a given | 91 '- Looked up ${cache.versionCacheHits} cached version lists'); |
105 // number of connections. | 92 buffer.writeln( |
106 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune | 93 '- Requested ${cache.pubspecCacheMisses} pubspecs'); |
107 // when we have a better picture of real-world package topologies. | 94 buffer.writeln( |
108 _numIterations++; | 95 '- Looked up ${cache.pubspecCacheHits} cached pubspecs'); |
109 if (_numIterations > max(50, _packages.length * 5)) { | 96 log.solver(buffer); |
110 throw new CouldNotSolveException(); | |
111 } | |
112 | |
113 // Run the first work item. | |
114 var future = _work.removeFirst().process(this); | |
115 | |
116 // If we have an async operation to perform, chain the loop to resume | |
117 // when it's done. Otherwise, just loop synchronously. | |
118 if (future != null) { | |
119 return future.then(processNextWorkItem); | |
120 } | |
121 } | |
122 } | |
123 | |
124 return processNextWorkItem(null); | |
125 } | |
126 | |
127 void enqueue(WorkItem work) { | |
128 _work.add(work); | |
129 } | |
130 | |
131 Dependency getDependency(String package) { | |
132 // There can be unused dependencies in the graph, so just create an empty | |
133 // one if needed. | |
134 _packages.putIfAbsent(package, () => new Dependency(package)); | |
135 return _packages[package]; | |
136 } | |
137 | |
138 /// Sets the best selected version of [package] to [version]. | |
139 void setVersion(String package, Version version) { | |
140 _packages[package].version = version; | |
141 } | |
142 | |
143 /// Returns the most recent version of [dependency] that satisfies all of its | |
144 /// version constraints. | |
145 Future<Version> getBestVersion(Dependency dependency) { | |
146 return dependency.getVersions().then((versions) { | |
147 var best = null; | |
148 for (var version in versions) { | |
149 if (dependency.useLatestVersion || | |
150 dependency.constraint.allows(version)) { | |
151 if (best == null || version > best) best = version; | |
152 } | |
153 } | |
154 | |
155 // TODO(rnystrom): Better exception. | |
156 if (best == null) { | |
157 if (tryUnlockDepender(dependency)) return null; | |
158 throw new NoVersionException(dependency.name, dependency.constraint, | |
159 dependency._refs); | |
160 } else if (!dependency.constraint.allows(best)) { | |
161 if (tryUnlockDepender(dependency)) return null; | |
162 throw new CouldNotUpdateException( | |
163 dependency.name, dependency.constraint, best); | |
164 } | |
165 | |
166 return best; | |
167 }); | 97 }); |
168 } | 98 } |
169 | 99 |
170 /// Looks for a package that depends (transitively) on [dependency] and has | 100 /// Entrypoint for subclasses to actually begin solving. External code should |
171 /// its version locked in the lockfile. If one is found, enqueues an | 101 /// call [solve()]. |
172 /// [UnlockPackage] work item for it and returns true. Otherwise, returns | 102 Future<List<PackageId>> runSolver(); |
173 /// false. | |
174 /// | |
175 /// This does a breadth-first search; immediate dependers will be unlocked | |
176 /// first, followed by transitive dependers. | |
177 bool tryUnlockDepender(Dependency dependency, [Set<String> seen]) { | |
178 if (seen == null) seen = new Set(); | |
179 // Avoid an infinite loop if there are circular dependencies. | |
180 if (seen.contains(dependency.name)) return false; | |
181 seen.add(dependency.name); | |
182 | |
183 for (var dependerName in dependency.dependers) { | |
184 var depender = getDependency(dependerName); | |
185 var locked = lockFile.packages[dependerName]; | |
186 if (locked != null && depender.version == locked.version && | |
187 depender.source.name == locked.source.name) { | |
188 enqueue(new UnlockPackage(depender)); | |
189 return true; | |
190 } | |
191 } | |
192 | |
193 return dependency.dependers.map(getDependency).any((subdependency) => | |
194 tryUnlockDepender(subdependency, seen)); | |
195 } | |
196 | |
197 List<PackageId> buildResults() { | |
198 return _packages.values.where((dep) => dep.isDependedOn).map((dep) { | |
199 var description = dep.description; | |
200 | |
201 // If the lockfile contains a fully-resolved description for the package, | |
202 // use that. This allows e.g. Git to ensure that the same commit is used. | |
203 var lockedPackage = lockFile.packages[dep.name]; | |
204 if (lockedPackage != null && lockedPackage.version == dep.version && | |
205 lockedPackage.source.name == dep.source.name && | |
206 dep.source.descriptionsEqual( | |
207 description, lockedPackage.description)) { | |
208 description = lockedPackage.description; | |
209 } | |
210 | |
211 return new PackageId(dep.name, dep.source, dep.version, description); | |
212 }) | |
213 .toList(); | |
214 } | |
215 } | 103 } |
216 | 104 |
217 /// The constraint solver works by iteratively processing a queue of work items. | 105 /// Maintains a cache of previously-requested data: pubspecs and version lists. |
218 /// Each item is a single atomic change to the dependency graph. Handling them | 106 /// Used to avoid requesting the same pubspec from the server repeatedly. |
219 /// in a queue lets us handle asynchrony (resolving versions requires | |
220 /// information from servers) as well as avoid deeply nested recursion. | |
221 abstract class WorkItem { | |
222 /// Processes this work item. Returns a future that completes when the work is | |
223 /// done. If `null` is returned, that means the work has completed | |
224 /// synchronously and the next item can be started immediately. | |
225 Future process(VersionSolver solver); | |
226 } | |
227 | |
228 /// The best selected version for a package has changed to [version]. If the | |
229 /// previous version of the package is `null`, that means the package is being | |
230 /// added to the graph. If [version] is `null`, it is being removed. | |
231 class ChangeVersion implements WorkItem { | |
232 /// The name of the package whose version is being changed. | |
233 final String package; | |
234 | |
235 /// The source of the package whose version is changing. | |
236 final Source source; | |
237 | |
238 /// The description identifying the package whose version is changing. | |
239 final description; | |
240 | |
241 /// The new selected version. | |
242 final Version version; | |
243 | |
244 ChangeVersion(this.package, this.source, this.description, this.version); | |
245 | |
246 Future process(VersionSolver solver) { | |
247 log.fine("Changing $package to version $version."); | |
248 | |
249 var dependency = solver.getDependency(package); | |
250 var oldVersion = dependency.version; | |
251 solver.setVersion(package, version); | |
252 | |
253 // The dependencies between the old and new version may be different. Walk | |
254 // them both and update any constraints that differ between the two. | |
255 return Future.wait([ | |
256 getDependencyRefs(solver, oldVersion), | |
257 getDependencyRefs(solver, version)]).then((list) { | |
258 var oldDependencyRefs = list[0]; | |
259 var newDependencyRefs = list[1]; | |
260 | |
261 for (var oldRef in oldDependencyRefs.values) { | |
262 if (newDependencyRefs.containsKey(oldRef.name)) { | |
263 // The dependency is in both versions of this package, but its | |
264 // constraint may have changed. | |
265 var newRef = newDependencyRefs.remove(oldRef.name); | |
266 solver.enqueue(new AddConstraint(package, newRef)); | |
267 } else { | |
268 // The dependency is not in the new version of the package, so just | |
269 // remove its constraint. | |
270 solver.enqueue(new RemoveConstraint(package, oldRef.name)); | |
271 } | |
272 } | |
273 | |
274 // Everything that's left is a depdendency that's only in the new | |
275 // version of the package. | |
276 for (var newRef in newDependencyRefs.values) { | |
277 solver.enqueue(new AddConstraint(package, newRef)); | |
278 } | |
279 }); | |
280 } | |
281 | |
282 /// Get the dependencies at [version] of the package being changed. | |
283 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, | |
284 Version version) { | |
285 // If there is no version, it means no package, so no dependencies. | |
286 if (version == null) { | |
287 return new Future<Map<String, PackageRef>>.immediate( | |
288 <String, PackageRef>{}); | |
289 } | |
290 | |
291 var id = new PackageId(package, source, version, description); | |
292 return solver._pubspecs.load(id).then((pubspec) { | |
293 var dependencies = <String, PackageRef>{}; | |
294 for (var dependency in pubspec.dependencies) { | |
295 dependencies[dependency.name] = dependency; | |
296 } | |
297 | |
298 // Include dev dependencies only from the root package. | |
299 if (id.isRoot) { | |
300 for (var dependency in pubspec.devDependencies) { | |
301 dependencies[dependency.name] = dependency; | |
302 } | |
303 } | |
304 | |
305 return dependencies; | |
306 }); | |
307 } | |
308 } | |
309 | |
310 /// A constraint that a depending package places on a dependent package has | |
311 /// changed. | |
312 /// | |
313 /// This is an abstract class that contains logic for updating the dependency | |
314 /// graph once a dependency has changed. Changing the dependency is the | |
315 /// responsibility of subclasses. | |
316 abstract class ChangeConstraint implements WorkItem { | |
317 Future process(VersionSolver solver); | |
318 | |
319 void undo(VersionSolver solver); | |
320 | |
321 Future _processChange(VersionSolver solver, Dependency oldDependency, | |
322 Dependency newDependency) { | |
323 var name = newDependency.name; | |
324 var source = oldDependency.source != null ? | |
325 oldDependency.source : newDependency.source; | |
326 var description = oldDependency.description != null ? | |
327 oldDependency.description : newDependency.description; | |
328 var oldConstraint = oldDependency.constraint; | |
329 var newConstraint = newDependency.constraint; | |
330 | |
331 // If the package is over-constrained, i.e. the packages depending have | |
332 // disjoint constraints, then try unlocking a depender that's locked by the | |
333 // lockfile. If there are no remaining locked dependencies, throw an error. | |
334 if (newConstraint != null && newConstraint.isEmpty) { | |
335 if (solver.tryUnlockDepender(newDependency)) { | |
336 undo(solver); | |
337 return null; | |
338 } | |
339 | |
340 throw new DisjointConstraintException(name, newDependency._refs); | |
341 } | |
342 | |
343 // If this constraint change didn't cause the overall constraint on the | |
344 // package to change, then we don't need to do any further work. | |
345 if (oldConstraint == newConstraint) return null; | |
346 | |
347 // If the dependency has been cut free from the graph, just remove it. | |
348 if (!newDependency.isDependedOn) { | |
349 solver.enqueue(new ChangeVersion(name, source, description, null)); | |
350 return null; | |
351 } | |
352 | |
353 // If the dependency is on the root package, then we don't need to do | |
354 // anything since it's already at the best version. | |
355 if (name == solver._root.name) { | |
356 solver.enqueue(new ChangeVersion( | |
357 name, source, description, solver._root.version)); | |
358 return null; | |
359 } | |
360 | |
361 // If the dependency is on a package in the lockfile, use the lockfile's | |
362 // version for that package if it's valid given the other constraints. | |
363 var lockedPackage = solver.lockFile.packages[name]; | |
364 if (lockedPackage != null && newDependency.source == lockedPackage.source) { | |
365 var lockedVersion = lockedPackage.version; | |
366 if (newConstraint.allows(lockedVersion)) { | |
367 solver.enqueue( | |
368 new ChangeVersion(name, source, description, lockedVersion)); | |
369 return null; | |
370 } | |
371 } | |
372 | |
373 // The constraint has changed, so see what the best version of the package | |
374 // that meets the new constraint is. | |
375 return solver.getBestVersion(newDependency).then((best) { | |
376 if (best == null) { | |
377 undo(solver); | |
378 } else if (newDependency.version != best) { | |
379 solver.enqueue(new ChangeVersion(name, source, description, best)); | |
380 } | |
381 }); | |
382 } | |
383 } | |
384 | |
385 /// The constraint given by [ref] is being placed by [depender]. | |
386 class AddConstraint extends ChangeConstraint { | |
387 /// The package that has the dependency. | |
388 final String depender; | |
389 | |
390 /// The package being depended on and the constraints being placed on it. The | |
391 /// source, version, and description in this ref are all considered | |
392 /// constraints on the dependent package. | |
393 final PackageRef ref; | |
394 | |
395 AddConstraint(this.depender, this.ref); | |
396 | |
397 Future process(VersionSolver solver) { | |
398 log.fine("Adding $depender's constraint $ref."); | |
399 | |
400 var dependency = solver.getDependency(ref.name); | |
401 var oldDependency = dependency.clone(); | |
402 dependency.placeConstraint(depender, ref); | |
403 return _processChange(solver, oldDependency, dependency); | |
404 } | |
405 | |
406 void undo(VersionSolver solver) { | |
407 solver.getDependency(ref.name).removeConstraint(depender); | |
408 } | |
409 } | |
410 | |
411 /// [depender] is no longer placing a constraint on [dependent]. | |
412 class RemoveConstraint extends ChangeConstraint { | |
413 /// The package that was placing a constraint on [dependent]. | |
414 String depender; | |
415 | |
416 /// The package that was being depended on. | |
417 String dependent; | |
418 | |
419 /// The constraint that was removed. | |
420 PackageRef _removed; | |
421 | |
422 RemoveConstraint(this.depender, this.dependent); | |
423 | |
424 Future process(VersionSolver solver) { | |
425 log.fine("Removing $depender's constraint ($_removed) on $dependent."); | |
426 | |
427 var dependency = solver.getDependency(dependent); | |
428 var oldDependency = dependency.clone(); | |
429 _removed = dependency.removeConstraint(depender); | |
430 return _processChange(solver, oldDependency, dependency); | |
431 } | |
432 | |
433 void undo(VersionSolver solver) { | |
434 solver.getDependency(dependent).placeConstraint(depender, _removed); | |
435 } | |
436 } | |
437 | |
438 /// [package]'s version is no longer constrained by the lockfile. | |
439 class UnlockPackage implements WorkItem { | |
440 /// The package being unlocked. | |
441 Dependency package; | |
442 | |
443 UnlockPackage(this.package); | |
444 | |
445 Future process(VersionSolver solver) { | |
446 log.fine("Unlocking ${package.name}."); | |
447 | |
448 solver.lockFile.packages.remove(package.name); | |
449 return solver.getBestVersion(package).then((best) { | |
450 if (best == null) return null; | |
451 solver.enqueue(new ChangeVersion( | |
452 package.name, package.source, package.description, best)); | |
453 }); | |
454 } | |
455 } | |
456 | |
457 // TODO(rnystrom): Instead of always pulling from the source (which will mean | |
458 // hitting a server), we should consider caching pubspecs of uninstalled | |
459 // packages in the system cache. | |
460 /// Maintains a cache of previously-loaded pubspecs. Used to avoid requesting | |
461 /// the same pubspec from the server repeatedly. | |
462 class PubspecCache { | 107 class PubspecCache { |
463 final SourceRegistry _sources; | 108 final SourceRegistry _sources; |
464 final Map<PackageId, Pubspec> _pubspecs; | 109 final _versions = new Map<PackageId, List<Version>>(); |
110 final _pubspecs = new Map<PackageId, Pubspec>(); | |
465 | 111 |
466 PubspecCache(this._sources) | 112 /// The number of times a version list was requested and it wasn't cached and |
467 : _pubspecs = new Map<PackageId, Pubspec>(); | 113 /// had to be requested from the source. |
114 int versionCacheMisses = 0; | |
115 | |
116 /// The number of times a version list was requested and the cached version | |
117 /// was returned. | |
118 int versionCacheHits = 0; | |
119 | |
120 /// The number of times a pubspec was requested and it wasn't cached and had | |
121 /// to be requested from the source. | |
122 int pubspecCacheMisses = 0; | |
123 | |
124 /// The number of times a pubspec was requested and the cached version was | |
125 /// returned. | |
126 int pubspecCacheHits = 0; | |
127 | |
128 PubspecCache(this._sources); | |
468 | 129 |
469 /// Caches [pubspec] as the [Pubspec] for the package identified by [id]. | 130 /// Caches [pubspec] as the [Pubspec] for the package identified by [id]. |
470 void cache(PackageId id, Pubspec pubspec) { | 131 void cache(PackageId id, Pubspec pubspec) { |
471 _pubspecs[id] = pubspec; | 132 _pubspecs[id] = pubspec; |
472 } | 133 } |
473 | 134 |
474 /// Loads the pubspec for the package identified by [id]. | 135 /// Loads the pubspec for the package identified by [id]. |
475 Future<Pubspec> load(PackageId id) { | 136 Future<Pubspec> getPubspec(PackageId id) { |
476 // Complete immediately if it's already cached. | 137 // Complete immediately if it's already cached. |
477 if (_pubspecs.containsKey(id)) { | 138 if (_pubspecs.containsKey(id)) { |
139 pubspecCacheHits++; | |
478 return new Future<Pubspec>.immediate(_pubspecs[id]); | 140 return new Future<Pubspec>.immediate(_pubspecs[id]); |
479 } | 141 } |
480 | 142 |
143 pubspecCacheMisses++; | |
481 return id.describe().then((pubspec) { | 144 return id.describe().then((pubspec) { |
145 log.solver('requested ${id} pubspec'); | |
nweiz
2013/03/29 01:58:25
"${id}" => "$id"
Bob Nystrom
2013/03/30 00:15:55
Done.
| |
146 | |
482 // Cache it. | 147 // Cache it. |
483 _pubspecs[id] = pubspec; | 148 _pubspecs[id] = pubspec; |
484 return pubspec; | 149 return pubspec; |
485 }); | 150 }); |
486 } | 151 } |
152 | |
153 /// Gets the list of versions for [package] in descending order. | |
154 Future<List<PackageId>> getVersions(String package, Source source, description ) { | |
155 // Create a fake ID to use as a key. | |
156 var id = new PackageId(package, source, Version.none, description); | |
157 | |
158 // See if we have it cached. | |
159 var versions = _versions[id]; | |
160 if (versions != null) { | |
161 versionCacheHits++; | |
162 return new Future.immediate(versions); | |
163 } | |
164 | |
165 versionCacheMisses++; | |
166 return source.getVersions(package, description).then((versions) { | |
167 var ids = versions | |
168 .map((version) => new PackageId(package, source, version, description) ) | |
169 .toList(); | |
170 | |
171 // Sort by descending version so we try newer versions first. | |
172 ids.sort((a, b) => b.version.compareTo(a.version)); | |
173 | |
174 log.solver('requested $package version list'); | |
175 _versions[id] = ids; | |
176 return ids; | |
177 }); | |
178 } | |
487 } | 179 } |
488 | 180 |
489 /// Describes one [Package] in the [DependencyGraph] and keeps track of which | 181 /// A reference from a depending package to a package that it depends on. |
490 /// packages depend on it and what constraints they place on it. | |
491 class Dependency { | 182 class Dependency { |
492 /// The name of the this dependency's package. | 183 /// The name of the package that has this dependency. |
493 final String name; | 184 final String depender; |
494 | 185 |
495 /// The [PackageRefs] that represent constraints that depending packages have | 186 /// The referenced dependent package. |
496 /// placed on this one. | 187 final PackageRef ref; |
497 final Map<String, PackageRef> _refs; | |
498 | 188 |
499 /// The currently-selected best version for this dependency. | 189 Dependency(this.depender, this.ref); |
500 Version version; | 190 } |
501 | 191 |
502 /// Whether this dependency should always select the latest version. | 192 /// Base class for all failures that can occur while trying to resolve versions. |
503 bool useLatestVersion = false; | 193 class SolverFailure implements Exception { |
504 | 194 /// Writes [deps] to [buffer] as a bullet list. |
505 /// Gets whether or not any other packages are currently depending on this | 195 void _writeDependencies(StringBuffer buffer, List<Dependency> deps) { |
506 /// one. If `false`, then it means this package is not part of the dependency | 196 var map = {}; |
507 /// graph and should be omitted. | 197 for (var dep in deps) { |
508 bool get isDependedOn => !_refs.isEmpty; | 198 map[dep.depender] = dep.ref; |
509 | |
510 /// The names of all the packages that depend on this dependency. | |
511 Iterable<String> get dependers => _refs.keys; | |
512 | |
513 /// Gets the overall constraint that all packages are placing on this one. | |
514 /// If no packages have a constraint on this one (which can happen when this | |
515 /// package is in the process of being added to the graph), returns `null`. | |
516 VersionConstraint get constraint { | |
517 if (_refs.isEmpty) return null; | |
518 return new VersionConstraint.intersection( | |
519 _refs.values.map((ref) => ref.constraint)); | |
520 } | |
521 | |
522 /// The source of this dependency's package. | |
523 Source get source { | |
524 var canonical = _canonicalRef(); | |
525 if (canonical == null) return null; | |
526 return canonical.source; | |
527 } | |
528 | |
529 /// The description of this dependency's package. | |
530 get description { | |
531 var canonical = _canonicalRef(); | |
532 if (canonical == null) return null; | |
533 return canonical.description; | |
534 } | |
535 | |
536 /// Return the PackageRef that has the canonical source and description for | |
537 /// this package. If any dependency is on the root package, that will be used; | |
538 /// otherwise, it will be the source and description that all dependencies | |
539 /// agree upon. | |
540 PackageRef _canonicalRef() { | |
541 if (_refs.isEmpty) return null; | |
542 var refs = _refs.values; | |
543 for (var ref in refs) { | |
544 if (ref.isRoot) return ref; | |
545 } | |
546 return refs.first; | |
547 } | |
548 | |
549 Dependency(this.name) | |
550 : _refs = <String, PackageRef>{}; | |
551 | |
552 Dependency._clone(Dependency other) | |
553 : name = other.name, | |
554 version = other.version, | |
555 _refs = new Map<String, PackageRef>.from(other._refs); | |
556 | |
557 /// Creates a copy of this dependency. | |
558 Dependency clone() => new Dependency._clone(this); | |
559 | |
560 /// Return a list of available versions for this dependency. | |
561 Future<List<Version>> getVersions() => source.getVersions(name, description); | |
562 | |
563 /// Places [ref] as a constraint from [package] onto this. | |
564 void placeConstraint(String package, PackageRef ref) { | |
565 var requiredDepender = _requiredDepender(); | |
566 if (requiredDepender != null) { | |
567 var required = _refs[requiredDepender]; | |
568 if (required.source.name != ref.source.name) { | |
569 throw new SourceMismatchException(name, | |
570 requiredDepender, required.source, package, ref.source); | |
571 } else if (!required.source.descriptionsEqual( | |
572 required.description, ref.description)) { | |
573 throw new DescriptionMismatchException(name, | |
574 requiredDepender, required.description, package, ref.description); | |
575 } | |
576 } | 199 } |
577 | 200 |
578 _refs[package] = ref; | 201 var names = map.keys.toList(); |
202 names.sort(); | |
203 | |
204 for (var name in names) { | |
205 buffer.writeln("- '$name' depends on version ${map[name].constraint}"); | |
206 } | |
579 } | 207 } |
208 } | |
580 | 209 |
581 /// Returns the name of a package whose constraint source and description | 210 /// Exception thrown when the [VersionSolver] fails to find a solution after a |
582 /// all other constraints must match. Returns null if there are no | 211 /// certain number of iterations. |
583 /// requirements on new constraints. | 212 class CouldNotSolveException extends SolverFailure { |
584 String _requiredDepender() { | 213 CouldNotSolveException(); |
585 if (_refs.isEmpty) return null; | |
586 | 214 |
587 var dependers = _refs.keys.toList(); | 215 String toString() => |
588 if (dependers.length == 1) { | 216 "Could not find a solution that met all version constraints."; |
589 var depender = dependers[0]; | |
590 if (_refs[depender].isRoot) return null; | |
591 return depender; | |
592 } | |
593 | |
594 return dependers[1]; | |
595 } | |
596 | |
597 /// Removes the constraint from [package] onto this. | |
598 PackageRef removeConstraint(String package) => _refs.remove(package); | |
599 } | 217 } |
600 | 218 |
601 /// Exception thrown when the [VersionConstraint] used to match a package is | 219 /// Exception thrown when the [VersionConstraint] used to match a package is |
602 /// valid (i.e. non-empty), but there are no released versions of the package | 220 /// valid (i.e. non-empty), but there are no available versions of the package |
603 /// that fit that constraint. | 221 /// that fit that constraint. |
604 class NoVersionException implements Exception { | 222 class NoVersionException extends SolverFailure { |
605 final String package; | 223 final String package; |
606 final VersionConstraint constraint; | 224 final VersionConstraint constraint; |
607 final Map<String, PackageRef> _dependencies; | 225 final List<Dependency> _dependencies; |
608 | 226 |
609 NoVersionException(this.package, this.constraint, this._dependencies); | 227 NoVersionException(this.package, this.constraint, this._dependencies); |
610 | 228 |
611 String toString() { | 229 String toString() { |
612 var buffer = new StringBuffer(); | 230 var buffer = new StringBuffer(); |
613 buffer.write("Package '$package' has no versions that match $constraint " | 231 buffer.write("Package '$package' has no versions that match $constraint " |
614 "derived from:\n"); | 232 "derived from:\n"); |
615 | 233 _writeDependencies(buffer, _dependencies); |
616 var keys = new List.from(_dependencies.keys); | |
617 keys.sort(); | |
618 | |
619 for (var key in keys) { | |
620 buffer.write("- '$key' depends on version " | |
621 "${_dependencies[key].constraint}\n"); | |
622 } | |
623 | |
624 return buffer.toString(); | 234 return buffer.toString(); |
625 } | 235 } |
626 } | 236 } |
627 | 237 |
628 // TODO(rnystrom): Report the list of depending packages and their constraints. | 238 // TODO(rnystrom): Report the list of depending packages and their constraints. |
629 /// Exception thrown when the most recent version of [package] must be selected, | 239 /// Exception thrown when the most recent version of [package] must be selected, |
630 /// but doesn't match the [VersionConstraint] imposed on the package. | 240 /// but doesn't match the [VersionConstraint] imposed on the package. |
631 class CouldNotUpdateException implements Exception { | 241 class CouldNotUpdateException extends SolverFailure { |
632 final String package; | 242 final String package; |
633 final VersionConstraint constraint; | 243 final VersionConstraint constraint; |
634 final Version best; | 244 final Version best; |
635 | 245 |
636 CouldNotUpdateException(this.package, this.constraint, this.best); | 246 CouldNotUpdateException(this.package, this.constraint, this.best); |
637 | 247 |
638 String toString() => | 248 String toString() => |
639 "The latest version of '$package', $best, does not match $constraint."; | 249 "The latest version of '$package', $best, does not match $constraint."; |
640 } | 250 } |
641 | 251 |
642 /// Exception thrown when the [VersionConstraint] used to match a package is | 252 /// Exception thrown when the [VersionConstraint] used to match a package is |
643 /// the empty set: in other words, multiple packages depend on it and have | 253 /// the empty set: in other words, multiple packages depend on it and have |
644 /// conflicting constraints that have no overlap. | 254 /// conflicting constraints that have no overlap. |
645 class DisjointConstraintException implements Exception { | 255 class DisjointConstraintException extends SolverFailure { |
646 final String package; | 256 final String package; |
647 final Map<String, PackageRef> _dependencies; | 257 final List<Dependency> _dependencies; |
648 | 258 |
649 DisjointConstraintException(this.package, this._dependencies); | 259 DisjointConstraintException(this.package, this._dependencies); |
650 | 260 |
651 String toString() { | 261 String toString() { |
652 var buffer = new StringBuffer(); | 262 var buffer = new StringBuffer(); |
653 buffer.write("Incompatible version constraints on '$package':\n"); | 263 buffer.write("Incompatible version constraints on '$package':\n"); |
654 | 264 _writeDependencies(buffer, _dependencies); |
655 var keys = new List.from(_dependencies.keys); | |
656 keys.sort(); | |
657 | |
658 for (var key in keys) { | |
659 buffer.write("- '$key' depends on version " | |
660 "${_dependencies[key].constraint}\n"); | |
661 } | |
662 | |
663 return buffer.toString(); | 265 return buffer.toString(); |
664 } | 266 } |
665 } | 267 } |
666 | 268 |
667 /// Exception thrown when the [VersionSolver] fails to find a solution after a | |
668 /// certain number of iterations. | |
669 class CouldNotSolveException implements Exception { | |
670 CouldNotSolveException(); | |
671 | |
672 String toString() => | |
673 "Could not find a solution that met all version constraints."; | |
674 } | |
675 | |
676 /// Exception thrown when two packages with the same name but different sources | 269 /// Exception thrown when two packages with the same name but different sources |
677 /// are depended upon. | 270 /// are depended upon. |
678 class SourceMismatchException implements Exception { | 271 class SourceMismatchException extends SolverFailure { |
679 final String package; | 272 final String package; |
680 final String depender1; | 273 final String depender1; |
681 final Source source1; | 274 final Source source1; |
682 final String depender2; | 275 final String depender2; |
683 final Source source2; | 276 final Source source2; |
684 | 277 |
685 SourceMismatchException(this.package, this.depender1, this.source1, | 278 SourceMismatchException(this.package, this.depender1, this.source1, |
686 this.depender2, this.source2); | 279 this.depender2, this.source2); |
687 | 280 |
688 String toString() { | 281 String toString() { |
689 return "Incompatible dependencies on '$package':\n" | 282 return "Incompatible dependencies on '$package':\n" |
690 "- '$depender1' depends on it from source '$source1'\n" | 283 "- '$depender1' depends on it from source '$source1'\n" |
691 "- '$depender2' depends on it from source '$source2'"; | 284 "- '$depender2' depends on it from source '$source2'"; |
692 } | 285 } |
693 } | 286 } |
694 | 287 |
695 /// Exception thrown when two packages with the same name and source but | 288 /// Exception thrown when two packages with the same name and source but |
696 /// different descriptions are depended upon. | 289 /// different descriptions are depended upon. |
697 class DescriptionMismatchException implements Exception { | 290 class DescriptionMismatchException extends SolverFailure { |
698 final String package; | 291 final String package; |
699 final String depender1; | 292 final String depender1; |
700 final description1; | 293 final description1; |
701 final String depender2; | 294 final String depender2; |
702 final description2; | 295 final description2; |
703 | 296 |
704 DescriptionMismatchException(this.package, this.depender1, this.description1, | 297 DescriptionMismatchException(this.package, this.depender1, this.description1, |
705 this.depender2, this.description2); | 298 this.depender2, this.description2); |
706 | 299 |
707 String toString() { | 300 String toString() { |
708 // TODO(nweiz): Dump descriptions to YAML when that's supported. | 301 // TODO(nweiz): Dump descriptions to YAML when that's supported. |
709 return "Incompatible dependencies on '$package':\n" | 302 return "Incompatible dependencies on '$package':\n" |
710 "- '$depender1' depends on it with description " | 303 "- '$depender1' depends on it with description " |
711 "${json.stringify(description1)}\n" | 304 "${json.stringify(description1)}\n" |
712 "- '$depender2' depends on it with description " | 305 "- '$depender2' depends on it with description " |
713 "${json.stringify(description2)}"; | 306 "${json.stringify(description2)}"; |
714 } | 307 } |
715 } | 308 } |
OLD | NEW |