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

Side by Side Diff: utils/pub/solver/greedy_solver.dart

Issue 14232023: Switch to backtracking solver. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: 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
« no previous file with comments | « utils/pub/solver/backtracking_solver.dart ('k') | utils/pub/solver/version_solver.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 /// 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_solver1;
37
38 import 'dart:async';
39 import 'dart:collection' show Queue;
40 import 'dart:math' as math;
41
42 import '../lock_file.dart';
43 import '../log.dart' as log;
44 import '../package.dart';
45 import '../source.dart';
46 import '../source_registry.dart';
47 import '../version.dart';
48 import 'version_solver.dart';
49
50 class GreedyVersionSolver extends VersionSolver {
51 final _packages = <String, DependencyNode>{};
52 final _work = new Queue<WorkItem>();
53 int _numIterations = 0;
54
55 GreedyVersionSolver(SourceRegistry sources, Package root, LockFile lockFile,
56 List<String> useLatest)
57 : super(sources, root, lockFile, useLatest);
58
59 /// The non-backtracking solver always only tries one solution.
60 int get attemptedSolutions => 1;
61
62 void forceLatestVersion(String package) {
63 // TODO(nweiz): How do we want to detect and handle unknown dependencies
64 // here?
65 getDependency(package).useLatestVersion = true;
66 }
67
68 Future<List<PackageId>> runSolver() {
69 // Kick off the work by adding the root package at its concrete version to
70 // the dependency graph.
71 enqueue(new AddConstraint('(entrypoint)', new PackageRef.root(root)));
72
73 Future processNextWorkItem(_) {
74 while (true) {
75 // Stop if we are done.
76 if (_work.isEmpty) return new Future.value(buildResults());
77
78 // If we appear to be stuck in a loop, then we probably have an unstable
79 // graph, bail. We guess this based on a rough heuristic that it should
80 // only take a certain number of steps to solve a graph with a given
81 // number of connections.
82 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune
83 // when we have a better picture of real-world package topologies.
84 _numIterations++;
85 if (_numIterations > math.max(50, _packages.length * 5)) {
86 throw new CouldNotSolveException();
87 }
88
89 // Run the first work item.
90 var future = _work.removeFirst().process(this);
91
92 // If we have an async operation to perform, chain the loop to resume
93 // when it's done. Otherwise, just loop synchronously.
94 if (future != null) {
95 return future.then(processNextWorkItem);
96 }
97 }
98 }
99
100 return processNextWorkItem(null);
101 }
102
103 void enqueue(WorkItem work) {
104 _work.add(work);
105 }
106
107 DependencyNode getDependency(String package) {
108 // There can be unused dependencies in the graph, so just create an empty
109 // one if needed.
110 _packages.putIfAbsent(package, () => new DependencyNode(package));
111 return _packages[package];
112 }
113
114 /// Sets the best selected version of [package] to [version].
115 void setVersion(String package, Version version) {
116 _packages[package].version = version;
117 }
118
119 /// Returns the most recent version of [dependency] that satisfies all of its
120 /// version constraints.
121 Future<Version> getBestVersion(DependencyNode dependency) {
122 return cache.getVersions(dependency.name,
123 dependency.source, dependency.description).then((versions) {
124 var best = null;
125 for (var ref in versions) {
126 if (dependency.useLatestVersion ||
127 dependency.constraint.allows(ref.version)) {
128 if (best == null || ref.version > best) best = ref.version;
129 }
130 }
131
132 // TODO(rnystrom): Better exception.
133 if (best == null) {
134 if (tryUnlockDepender(dependency)) return null;
135 throw new NoVersionException(dependency.name, dependency.constraint,
136 dependency.toList());
137 } else if (!dependency.constraint.allows(best)) {
138 if (tryUnlockDepender(dependency)) return null;
139 throw new CouldNotUpdateException(
140 dependency.name, dependency.constraint, best);
141 }
142
143 return best;
144 });
145 }
146
147 /// Looks for a package that depends (transitively) on [dependency] and has
148 /// its version locked in the lockfile. If one is found, enqueues an
149 /// [UnlockPackage] work item for it and returns true. Otherwise, returns
150 /// false.
151 ///
152 /// This does a breadth-first search; immediate dependers will be unlocked
153 /// first, followed by transitive dependers.
154 bool tryUnlockDepender(DependencyNode dependency, [Set<String> seen]) {
155 if (seen == null) seen = new Set();
156 // Avoid an infinite loop if there are circular dependencies.
157 if (seen.contains(dependency.name)) return false;
158 seen.add(dependency.name);
159
160 for (var dependerName in dependency.dependers) {
161 var depender = getDependency(dependerName);
162 var locked = lockFile.packages[dependerName];
163 if (locked != null && depender.version == locked.version &&
164 depender.source.name == locked.source.name) {
165 enqueue(new UnlockPackage(depender));
166 return true;
167 }
168 }
169
170 return dependency.dependers.map(getDependency).any((subdependency) =>
171 tryUnlockDepender(subdependency, seen));
172 }
173
174 List<PackageId> buildResults() {
175 return _packages.values
176 .where((dep) => dep.isDependedOn)
177 .map(_dependencyToPackageId)
178 .toList();
179 }
180
181 PackageId _dependencyToPackageId(DependencyNode dep) {
182 var description = dep.description;
183
184 // If the lockfile contains a fully-resolved description for the package,
185 // use that. This allows e.g. Git to ensure that the same commit is used.
186 var lockedPackage = lockFile.packages[dep.name];
187 if (lockedPackage != null && lockedPackage.version == dep.version &&
188 lockedPackage.source.name == dep.source.name &&
189 dep.source.descriptionsEqual(
190 description, lockedPackage.description)) {
191 description = lockedPackage.description;
192 }
193
194 return new PackageId(dep.name, dep.source, dep.version, description);
195 }
196 }
197
198 /// The constraint solver works by iteratively processing a queue of work items.
199 /// Each item is a single atomic change to the dependency graph. Handling them
200 /// in a queue lets us handle asynchrony (resolving versions requires
201 /// information from servers) as well as avoid deeply nested recursion.
202 abstract class WorkItem {
203 /// Processes this work item. Returns a future that completes when the work is
204 /// done. If `null` is returned, that means the work has completed
205 /// synchronously and the next item can be started immediately.
206 Future process(GreedyVersionSolver solver);
207 }
208
209 /// The best selected version for a package has changed to [version]. If the
210 /// previous version of the package is `null`, that means the package is being
211 /// added to the graph. If [version] is `null`, it is being removed.
212 class ChangeVersion implements WorkItem {
213 /// The name of the package whose version is being changed.
214 final String package;
215
216 /// The source of the package whose version is changing.
217 final Source source;
218
219 /// The description identifying the package whose version is changing.
220 final description;
221
222 /// The new selected version.
223 final Version version;
224
225 ChangeVersion(this.package, this.source, this.description, this.version);
226
227 Future process(GreedyVersionSolver solver) {
228 log.fine("Changing $package to version $version.");
229
230 var dependency = solver.getDependency(package);
231 var oldVersion = dependency.version;
232 solver.setVersion(package, version);
233
234 // The dependencies between the old and new version may be different. Walk
235 // them both and update any constraints that differ between the two.
236 return Future.wait([
237 getDependencyRefs(solver, oldVersion),
238 getDependencyRefs(solver, version)]).then((list) {
239 var oldDependencyRefs = list[0];
240 var newDependencyRefs = list[1];
241
242 for (var oldRef in oldDependencyRefs.values) {
243 if (newDependencyRefs.containsKey(oldRef.name)) {
244 // The dependency is in both versions of this package, but its
245 // constraint may have changed.
246 var newRef = newDependencyRefs.remove(oldRef.name);
247 solver.enqueue(new AddConstraint(package, newRef));
248 } else {
249 // The dependency is not in the new version of the package, so just
250 // remove its constraint.
251 solver.enqueue(new RemoveConstraint(package, oldRef.name));
252 }
253 }
254
255 // Everything that's left is a depdendency that's only in the new
256 // version of the package.
257 for (var newRef in newDependencyRefs.values) {
258 solver.enqueue(new AddConstraint(package, newRef));
259 }
260 });
261 }
262
263 /// Get the dependencies at [version] of the package being changed.
264 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver,
265 Version version) {
266 // If there is no version, it means no package, so no dependencies.
267 if (version == null) {
268 return new Future<Map<String, PackageRef>>.value(<String, PackageRef>{});
269 }
270
271 var id = new PackageId(package, source, version, description);
272 return solver.cache.getPubspec(id).then((pubspec) {
273 var dependencies = <String, PackageRef>{};
274 for (var dependency in pubspec.dependencies) {
275 dependencies[dependency.name] = dependency;
276 }
277
278 // Include dev dependencies only from the root package.
279 if (id.isRoot) {
280 for (var dependency in pubspec.devDependencies) {
281 dependencies[dependency.name] = dependency;
282 }
283 }
284
285 return dependencies;
286 });
287 }
288 }
289
290 /// A constraint that a depending package places on a dependent package has
291 /// changed.
292 ///
293 /// This is an abstract class that contains logic for updating the dependency
294 /// graph once a dependency has changed. Changing the dependency is the
295 /// responsibility of subclasses.
296 abstract class ChangeConstraint implements WorkItem {
297 Future process(GreedyVersionSolver solver);
298
299 void undo(GreedyVersionSolver solver);
300
301 Future _processChange(GreedyVersionSolver solver,
302 DependencyNode oldDependency,
303 DependencyNode newDependency) {
304 var name = newDependency.name;
305 var source = oldDependency.source != null ?
306 oldDependency.source : newDependency.source;
307 var description = oldDependency.description != null ?
308 oldDependency.description : newDependency.description;
309 var oldConstraint = oldDependency.constraint;
310 var newConstraint = newDependency.constraint;
311
312 // If the package is over-constrained, i.e. the packages depending have
313 // disjoint constraints, then try unlocking a depender that's locked by the
314 // lockfile. If there are no remaining locked dependencies, throw an error.
315 if (newConstraint != null && newConstraint.isEmpty) {
316 if (solver.tryUnlockDepender(newDependency)) {
317 undo(solver);
318 return null;
319 }
320
321 throw new DisjointConstraintException(name, newDependency.toList());
322 }
323
324 // If this constraint change didn't cause the overall constraint on the
325 // package to change, then we don't need to do any further work.
326 if (oldConstraint == newConstraint) return null;
327
328 // If the dependency has been cut free from the graph, just remove it.
329 if (!newDependency.isDependedOn) {
330 solver.enqueue(new ChangeVersion(name, source, description, null));
331 return null;
332 }
333
334 // If the dependency is on the root package, then we don't need to do
335 // anything since it's already at the best version.
336 if (name == solver.root.name) {
337 solver.enqueue(new ChangeVersion(
338 name, source, description, solver.root.version));
339 return null;
340 }
341
342 // If the dependency is on a package in the lockfile, use the lockfile's
343 // version for that package if it's valid given the other constraints.
344 var lockedPackage = solver.lockFile.packages[name];
345 if (lockedPackage != null && newDependency.source == lockedPackage.source) {
346 var lockedVersion = lockedPackage.version;
347 if (newConstraint.allows(lockedVersion)) {
348 solver.enqueue(
349 new ChangeVersion(name, source, description, lockedVersion));
350 return null;
351 }
352 }
353
354 // The constraint has changed, so see what the best version of the package
355 // that meets the new constraint is.
356 return solver.getBestVersion(newDependency).then((best) {
357 if (best == null) {
358 undo(solver);
359 } else if (newDependency.version != best) {
360 solver.enqueue(new ChangeVersion(name, source, description, best));
361 }
362 });
363 }
364 }
365
366 /// The constraint given by [ref] is being placed by [depender].
367 class AddConstraint extends ChangeConstraint {
368 /// The package that has the dependency.
369 final String depender;
370
371 /// The package being depended on and the constraints being placed on it. The
372 /// source, version, and description in this ref are all considered
373 /// constraints on the dependent package.
374 final PackageRef ref;
375
376 AddConstraint(this.depender, this.ref);
377
378 Future process(GreedyVersionSolver solver) {
379 log.fine("Adding $depender's constraint $ref.");
380
381 var dependency = solver.getDependency(ref.name);
382 var oldDependency = dependency.clone();
383 dependency.placeConstraint(depender, ref);
384 return _processChange(solver, oldDependency, dependency);
385 }
386
387 void undo(GreedyVersionSolver solver) {
388 solver.getDependency(ref.name).removeConstraint(depender);
389 }
390 }
391
392 /// [depender] is no longer placing a constraint on [dependent].
393 class RemoveConstraint extends ChangeConstraint {
394 /// The package that was placing a constraint on [dependent].
395 String depender;
396
397 /// The package that was being depended on.
398 String dependent;
399
400 /// The constraint that was removed.
401 PackageRef _removed;
402
403 RemoveConstraint(this.depender, this.dependent);
404
405 Future process(GreedyVersionSolver solver) {
406 log.fine("Removing $depender's constraint ($_removed) on $dependent.");
407
408 var dependency = solver.getDependency(dependent);
409 var oldDependency = dependency.clone();
410 _removed = dependency.removeConstraint(depender);
411 return _processChange(solver, oldDependency, dependency);
412 }
413
414 void undo(GreedyVersionSolver solver) {
415 solver.getDependency(dependent).placeConstraint(depender, _removed);
416 }
417 }
418
419 /// [package]'s version is no longer constrained by the lockfile.
420 class UnlockPackage implements WorkItem {
421 /// The package being unlocked.
422 DependencyNode package;
423
424 UnlockPackage(this.package);
425
426 Future process(GreedyVersionSolver solver) {
427 log.fine("Unlocking ${package.name}.");
428
429 solver.lockFile.packages.remove(package.name);
430 return solver.getBestVersion(package).then((best) {
431 if (best == null) return null;
432 solver.enqueue(new ChangeVersion(
433 package.name, package.source, package.description, best));
434 });
435 }
436 }
437
438 /// Describes one [Package] in the [DependencyGraph] and keeps track of which
439 /// packages depend on it and what constraints they place on it.
440 class DependencyNode {
441 /// The name of the this dependency's package.
442 final String name;
443
444 /// The [PackageRefs] that represent constraints that depending packages have
445 /// placed on this one.
446 final Map<String, PackageRef> _refs;
447
448 /// The currently-selected best version for this dependency.
449 Version version;
450
451 /// Whether this dependency should always select the latest version.
452 bool useLatestVersion = false;
453
454 /// Gets whether or not any other packages are currently depending on this
455 /// one. If `false`, then it means this package is not part of the dependency
456 /// graph and should be omitted.
457 bool get isDependedOn => !_refs.isEmpty;
458
459 /// The names of all the packages that depend on this dependency.
460 Iterable<String> get dependers => _refs.keys;
461
462 /// Gets the overall constraint that all packages are placing on this one.
463 /// If no packages have a constraint on this one (which can happen when this
464 /// package is in the process of being added to the graph), returns `null`.
465 VersionConstraint get constraint {
466 if (_refs.isEmpty) return null;
467 return new VersionConstraint.intersection(
468 _refs.values.map((ref) => ref.constraint));
469 }
470
471 /// The source of this dependency's package.
472 Source get source {
473 var canonical = _canonicalRef();
474 if (canonical == null) return null;
475 return canonical.source;
476 }
477
478 /// The description of this dependency's package.
479 get description {
480 var canonical = _canonicalRef();
481 if (canonical == null) return null;
482 return canonical.description;
483 }
484
485 /// Return the PackageRef that has the canonical source and description for
486 /// this package. If any dependency is on the root package, that will be used;
487 /// otherwise, it will be the source and description that all dependencies
488 /// agree upon.
489 PackageRef _canonicalRef() {
490 if (_refs.isEmpty) return null;
491 var refs = _refs.values;
492 for (var ref in refs) {
493 if (ref.isRoot) return ref;
494 }
495 return refs.first;
496 }
497
498 DependencyNode(this.name)
499 : _refs = <String, PackageRef>{};
500
501 DependencyNode._clone(DependencyNode other)
502 : name = other.name,
503 version = other.version,
504 _refs = new Map<String, PackageRef>.from(other._refs);
505
506 /// Creates a copy of this dependency.
507 DependencyNode clone() => new DependencyNode._clone(this);
508
509 /// Places [ref] as a constraint from [package] onto this.
510 void placeConstraint(String package, PackageRef ref) {
511 var requiredDepender = _requiredDepender();
512 if (requiredDepender != null) {
513 var required = _refs[requiredDepender];
514 if (required.source.name != ref.source.name) {
515 throw new SourceMismatchException(name, [
516 new Dependency(requiredDepender, required),
517 new Dependency(package, ref)]);
518 } else if (!required.descriptionEquals(ref)) {
519 throw new DescriptionMismatchException(name, [
520 new Dependency(requiredDepender, required),
521 new Dependency(package, ref)]);
522 }
523 }
524
525 _refs[package] = ref;
526 }
527
528 /// Returns the name of a package whose constraint source and description
529 /// all other constraints must match. Returns null if there are no
530 /// requirements on new constraints.
531 String _requiredDepender() {
532 if (_refs.isEmpty) return null;
533
534 var dependers = _refs.keys.toList();
535 if (dependers.length == 1) {
536 var depender = dependers[0];
537 if (_refs[depender].isRoot) return null;
538 return depender;
539 }
540
541 return dependers[1];
542 }
543
544 /// Removes the constraint from [package] onto this.
545 PackageRef removeConstraint(String package) => _refs.remove(package);
546
547 /// Converts this to a list of [Dependency] objects like the error types
548 /// expect.
549 List<Dependency> toList() {
550 var result = <Dependency>[];
551 _refs.forEach((name, ref) {
552 result.add(new Dependency(name, ref));
553 });
554 return result;
555 }
556 }
OLDNEW
« no previous file with comments | « utils/pub/solver/backtracking_solver.dart ('k') | utils/pub/solver/version_solver.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698