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

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

Issue 10854152: Allow `pub install` and `pub update` to update dependers. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Minor changes Created 8 years, 4 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 /** 5 /**
6 * Attempts to resolve a set of version constraints for a package dependency 6 * Attempts to resolve a set of version constraints for a package dependency
7 * graph and select an appropriate set of best specific versions for all 7 * graph and select an appropriate set of best specific versions for all
8 * dependent packages. It works iteratively and tries to reach a stable 8 * dependent packages. It works iteratively and tries to reach a stable
9 * solution where the constraints of all dependencies are met. If it fails to 9 * solution where the constraints of all dependencies are met. If it fails to
10 * reach a solution after a certain number of iterations, it assumes the 10 * reach a solution after a certain number of iterations, it assumes the
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
69 final Map<String, Dependency> _packages; 69 final Map<String, Dependency> _packages;
70 final Queue<WorkItem> _work; 70 final Queue<WorkItem> _work;
71 int _numIterations = 0; 71 int _numIterations = 0;
72 72
73 VersionSolver(SourceRegistry sources, this._root, this.lockFile) 73 VersionSolver(SourceRegistry sources, this._root, this.lockFile)
74 : _sources = sources, 74 : _sources = sources,
75 _pubspecs = new PubspecCache(sources), 75 _pubspecs = new PubspecCache(sources),
76 _packages = <Dependency>{}, 76 _packages = <Dependency>{},
77 _work = new Queue<WorkItem>(); 77 _work = new Queue<WorkItem>();
78 78
79 /**
80 * Tell the version solver to use the most recent version of [package] that
81 * exists in whatever source it's installed from. If that version violates
82 * constraints imposed by other dependencies, an error will be raised when
83 * solving the versions, even if an earlier compatible version exists.
Bob Nystrom 2012/08/15 17:48:16 Nice comment. :)
84 */
85 void useBestVersion(String package) {
Bob Nystrom 2012/08/15 17:48:16 "useLatestVersion" Up to this point, "best" alway
nweiz 2012/08/15 21:25:42 Done.
86 // TODO(nweiz): How do we want to detect and handle unknown dependencies
87 // here?
88 getDependency(package).useBestVersion = true;
89 lockFile.packages.remove(package);
90 }
91
79 Future<List<PackageId>> solve() { 92 Future<List<PackageId>> solve() {
80 // Kick off the work by adding the root package at its concrete version to 93 // Kick off the work by adding the root package at its concrete version to
81 // the dependency graph. 94 // the dependency graph.
82 var ref = new PackageRef(new RootSource(_root), _root.version, _root.name); 95 var ref = new PackageRef(new RootSource(_root), _root.version, _root.name);
83 enqueue(new AddConstraint('(entrypoint)', ref)); 96 enqueue(new AddConstraint('(entrypoint)', ref));
84 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); 97 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec);
85 98
86 Future processNextWorkItem(_) { 99 Future processNextWorkItem(_) {
87 while (true) { 100 while (true) {
88 // Stop if we are done. 101 // Stop if we are done.
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
124 return _packages[package]; 137 return _packages[package];
125 } 138 }
126 139
127 /** 140 /**
128 * Sets the best selected version of [package] to [version]. 141 * Sets the best selected version of [package] to [version].
129 */ 142 */
130 void setVersion(String package, Version version) { 143 void setVersion(String package, Version version) {
131 _packages[package].version = version; 144 _packages[package].version = version;
132 } 145 }
133 146
147 /**
148 * Returns the most recent version of [dependency] that satisfies all of its
149 * version constraints.
150 */
151 Future<Version> getBestVersion(Dependency dependency) {
152 return dependency.source.getVersions(dependency.description)
153 .transform((versions) {
154 var best = null;
155 for (var version in versions) {
156 if (dependency.useBestVersion ||
157 dependency.constraint.allows(version)) {
158 if (best == null || version > best) best = version;
159 }
160 }
161
162 // TODO(rnystrom): Better exception.
163 if (best == null) {
164 if (unlockDepender(dependency)) return null;
165 throw new NoVersionException(dependency.name, dependency.constraint);
166 } else if (!dependency.constraint.allows(best)) {
167 if (unlockDepender(dependency)) return null;
168 throw new CouldNotUpdateException(
169 dependency.name, dependency.constraint, best);
170 }
171
172 return best;
173 });
174 }
175
176 /**
177 * Looks for a package that depends (transitively) on [dependency] and has its
178 * version locked in the lockfile. If one is found, enqueues an
179 * [UnlockPackage] work item for it and returns true. Otherwise, returns
180 * false.
181 *
182 * This does a breadth-first search; immediate dependers will be unlocked
183 * first, followed by transitive dependers.
184 */
185 bool unlockDepender(Dependency dependency) {
Bob Nystrom 2012/08/15 17:48:16 "unlockDepender" -> "tryUnlockDepender"
nweiz 2012/08/15 21:25:42 Done.
186 for (var dependerName in dependency.dependers) {
187 var depender = getDependency(dependerName);
188 var locked = lockFile.packages[dependerName];
189 if (locked != null && depender.version == locked.version) {
190 enqueue(new UnlockPackage(depender));
191 return true;
192 }
193 }
194 return dependency.dependers.map(getDependency).some(unlockDepender);
195 }
196
134 List<PackageId> buildResults() { 197 List<PackageId> buildResults() {
135 return _packages.getValues().filter((dep) => dep.isDependedOn).map((dep) { 198 return _packages.getValues().filter((dep) => dep.isDependedOn).map((dep) {
136 var description = dep.description; 199 var description = dep.description;
137 200
138 // If the lockfile contains a fully-resolved description for the package, 201 // If the lockfile contains a fully-resolved description for the package,
139 // use that. This allows e.g. Git to ensure that the same commit is used. 202 // use that. This allows e.g. Git to ensure that the same commit is used.
140 var lockedPackage = lockFile.packages[dep.name]; 203 var lockedPackage = lockFile.packages[dep.name];
141 if (lockedPackage != null && lockedPackage.version == dep.version && 204 if (lockedPackage != null && lockedPackage.version == dep.version &&
142 lockedPackage.source.name == dep.source.name && 205 lockedPackage.source.name == dep.source.name &&
143 dep.source.descriptionsEqual( 206 dep.source.descriptionsEqual(
(...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after
254 * A constraint that a depending package places on a dependent package has 317 * A constraint that a depending package places on a dependent package has
255 * changed. 318 * changed.
256 * 319 *
257 * This is an abstract class that contains logic for updating the dependency 320 * This is an abstract class that contains logic for updating the dependency
258 * graph once a dependency has changed. Changing the dependency is the 321 * graph once a dependency has changed. Changing the dependency is the
259 * responsibility of subclasses. 322 * responsibility of subclasses.
260 */ 323 */
261 class ChangeConstraint implements WorkItem { 324 class ChangeConstraint implements WorkItem {
262 abstract Future process(VersionSolver solver); 325 abstract Future process(VersionSolver solver);
263 326
264 Future _processChange(VersionSolver solver, Source source, description, 327 abstract void undo(VersionSolver solver);
265 Dependency dependency, VersionConstraint oldConstraint, 328
266 VersionConstraint newConstraint) { 329 Future _processChange(VersionSolver solver, Dependency dependency,
330 VersionConstraint oldConstraint) {
267 var name = dependency.name; 331 var name = dependency.name;
332 var description = dependency.description;
333 var source = dependency.source;
334 var newConstraint = dependency.constraint;
268 335
269 // If the package is over-constrained, i.e. the packages depending have 336 // If the package is over-constrained, i.e. the packages depending have
270 // disjoint constraints, then stop. 337 // disjoint constraints, then try unlocking a depender that's locked by the
338 // lockfile. If there are no remaining locked dependencies, throw an error.
271 if (newConstraint != null && newConstraint.isEmpty) { 339 if (newConstraint != null && newConstraint.isEmpty) {
340 if (solver.unlockDepender(dependency)) {
341 undo(solver);
342 return null;
343 }
344
272 throw new DisjointConstraintException(name); 345 throw new DisjointConstraintException(name);
273 } 346 }
274 347
275 // If this constraint change didn't cause the overall constraint on the 348 // If this constraint change didn't cause the overall constraint on the
276 // package to change, then we don't need to do any further work. 349 // package to change, then we don't need to do any further work.
277 if (oldConstraint == newConstraint) return null; 350 if (oldConstraint == newConstraint) return null;
278 351
279 // If the dependency has been cut free from the graph, just remove it. 352 // If the dependency has been cut free from the graph, just remove it.
280 if (!dependency.isDependedOn) { 353 if (!dependency.isDependedOn) {
281 solver.enqueue(new ChangeVersion(source, description, null)); 354 solver.enqueue(new ChangeVersion(source, description, null));
(...skipping 14 matching lines...) Expand all
296 if (lockedPackage != null) { 369 if (lockedPackage != null) {
297 var lockedVersion = lockedPackage.version; 370 var lockedVersion = lockedPackage.version;
298 if (newConstraint.allows(lockedVersion)) { 371 if (newConstraint.allows(lockedVersion)) {
299 solver.enqueue(new ChangeVersion(source, description, lockedVersion)); 372 solver.enqueue(new ChangeVersion(source, description, lockedVersion));
300 return null; 373 return null;
301 } 374 }
302 } 375 }
303 376
304 // The constraint has changed, so see what the best version of the package 377 // The constraint has changed, so see what the best version of the package
305 // that meets the new constraint is. 378 // that meets the new constraint is.
306 return source.getVersions(description).transform((versions) { 379 return solver.getBestVersion(dependency).transform((best) {
307 var best = null; 380 if (best == null) {
308 for (var version in versions) { 381 undo(solver);
309 if (newConstraint.allows(version)) { 382 } else if (dependency.version != best) {
310 if (best == null || version > best) best = version;
311 }
312 }
313
314 // TODO(rnystrom): Better exception.
315 if (best == null) throw new NoVersionException(name, newConstraint);
316
317 if (dependency.version != best) {
318 solver.enqueue(new ChangeVersion(source, description, best)); 383 solver.enqueue(new ChangeVersion(source, description, best));
Bob Nystrom 2012/08/15 17:48:16 Here, we check that the version has actually chang
nweiz 2012/08/15 21:25:42 I didn't go that direction for code complexity iss
319 } 384 }
320 }); 385 });
321 } 386 }
322 } 387 }
323 388
324 /** 389 /**
325 * The constraint given by [ref] is being placed by [depender]. 390 * The constraint given by [ref] is being placed by [depender].
326 */ 391 */
327 class AddConstraint extends ChangeConstraint { 392 class AddConstraint extends ChangeConstraint {
328 /** 393 /**
329 * The package that has the dependency. 394 * The package that has the dependency.
330 */ 395 */
331 final String depender; 396 final String depender;
332 397
333 /** 398 /**
334 * The package being depended on and the constraints being placed on it. The 399 * The package being depended on and the constraints being placed on it. The
335 * source, version, and description in this ref are all considered constraints 400 * source, version, and description in this ref are all considered constraints
336 * on the dependent package. 401 * on the dependent package.
337 */ 402 */
338 final PackageRef ref; 403 final PackageRef ref;
339 404
340 AddConstraint(this.depender, this.ref); 405 AddConstraint(this.depender, this.ref);
341 406
342 Future process(VersionSolver solver) { 407 Future process(VersionSolver solver) {
343 var dependency = solver.getDependency(ref.name); 408 var dependency = solver.getDependency(ref.name);
344 var oldConstraint = dependency.constraint; 409 var oldConstraint = dependency.constraint;
345 dependency.placeConstraint(depender, ref); 410 dependency.placeConstraint(depender, ref);
346 var newConstraint = dependency.constraint; 411 return _processChange(solver, dependency, oldConstraint);
347 return _processChange(solver, ref.source, ref.description, dependency, 412 }
348 oldConstraint, newConstraint); 413
414 void undo(VersionSolver solver) {
415 solver.getDependency(ref.name).removeConstraint(depender);
349 } 416 }
350 } 417 }
351 418
352 /** 419 /**
353 * [depender] is no longer placing a constraint on [dependent]. 420 * [depender] is no longer placing a constraint on [dependent].
354 */ 421 */
355 class RemoveConstraint extends ChangeConstraint { 422 class RemoveConstraint extends ChangeConstraint {
356 /** 423 /**
357 * The package that was placing a constraint on [dependent]. 424 * The package that was placing a constraint on [dependent].
358 */ 425 */
359 String depender; 426 String depender;
360 427
361 /** 428 /**
362 * The package that was being depended on. 429 * The package that was being depended on.
363 */ 430 */
364 String dependent; 431 String dependent;
365 432
433 /** The constraint that was removed. */
434 PackageRef _removed;
435
366 RemoveConstraint(this.depender, this.dependent); 436 RemoveConstraint(this.depender, this.dependent);
367 437
368 Future process(VersionSolver solver) { 438 Future process(VersionSolver solver) {
369 var dependency = solver.getDependency(dependent); 439 var dependency = solver.getDependency(dependent);
370 var oldConstraint = dependency.constraint; 440 var oldConstraint = dependency.constraint;
371 var source = dependency.source; 441 var source = dependency.source;
372 var description = dependency.description; 442 _removed = dependency.removeConstraint(depender);
373 dependency.removeConstraint(depender); 443 return _processChange(solver, dependency, oldConstraint);
374 var newConstraint = dependency.constraint; 444 }
375 return _processChange(solver, source, description, dependency, 445
376 oldConstraint, newConstraint); 446 void undo() {
447 solver.getDependency(dependent).placeConstraint(depender, _removed);
377 } 448 }
378 } 449 }
379 450
451 /** [package]'s version is no longer constrained by the lockfile. */
452 class UnlockPackage implements WorkItem {
453 /** The package being unlocked. */
454 Dependency package;
455
456 UnlockPackage(this.package);
457
458 Future process(VersionSolver solver) {
459 solver.lockFile.packages.remove(package.name);
460 return solver.getBestVersion(package).transform((best) {
461 if (best == null) return null;
462 solver.enqueue(new ChangeVersion(
463 package.source, package.description, best));
464 });
465 }
466 }
467
380 // TODO(rnystrom): Instead of always pulling from the source (which will mean 468 // TODO(rnystrom): Instead of always pulling from the source (which will mean
381 // hitting a server), we should consider caching pubspecs of uninstalled 469 // hitting a server), we should consider caching pubspecs of uninstalled
382 // packages in the system cache. 470 // packages in the system cache.
383 /** 471 /**
384 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting 472 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting
385 * the same pubspec from the server repeatedly. 473 * the same pubspec from the server repeatedly.
386 */ 474 */
387 class PubspecCache { 475 class PubspecCache {
388 final SourceRegistry _sources; 476 final SourceRegistry _sources;
389 final Map<PackageId, Pubspec> _pubspecs; 477 final Map<PackageId, Pubspec> _pubspecs;
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
445 * according to [source]. 533 * according to [source].
446 */ 534 */
447 var description; 535 var description;
448 536
449 /** 537 /**
450 * The currently-selected best version for this dependency. 538 * The currently-selected best version for this dependency.
451 */ 539 */
452 Version version; 540 Version version;
453 541
454 /** 542 /**
543 * Whether this dependency should always select the best version available.
Bob Nystrom 2012/08/15 17:48:16 "best" -> "latest"
nweiz 2012/08/15 21:25:42 Done.
544 */
545 bool useBestVersion = false;
Bob Nystrom 2012/08/15 17:48:16 "useLatestVersion"
nweiz 2012/08/15 21:25:42 Done.
546
547 /**
455 * Gets whether or not any other packages are currently depending on this 548 * Gets whether or not any other packages are currently depending on this
456 * one. If `false`, then it means this package is not part of the dependency 549 * one. If `false`, then it means this package is not part of the dependency
457 * graph and should be omitted. 550 * graph and should be omitted.
458 */ 551 */
459 bool get isDependedOn() => !_refs.isEmpty(); 552 bool get isDependedOn() => !_refs.isEmpty();
460 553
554 /** A list of all the packages that depend on this dependency. */
Bob Nystrom 2012/08/15 17:48:16 "A list" -> "The names"
nweiz 2012/08/15 21:25:42 Done.
555 Collection<String> get dependers() => _refs.getKeys();
556
461 /** 557 /**
462 * Gets the overall constraint that all packages are placing on this one. 558 * 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 559 * 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`. 560 * package is in the process of being added to the graph), returns `null`.
465 */ 561 */
466 VersionConstraint get constraint() { 562 VersionConstraint get constraint() {
467 if (_refs.isEmpty()) return null; 563 if (_refs.isEmpty()) return null;
468 return new VersionConstraint.intersect( 564 return new VersionConstraint.intersect(
469 _refs.getValues().map((ref) => ref.constraint)); 565 _refs.getValues().map((ref) => ref.constraint));
470 } 566 }
(...skipping 16 matching lines...) Expand all
487 throw new DescriptionMismatchException( 583 throw new DescriptionMismatchException(
488 name, description, ref.description); 584 name, description, ref.description);
489 } 585 }
490 586
491 _refs[package] = ref; 587 _refs[package] = ref;
492 } 588 }
493 589
494 /** 590 /**
495 * Removes the constraint from [package] onto this. 591 * Removes the constraint from [package] onto this.
496 */ 592 */
497 void removeConstraint(String package) { 593 PackageRef removeConstraint(String package) {
498 _refs.remove(package); 594 var removed = _refs.remove(package);
499 595
500 if (_refs.isEmpty()) { 596 if (_refs.isEmpty()) {
501 source = null; 597 source = null;
502 description = null; 598 description = null;
503 } 599 }
600
601 return removed;
504 } 602 }
505 } 603 }
506 604
507 // TODO(rnystrom): Report the last of depending packages and their constraints. 605 // TODO(rnystrom): Report the last of depending packages and their constraints.
508 /** 606 /**
509 * Exception thrown when the [VersionConstraint] used to match a package is 607 * Exception thrown when the [VersionConstraint] used to match a package is
510 * valid (i.e. non-empty), but there are no released versions of the package 608 * valid (i.e. non-empty), but there are no released versions of the package
511 * that fit that constraint. 609 * that fit that constraint.
512 */ 610 */
513 class NoVersionException implements Exception { 611 class NoVersionException implements Exception {
514 final String package; 612 final String package;
515 final VersionConstraint constraint; 613 final VersionConstraint constraint;
516 614
517 NoVersionException(this.package, this.constraint); 615 NoVersionException(this.package, this.constraint);
518 616
519 String toString() => 617 String toString() =>
520 "Package '$package' has no versions that match $constraint."; 618 "Package '$package' has no versions that match $constraint.";
521 } 619 }
522 620
621 // TODO(rnystrom): Report the list of depending packages and their constraints.
622 /**
623 * Exception thrown when the most recent version of [package] must be selected,
624 * but doesn't match the [VersionConstraint] imposed on the package.
625 */
626 class CouldNotUpdateException implements Exception {
627 final String package;
628 final VersionConstraint constraint;
629 final Version best;
630
631 CouldNotUpdateException(this.package, this.constraint, this.best);
632
633 String toString() =>
634 "The last version of '$package', $best, does not match $constraint.";
Bob Nystrom 2012/08/15 17:48:16 "last" -> "latest"
nweiz 2012/08/15 21:25:42 Done.
635 }
636
523 // TODO(rnystrom): Report the last of depending packages and their constraints. 637 // TODO(rnystrom): Report the last of depending packages and their constraints.
524 /** 638 /**
525 * Exception thrown when the [VersionConstraint] used to match a package is 639 * Exception thrown when the [VersionConstraint] used to match a package is
526 * the empty set: in other words, multiple packages depend on it and have 640 * the empty set: in other words, multiple packages depend on it and have
527 * conflicting constraints that have no overlap. 641 * conflicting constraints that have no overlap.
528 */ 642 */
529 class DisjointConstraintException implements Exception { 643 class DisjointConstraintException implements Exception {
530 final String package; 644 final String package;
531 645
532 DisjointConstraintException(this.package); 646 DisjointConstraintException(this.package);
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
572 final description1; 686 final description1;
573 final description2; 687 final description2;
574 688
575 DescriptionMismatchException(this.package, this.description1, 689 DescriptionMismatchException(this.package, this.description1,
576 this.description2); 690 this.description2);
577 691
578 // TODO(nweiz): Dump to YAML when that's supported 692 // TODO(nweiz): Dump to YAML when that's supported
579 String toString() => "Package '$package' has conflicting descriptions " 693 String toString() => "Package '$package' has conflicting descriptions "
580 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'"; 694 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'";
581 } 695 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698