Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(111)

Side by Side Diff: utils/pub/version_solver.dart

Issue 13095015: Use backtracking when solving dependency constraints. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Allow both solvers to coexist. Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698