| OLD | NEW |
| 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 Loading... |
| 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. |
| 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 Loading... |
| 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 110 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 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.tryUnlockDepender(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 Loading... |
| 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)); |
| 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 Loading... |
| 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 latest version. |
| 544 */ |
| 545 bool useLatestVersion = false; |
| 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 /** The names of all the packages that depend on this dependency. */ |
| 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 Loading... |
| 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 latest version of '$package', $best, does not match $constraint."; |
| 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 Loading... |
| 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 } |
| OLD | NEW |