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

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

Issue 10837274: Allow `pub install` and `pub update` to update dependers. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fix VersionSolver test failures. 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
« no previous file with comments | « utils/pub/entrypoint.dart ('k') | utils/tests/pub/pub_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 = <String, Dependency>{}, 76 _packages = <String, 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.
84 */
85 void useLatestVersion(String package) {
86 // TODO(nweiz): How do we want to detect and handle unknown dependencies
87 // here?
88 getDependency(package).useLatestVersion = 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.useLatestVersion ||
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 (tryUnlockDepender(dependency)) return null;
165 throw new NoVersionException(dependency.name, dependency.constraint);
166 } else if (!dependency.constraint.allows(best)) {
167 if (tryUnlockDepender(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 tryUnlockDepender(Dependency dependency) {
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(tryUnlockDepender);
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 111 matching lines...) Expand 10 before | Expand all | Expand 10 after
255 * A constraint that a depending package places on a dependent package has 318 * A constraint that a depending package places on a dependent package has
256 * changed. 319 * changed.
257 * 320 *
258 * This is an abstract class that contains logic for updating the dependency 321 * This is an abstract class that contains logic for updating the dependency
259 * graph once a dependency has changed. Changing the dependency is the 322 * graph once a dependency has changed. Changing the dependency is the
260 * responsibility of subclasses. 323 * responsibility of subclasses.
261 */ 324 */
262 class ChangeConstraint implements WorkItem { 325 class ChangeConstraint implements WorkItem {
263 abstract Future process(VersionSolver solver); 326 abstract Future process(VersionSolver solver);
264 327
328 abstract void undo(VersionSolver solver);
329
265 Future _processChange(VersionSolver solver, Source source, description, 330 Future _processChange(VersionSolver solver, Source source, description,
266 Dependency dependency, VersionConstraint oldConstraint, 331 Dependency dependency, VersionConstraint oldConstraint) {
Bob Nystrom 2012/08/21 18:29:57 (source, dep, constraint) is a PackageRef. Can we
nweiz 2012/08/21 19:57:23 Calling these values a PackageRef isn't really an
267 VersionConstraint newConstraint) {
268 var name = dependency.name; 332 var name = dependency.name;
333 var newConstraint = dependency.constraint;
269 334
270 // If the package is over-constrained, i.e. the packages depending have 335 // If the package is over-constrained, i.e. the packages depending have
271 // disjoint constraints, then stop. 336 // disjoint constraints, then try unlocking a depender that's locked by the
337 // lockfile. If there are no remaining locked dependencies, throw an error.
272 if (newConstraint != null && newConstraint.isEmpty) { 338 if (newConstraint != null && newConstraint.isEmpty) {
339 if (solver.tryUnlockDepender(dependency)) {
340 undo(solver);
341 return null;
342 }
343
273 throw new DisjointConstraintException(name); 344 throw new DisjointConstraintException(name);
274 } 345 }
275 346
276 // If this constraint change didn't cause the overall constraint on the 347 // If this constraint change didn't cause the overall constraint on the
277 // package to change, then we don't need to do any further work. 348 // package to change, then we don't need to do any further work.
278 if (oldConstraint == newConstraint) return null; 349 if (oldConstraint == newConstraint) return null;
279 350
280 // If the dependency has been cut free from the graph, just remove it. 351 // If the dependency has been cut free from the graph, just remove it.
281 if (!dependency.isDependedOn) { 352 if (!dependency.isDependedOn) {
353 if (source == null) print("dependency for null source: ${dependency.name}" );
Bob Nystrom 2012/08/21 18:29:57 Debug code. Nix.
nweiz 2012/08/21 19:57:23 Done.
282 solver.enqueue(new ChangeVersion(source, description, null)); 354 solver.enqueue(new ChangeVersion(source, description, null));
283 return null; 355 return null;
284 } 356 }
285 357
286 // If the dependency is on the root package, then we don't need to do 358 // If the dependency is on the root package, then we don't need to do
287 // anything since it's already at the best version. 359 // anything since it's already at the best version.
288 if (name == solver._root.name) { 360 if (name == solver._root.name) {
289 solver.enqueue(new ChangeVersion( 361 solver.enqueue(new ChangeVersion(
290 source, description, solver._root.version)); 362 source, description, solver._root.version));
291 return null; 363 return null;
292 } 364 }
293 365
294 // If the dependency is on a package in the lockfile, use the lockfile's 366 // If the dependency is on a package in the lockfile, use the lockfile's
295 // version for that package if it's valid given the other constraints. 367 // version for that package if it's valid given the other constraints.
296 var lockedPackage = solver.lockFile.packages[name]; 368 var lockedPackage = solver.lockFile.packages[name];
297 if (lockedPackage != null) { 369 if (lockedPackage != null) {
298 var lockedVersion = lockedPackage.version; 370 var lockedVersion = lockedPackage.version;
299 if (newConstraint.allows(lockedVersion)) { 371 if (newConstraint.allows(lockedVersion)) {
300 solver.enqueue(new ChangeVersion(source, description, lockedVersion)); 372 solver.enqueue(new ChangeVersion(source, description, lockedVersion));
301 return null; 373 return null;
302 } 374 }
303 } 375 }
304 376
305 // 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
306 // that meets the new constraint is. 378 // that meets the new constraint is.
307 return source.getVersions(description).transform((versions) { 379 return solver.getBestVersion(dependency).transform((best) {
308 var best = null; 380 if (best == null) {
309 for (var version in versions) { 381 undo(solver);
310 if (newConstraint.allows(version)) { 382 } else if (dependency.version != best) {
311 if (best == null || version > best) best = version;
312 }
313 }
314
315 // TODO(rnystrom): Better exception.
316 if (best == null) throw new NoVersionException(name, newConstraint);
317
318 if (dependency.version != best) {
319 solver.enqueue(new ChangeVersion(source, description, best)); 383 solver.enqueue(new ChangeVersion(source, description, best));
320 } 384 }
321 }); 385 });
322 } 386 }
323 } 387 }
324 388
325 /** 389 /**
326 * The constraint given by [ref] is being placed by [depender]. 390 * The constraint given by [ref] is being placed by [depender].
327 */ 391 */
328 class AddConstraint extends ChangeConstraint { 392 class AddConstraint extends ChangeConstraint {
329 /** 393 /**
330 * The package that has the dependency. 394 * The package that has the dependency.
331 */ 395 */
332 final String depender; 396 final String depender;
333 397
334 /** 398 /**
335 * 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
336 * source, version, and description in this ref are all considered constraints 400 * source, version, and description in this ref are all considered constraints
337 * on the dependent package. 401 * on the dependent package.
338 */ 402 */
339 final PackageRef ref; 403 final PackageRef ref;
340 404
341 AddConstraint(this.depender, this.ref); 405 AddConstraint(this.depender, this.ref);
342 406
343 Future process(VersionSolver solver) { 407 Future process(VersionSolver solver) {
344 var dependency = solver.getDependency(ref.name); 408 var dependency = solver.getDependency(ref.name);
345 var oldConstraint = dependency.constraint; 409 var oldConstraint = dependency.constraint;
346 dependency.placeConstraint(depender, ref); 410 dependency.placeConstraint(depender, ref);
347 var newConstraint = dependency.constraint;
348 return _processChange(solver, ref.source, ref.description, dependency, 411 return _processChange(solver, ref.source, ref.description, dependency,
349 oldConstraint, newConstraint); 412 oldConstraint);
413 }
414
415 void undo(VersionSolver solver) {
416 solver.getDependency(ref.name).removeConstraint(depender);
350 } 417 }
351 } 418 }
352 419
353 /** 420 /**
354 * [depender] is no longer placing a constraint on [dependent]. 421 * [depender] is no longer placing a constraint on [dependent].
355 */ 422 */
356 class RemoveConstraint extends ChangeConstraint { 423 class RemoveConstraint extends ChangeConstraint {
357 /** 424 /**
358 * The package that was placing a constraint on [dependent]. 425 * The package that was placing a constraint on [dependent].
359 */ 426 */
360 String depender; 427 String depender;
361 428
362 /** 429 /**
363 * The package that was being depended on. 430 * The package that was being depended on.
364 */ 431 */
365 String dependent; 432 String dependent;
366 433
434 /** The constraint that was removed. */
435 PackageRef _removed;
436
367 RemoveConstraint(this.depender, this.dependent); 437 RemoveConstraint(this.depender, this.dependent);
368 438
369 Future process(VersionSolver solver) { 439 Future process(VersionSolver solver) {
370 var dependency = solver.getDependency(dependent); 440 var dependency = solver.getDependency(dependent);
371 var oldConstraint = dependency.constraint; 441 var oldConstraint = dependency.constraint;
372 var source = dependency.source; 442 var source = dependency.source;
373 var description = dependency.description; 443 var description = dependency.description;
374 dependency.removeConstraint(depender); 444 _removed = dependency.removeConstraint(depender);
375 var newConstraint = dependency.constraint;
376 return _processChange(solver, source, description, dependency, 445 return _processChange(solver, source, description, dependency,
377 oldConstraint, newConstraint); 446 oldConstraint);
447 }
448
449 void undo() {
450 solver.getDependency(dependent).placeConstraint(depender, _removed);
378 } 451 }
379 } 452 }
380 453
454 /** [package]'s version is no longer constrained by the lockfile. */
455 class UnlockPackage implements WorkItem {
456 /** The package being unlocked. */
457 Dependency package;
458
459 UnlockPackage(this.package);
460
461 Future process(VersionSolver solver) {
462 solver.lockFile.packages.remove(package.name);
463 return solver.getBestVersion(package).transform((best) {
464 if (best == null) return null;
465 solver.enqueue(new ChangeVersion(
466 package.source, package.description, best));
467 });
468 }
469 }
470
381 // TODO(rnystrom): Instead of always pulling from the source (which will mean 471 // TODO(rnystrom): Instead of always pulling from the source (which will mean
382 // hitting a server), we should consider caching pubspecs of uninstalled 472 // hitting a server), we should consider caching pubspecs of uninstalled
383 // packages in the system cache. 473 // packages in the system cache.
384 /** 474 /**
385 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting 475 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting
386 * the same pubspec from the server repeatedly. 476 * the same pubspec from the server repeatedly.
387 */ 477 */
388 class PubspecCache { 478 class PubspecCache {
389 final SourceRegistry _sources; 479 final SourceRegistry _sources;
390 final Map<PackageId, Pubspec> _pubspecs; 480 final Map<PackageId, Pubspec> _pubspecs;
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
446 * according to [source]. 536 * according to [source].
447 */ 537 */
448 var description; 538 var description;
449 539
450 /** 540 /**
451 * The currently-selected best version for this dependency. 541 * The currently-selected best version for this dependency.
452 */ 542 */
453 Version version; 543 Version version;
454 544
455 /** 545 /**
546 * Whether this dependency should always select the latest version.
547 */
548 bool useLatestVersion = false;
549
550 /**
456 * Gets whether or not any other packages are currently depending on this 551 * Gets whether or not any other packages are currently depending on this
457 * one. If `false`, then it means this package is not part of the dependency 552 * one. If `false`, then it means this package is not part of the dependency
458 * graph and should be omitted. 553 * graph and should be omitted.
459 */ 554 */
460 bool get isDependedOn() => !_refs.isEmpty(); 555 bool get isDependedOn() => !_refs.isEmpty();
461 556
557 /** The names of all the packages that depend on this dependency. */
558 Collection<String> get dependers() => _refs.getKeys();
559
462 /** 560 /**
463 * Gets the overall constraint that all packages are placing on this one. 561 * Gets the overall constraint that all packages are placing on this one.
464 * If no packages have a constraint on this one (which can happen when this 562 * If no packages have a constraint on this one (which can happen when this
465 * package is in the process of being added to the graph), returns `null`. 563 * package is in the process of being added to the graph), returns `null`.
466 */ 564 */
467 VersionConstraint get constraint() { 565 VersionConstraint get constraint() {
468 if (_refs.isEmpty()) return null; 566 if (_refs.isEmpty()) return null;
469 return new VersionConstraint.intersect( 567 return new VersionConstraint.intersect(
470 _refs.getValues().map((ref) => ref.constraint)); 568 _refs.getValues().map((ref) => ref.constraint));
471 } 569 }
(...skipping 16 matching lines...) Expand all
488 throw new DescriptionMismatchException( 586 throw new DescriptionMismatchException(
489 name, description, ref.description); 587 name, description, ref.description);
490 } 588 }
491 589
492 _refs[package] = ref; 590 _refs[package] = ref;
493 } 591 }
494 592
495 /** 593 /**
496 * Removes the constraint from [package] onto this. 594 * Removes the constraint from [package] onto this.
497 */ 595 */
498 void removeConstraint(String package) { 596 PackageRef removeConstraint(String package) {
499 _refs.remove(package); 597 var removed = _refs.remove(package);
500 598
501 if (_refs.isEmpty()) { 599 if (_refs.isEmpty()) {
502 source = null; 600 source = null;
503 description = null; 601 description = null;
504 } 602 }
603
604 return removed;
505 } 605 }
506 } 606 }
507 607
508 // TODO(rnystrom): Report the last of depending packages and their constraints. 608 // TODO(rnystrom): Report the last of depending packages and their constraints.
509 /** 609 /**
510 * Exception thrown when the [VersionConstraint] used to match a package is 610 * Exception thrown when the [VersionConstraint] used to match a package is
511 * valid (i.e. non-empty), but there are no released versions of the package 611 * valid (i.e. non-empty), but there are no released versions of the package
512 * that fit that constraint. 612 * that fit that constraint.
513 */ 613 */
514 class NoVersionException implements Exception { 614 class NoVersionException implements Exception {
515 final String package; 615 final String package;
516 final VersionConstraint constraint; 616 final VersionConstraint constraint;
517 617
518 NoVersionException(this.package, this.constraint); 618 NoVersionException(this.package, this.constraint);
519 619
520 String toString() => 620 String toString() =>
521 "Package '$package' has no versions that match $constraint."; 621 "Package '$package' has no versions that match $constraint.";
522 } 622 }
523 623
624 // TODO(rnystrom): Report the list of depending packages and their constraints.
625 /**
626 * Exception thrown when the most recent version of [package] must be selected,
627 * but doesn't match the [VersionConstraint] imposed on the package.
628 */
629 class CouldNotUpdateException implements Exception {
630 final String package;
631 final VersionConstraint constraint;
632 final Version best;
633
634 CouldNotUpdateException(this.package, this.constraint, this.best);
635
636 String toString() =>
637 "The latest version of '$package', $best, does not match $constraint.";
638 }
639
524 // TODO(rnystrom): Report the last of depending packages and their constraints. 640 // TODO(rnystrom): Report the last of depending packages and their constraints.
525 /** 641 /**
526 * Exception thrown when the [VersionConstraint] used to match a package is 642 * Exception thrown when the [VersionConstraint] used to match a package is
527 * the empty set: in other words, multiple packages depend on it and have 643 * the empty set: in other words, multiple packages depend on it and have
528 * conflicting constraints that have no overlap. 644 * conflicting constraints that have no overlap.
529 */ 645 */
530 class DisjointConstraintException implements Exception { 646 class DisjointConstraintException implements Exception {
531 final String package; 647 final String package;
532 648
533 DisjointConstraintException(this.package); 649 DisjointConstraintException(this.package);
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
573 final description1; 689 final description1;
574 final description2; 690 final description2;
575 691
576 DescriptionMismatchException(this.package, this.description1, 692 DescriptionMismatchException(this.package, this.description1,
577 this.description2); 693 this.description2);
578 694
579 // TODO(nweiz): Dump to YAML when that's supported 695 // TODO(nweiz): Dump to YAML when that's supported
580 String toString() => "Package '$package' has conflicting descriptions " 696 String toString() => "Package '$package' has conflicting descriptions "
581 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'"; 697 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'";
582 } 698 }
OLDNEW
« no previous file with comments | « utils/pub/entrypoint.dart ('k') | utils/tests/pub/pub_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698