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

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: Code review 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
« 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
265 Future _processChange(VersionSolver solver, Source source, description, 328 abstract void undo(VersionSolver solver);
266 Dependency dependency, VersionConstraint oldConstraint, 329
267 VersionConstraint newConstraint) { 330 Future _processChange(VersionSolver solver, Dependency oldDependency,
268 var name = dependency.name; 331 Dependency newDependency) {
332 var name = newDependency.name;
333 var source = oldDependency.source != null ?
334 oldDependency.source : newDependency.source;
335 var description = oldDependency.description != null ?
336 oldDependency.description : newDependency.description;
337 var oldConstraint = oldDependency.constraint;
338 var newConstraint = newDependency.constraint;
269 339
270 // If the package is over-constrained, i.e. the packages depending have 340 // If the package is over-constrained, i.e. the packages depending have
271 // disjoint constraints, then stop. 341 // disjoint constraints, then try unlocking a depender that's locked by the
342 // lockfile. If there are no remaining locked dependencies, throw an error.
272 if (newConstraint != null && newConstraint.isEmpty) { 343 if (newConstraint != null && newConstraint.isEmpty) {
344 if (solver.tryUnlockDepender(newDependency)) {
345 undo(solver);
346 return null;
347 }
348
273 throw new DisjointConstraintException(name); 349 throw new DisjointConstraintException(name);
274 } 350 }
275 351
276 // If this constraint change didn't cause the overall constraint on the 352 // 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. 353 // package to change, then we don't need to do any further work.
278 if (oldConstraint == newConstraint) return null; 354 if (oldConstraint == newConstraint) return null;
279 355
280 // If the dependency has been cut free from the graph, just remove it. 356 // If the dependency has been cut free from the graph, just remove it.
281 if (!dependency.isDependedOn) { 357 if (!newDependency.isDependedOn) {
282 solver.enqueue(new ChangeVersion(source, description, null)); 358 solver.enqueue(new ChangeVersion(source, description, null));
283 return null; 359 return null;
284 } 360 }
285 361
286 // If the dependency is on the root package, then we don't need to do 362 // 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. 363 // anything since it's already at the best version.
288 if (name == solver._root.name) { 364 if (name == solver._root.name) {
289 solver.enqueue(new ChangeVersion( 365 solver.enqueue(new ChangeVersion(
290 source, description, solver._root.version)); 366 source, description, solver._root.version));
291 return null; 367 return null;
292 } 368 }
293 369
294 // If the dependency is on a package in the lockfile, use the lockfile's 370 // 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. 371 // version for that package if it's valid given the other constraints.
296 var lockedPackage = solver.lockFile.packages[name]; 372 var lockedPackage = solver.lockFile.packages[name];
297 if (lockedPackage != null) { 373 if (lockedPackage != null) {
298 var lockedVersion = lockedPackage.version; 374 var lockedVersion = lockedPackage.version;
299 if (newConstraint.allows(lockedVersion)) { 375 if (newConstraint.allows(lockedVersion)) {
300 solver.enqueue(new ChangeVersion(source, description, lockedVersion)); 376 solver.enqueue(new ChangeVersion(source, description, lockedVersion));
301 return null; 377 return null;
302 } 378 }
303 } 379 }
304 380
305 // The constraint has changed, so see what the best version of the package 381 // The constraint has changed, so see what the best version of the package
306 // that meets the new constraint is. 382 // that meets the new constraint is.
307 return source.getVersions(description).transform((versions) { 383 return solver.getBestVersion(newDependency).transform((best) {
308 var best = null; 384 if (best == null) {
309 for (var version in versions) { 385 undo(solver);
310 if (newConstraint.allows(version)) { 386 } else if (newDependency.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)); 387 solver.enqueue(new ChangeVersion(source, description, best));
320 } 388 }
321 }); 389 });
322 } 390 }
323 } 391 }
324 392
325 /** 393 /**
326 * The constraint given by [ref] is being placed by [depender]. 394 * The constraint given by [ref] is being placed by [depender].
327 */ 395 */
328 class AddConstraint extends ChangeConstraint { 396 class AddConstraint extends ChangeConstraint {
329 /** 397 /**
330 * The package that has the dependency. 398 * The package that has the dependency.
331 */ 399 */
332 final String depender; 400 final String depender;
333 401
334 /** 402 /**
335 * The package being depended on and the constraints being placed on it. The 403 * 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 404 * source, version, and description in this ref are all considered constraints
337 * on the dependent package. 405 * on the dependent package.
338 */ 406 */
339 final PackageRef ref; 407 final PackageRef ref;
340 408
341 AddConstraint(this.depender, this.ref); 409 AddConstraint(this.depender, this.ref);
342 410
343 Future process(VersionSolver solver) { 411 Future process(VersionSolver solver) {
344 var dependency = solver.getDependency(ref.name); 412 var dependency = solver.getDependency(ref.name);
345 var oldConstraint = dependency.constraint; 413 var oldDependency = dependency.clone();
346 dependency.placeConstraint(depender, ref); 414 dependency.placeConstraint(depender, ref);
347 var newConstraint = dependency.constraint; 415 return _processChange(solver, oldDependency, dependency);
348 return _processChange(solver, ref.source, ref.description, dependency, 416 }
349 oldConstraint, newConstraint); 417
418 void undo(VersionSolver solver) {
419 solver.getDependency(ref.name).removeConstraint(depender);
350 } 420 }
351 } 421 }
352 422
353 /** 423 /**
354 * [depender] is no longer placing a constraint on [dependent]. 424 * [depender] is no longer placing a constraint on [dependent].
355 */ 425 */
356 class RemoveConstraint extends ChangeConstraint { 426 class RemoveConstraint extends ChangeConstraint {
357 /** 427 /**
358 * The package that was placing a constraint on [dependent]. 428 * The package that was placing a constraint on [dependent].
359 */ 429 */
360 String depender; 430 String depender;
361 431
362 /** 432 /**
363 * The package that was being depended on. 433 * The package that was being depended on.
364 */ 434 */
365 String dependent; 435 String dependent;
366 436
437 /** The constraint that was removed. */
438 PackageRef _removed;
439
367 RemoveConstraint(this.depender, this.dependent); 440 RemoveConstraint(this.depender, this.dependent);
368 441
369 Future process(VersionSolver solver) { 442 Future process(VersionSolver solver) {
370 var dependency = solver.getDependency(dependent); 443 var dependency = solver.getDependency(dependent);
371 var oldConstraint = dependency.constraint; 444 var oldDependency = dependency.clone();
372 var source = dependency.source; 445 _removed = dependency.removeConstraint(depender);
373 var description = dependency.description; 446 return _processChange(solver, oldDependency, dependency);
374 dependency.removeConstraint(depender); 447 }
375 var newConstraint = dependency.constraint; 448
376 return _processChange(solver, source, description, dependency, 449 void undo() {
377 oldConstraint, newConstraint); 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 }
472 570
473 Dependency(this.name) 571 Dependency(this.name)
474 : _refs = <String, PackageRef>{}; 572 : _refs = <String, PackageRef>{};
475 573
574 Dependency._clone(Dependency other)
575 : name = other.name,
576 source = other.source,
577 description = other.description,
578 version = other.version,
579 _refs = new Map<String, PackageRef>.from(other._refs);
580
581 /** Creates a copy of this dependency. */
582 Dependency clone() => new Dependency._clone(this);
583
476 /** 584 /**
477 * Places [ref] as a constraint from [package] onto this. 585 * Places [ref] as a constraint from [package] onto this.
478 */ 586 */
479 void placeConstraint(String package, PackageRef ref) { 587 void placeConstraint(String package, PackageRef ref) {
480 // If this isn't the first constraint placed on this package, make sure it 588 // If this isn't the first constraint placed on this package, make sure it
481 // matches the source and description of past constraints. 589 // matches the source and description of past constraints.
482 if (_refs.isEmpty()) { 590 if (_refs.isEmpty()) {
483 source = ref.source; 591 source = ref.source;
484 description = ref.description; 592 description = ref.description;
485 } else if (source.name != ref.source.name) { 593 } else if (source.name != ref.source.name) {
486 throw new SourceMismatchException(name, source, ref.source); 594 throw new SourceMismatchException(name, source, ref.source);
487 } else if (!source.descriptionsEqual(description, ref.description)) { 595 } else if (!source.descriptionsEqual(description, ref.description)) {
488 throw new DescriptionMismatchException( 596 throw new DescriptionMismatchException(
489 name, description, ref.description); 597 name, description, ref.description);
490 } 598 }
491 599
492 _refs[package] = ref; 600 _refs[package] = ref;
493 } 601 }
494 602
495 /** 603 /**
496 * Removes the constraint from [package] onto this. 604 * Removes the constraint from [package] onto this.
497 */ 605 */
498 void removeConstraint(String package) { 606 PackageRef removeConstraint(String package) {
499 _refs.remove(package); 607 var removed = _refs.remove(package);
500 608
501 if (_refs.isEmpty()) { 609 if (_refs.isEmpty()) {
502 source = null; 610 source = null;
503 description = null; 611 description = null;
504 } 612 }
613
614 return removed;
505 } 615 }
506 } 616 }
507 617
508 // TODO(rnystrom): Report the last of depending packages and their constraints. 618 // TODO(rnystrom): Report the last of depending packages and their constraints.
509 /** 619 /**
510 * Exception thrown when the [VersionConstraint] used to match a package is 620 * 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 621 * valid (i.e. non-empty), but there are no released versions of the package
512 * that fit that constraint. 622 * that fit that constraint.
513 */ 623 */
514 class NoVersionException implements Exception { 624 class NoVersionException implements Exception {
515 final String package; 625 final String package;
516 final VersionConstraint constraint; 626 final VersionConstraint constraint;
517 627
518 NoVersionException(this.package, this.constraint); 628 NoVersionException(this.package, this.constraint);
519 629
520 String toString() => 630 String toString() =>
521 "Package '$package' has no versions that match $constraint."; 631 "Package '$package' has no versions that match $constraint.";
522 } 632 }
523 633
634 // TODO(rnystrom): Report the list of depending packages and their constraints.
635 /**
636 * Exception thrown when the most recent version of [package] must be selected,
637 * but doesn't match the [VersionConstraint] imposed on the package.
638 */
639 class CouldNotUpdateException implements Exception {
640 final String package;
641 final VersionConstraint constraint;
642 final Version best;
643
644 CouldNotUpdateException(this.package, this.constraint, this.best);
645
646 String toString() =>
647 "The latest version of '$package', $best, does not match $constraint.";
648 }
649
524 // TODO(rnystrom): Report the last of depending packages and their constraints. 650 // TODO(rnystrom): Report the last of depending packages and their constraints.
525 /** 651 /**
526 * Exception thrown when the [VersionConstraint] used to match a package is 652 * 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 653 * the empty set: in other words, multiple packages depend on it and have
528 * conflicting constraints that have no overlap. 654 * conflicting constraints that have no overlap.
529 */ 655 */
530 class DisjointConstraintException implements Exception { 656 class DisjointConstraintException implements Exception {
531 final String package; 657 final String package;
532 658
533 DisjointConstraintException(this.package); 659 DisjointConstraintException(this.package);
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
573 final description1; 699 final description1;
574 final description2; 700 final description2;
575 701
576 DescriptionMismatchException(this.package, this.description1, 702 DescriptionMismatchException(this.package, this.description1,
577 this.description2); 703 this.description2);
578 704
579 // TODO(nweiz): Dump to YAML when that's supported 705 // TODO(nweiz): Dump to YAML when that's supported
580 String toString() => "Package '$package' has conflicting descriptions " 706 String toString() => "Package '$package' has conflicting descriptions "
581 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'"; 707 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'";
582 } 708 }
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