| 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 | |
| 92 Future<List<PackageId>> solve() { | 79 Future<List<PackageId>> solve() { |
| 93 // Kick off the work by adding the root package at its concrete version to | 80 // Kick off the work by adding the root package at its concrete version to |
| 94 // the dependency graph. | 81 // the dependency graph. |
| 95 var ref = new PackageRef(new RootSource(_root), _root.version, _root.name); | 82 var ref = new PackageRef(new RootSource(_root), _root.version, _root.name); |
| 96 enqueue(new AddConstraint('(entrypoint)', ref)); | 83 enqueue(new AddConstraint('(entrypoint)', ref)); |
| 97 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); | 84 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); |
| 98 | 85 |
| 99 Future processNextWorkItem(_) { | 86 Future processNextWorkItem(_) { |
| 100 while (true) { | 87 while (true) { |
| 101 // Stop if we are done. | 88 // Stop if we are done. |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 137 return _packages[package]; | 124 return _packages[package]; |
| 138 } | 125 } |
| 139 | 126 |
| 140 /** | 127 /** |
| 141 * Sets the best selected version of [package] to [version]. | 128 * Sets the best selected version of [package] to [version]. |
| 142 */ | 129 */ |
| 143 void setVersion(String package, Version version) { | 130 void setVersion(String package, Version version) { |
| 144 _packages[package].version = version; | 131 _packages[package].version = version; |
| 145 } | 132 } |
| 146 | 133 |
| 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 | |
| 197 List<PackageId> buildResults() { | 134 List<PackageId> buildResults() { |
| 198 return _packages.getValues().filter((dep) => dep.isDependedOn).map((dep) { | 135 return _packages.getValues().filter((dep) => dep.isDependedOn).map((dep) { |
| 199 var description = dep.description; | 136 var description = dep.description; |
| 200 | 137 |
| 201 // If the lockfile contains a fully-resolved description for the package, | 138 // If the lockfile contains a fully-resolved description for the package, |
| 202 // use that. This allows e.g. Git to ensure that the same commit is used. | 139 // use that. This allows e.g. Git to ensure that the same commit is used. |
| 203 var lockedPackage = lockFile.packages[dep.name]; | 140 var lockedPackage = lockFile.packages[dep.name]; |
| 204 if (lockedPackage != null && lockedPackage.version == dep.version && | 141 if (lockedPackage != null && lockedPackage.version == dep.version && |
| 205 lockedPackage.source.name == dep.source.name && | 142 lockedPackage.source.name == dep.source.name && |
| 206 dep.source.descriptionsEqual( | 143 dep.source.descriptionsEqual( |
| (...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 317 * A constraint that a depending package places on a dependent package has | 254 * A constraint that a depending package places on a dependent package has |
| 318 * changed. | 255 * changed. |
| 319 * | 256 * |
| 320 * This is an abstract class that contains logic for updating the dependency | 257 * This is an abstract class that contains logic for updating the dependency |
| 321 * graph once a dependency has changed. Changing the dependency is the | 258 * graph once a dependency has changed. Changing the dependency is the |
| 322 * responsibility of subclasses. | 259 * responsibility of subclasses. |
| 323 */ | 260 */ |
| 324 class ChangeConstraint implements WorkItem { | 261 class ChangeConstraint implements WorkItem { |
| 325 abstract Future process(VersionSolver solver); | 262 abstract Future process(VersionSolver solver); |
| 326 | 263 |
| 327 abstract void undo(VersionSolver solver); | 264 Future _processChange(VersionSolver solver, Source source, description, |
| 328 | 265 Dependency dependency, VersionConstraint oldConstraint, |
| 329 Future _processChange(VersionSolver solver, Dependency dependency, | 266 VersionConstraint newConstraint) { |
| 330 VersionConstraint oldConstraint) { | |
| 331 var name = dependency.name; | 267 var name = dependency.name; |
| 332 var description = dependency.description; | |
| 333 var source = dependency.source; | |
| 334 var newConstraint = dependency.constraint; | |
| 335 | 268 |
| 336 // If the package is over-constrained, i.e. the packages depending have | 269 // If the package is over-constrained, i.e. the packages depending have |
| 337 // disjoint constraints, then try unlocking a depender that's locked by the | 270 // disjoint constraints, then stop. |
| 338 // lockfile. If there are no remaining locked dependencies, throw an error. | |
| 339 if (newConstraint != null && newConstraint.isEmpty) { | 271 if (newConstraint != null && newConstraint.isEmpty) { |
| 340 if (solver.tryUnlockDepender(dependency)) { | |
| 341 undo(solver); | |
| 342 return null; | |
| 343 } | |
| 344 | |
| 345 throw new DisjointConstraintException(name); | 272 throw new DisjointConstraintException(name); |
| 346 } | 273 } |
| 347 | 274 |
| 348 // If this constraint change didn't cause the overall constraint on the | 275 // If this constraint change didn't cause the overall constraint on the |
| 349 // package to change, then we don't need to do any further work. | 276 // package to change, then we don't need to do any further work. |
| 350 if (oldConstraint == newConstraint) return null; | 277 if (oldConstraint == newConstraint) return null; |
| 351 | 278 |
| 352 // If the dependency has been cut free from the graph, just remove it. | 279 // If the dependency has been cut free from the graph, just remove it. |
| 353 if (!dependency.isDependedOn) { | 280 if (!dependency.isDependedOn) { |
| 354 solver.enqueue(new ChangeVersion(source, description, null)); | 281 solver.enqueue(new ChangeVersion(source, description, null)); |
| (...skipping 14 matching lines...) Expand all Loading... |
| 369 if (lockedPackage != null) { | 296 if (lockedPackage != null) { |
| 370 var lockedVersion = lockedPackage.version; | 297 var lockedVersion = lockedPackage.version; |
| 371 if (newConstraint.allows(lockedVersion)) { | 298 if (newConstraint.allows(lockedVersion)) { |
| 372 solver.enqueue(new ChangeVersion(source, description, lockedVersion)); | 299 solver.enqueue(new ChangeVersion(source, description, lockedVersion)); |
| 373 return null; | 300 return null; |
| 374 } | 301 } |
| 375 } | 302 } |
| 376 | 303 |
| 377 // The constraint has changed, so see what the best version of the package | 304 // The constraint has changed, so see what the best version of the package |
| 378 // that meets the new constraint is. | 305 // that meets the new constraint is. |
| 379 return solver.getBestVersion(dependency).transform((best) { | 306 return source.getVersions(description).transform((versions) { |
| 380 if (best == null) { | 307 var best = null; |
| 381 undo(solver); | 308 for (var version in versions) { |
| 382 } else if (dependency.version != best) { | 309 if (newConstraint.allows(version)) { |
| 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) { |
| 383 solver.enqueue(new ChangeVersion(source, description, best)); | 318 solver.enqueue(new ChangeVersion(source, description, best)); |
| 384 } | 319 } |
| 385 }); | 320 }); |
| 386 } | 321 } |
| 387 } | 322 } |
| 388 | 323 |
| 389 /** | 324 /** |
| 390 * The constraint given by [ref] is being placed by [depender]. | 325 * The constraint given by [ref] is being placed by [depender]. |
| 391 */ | 326 */ |
| 392 class AddConstraint extends ChangeConstraint { | 327 class AddConstraint extends ChangeConstraint { |
| 393 /** | 328 /** |
| 394 * The package that has the dependency. | 329 * The package that has the dependency. |
| 395 */ | 330 */ |
| 396 final String depender; | 331 final String depender; |
| 397 | 332 |
| 398 /** | 333 /** |
| 399 * The package being depended on and the constraints being placed on it. The | 334 * The package being depended on and the constraints being placed on it. The |
| 400 * source, version, and description in this ref are all considered constraints | 335 * source, version, and description in this ref are all considered constraints |
| 401 * on the dependent package. | 336 * on the dependent package. |
| 402 */ | 337 */ |
| 403 final PackageRef ref; | 338 final PackageRef ref; |
| 404 | 339 |
| 405 AddConstraint(this.depender, this.ref); | 340 AddConstraint(this.depender, this.ref); |
| 406 | 341 |
| 407 Future process(VersionSolver solver) { | 342 Future process(VersionSolver solver) { |
| 408 var dependency = solver.getDependency(ref.name); | 343 var dependency = solver.getDependency(ref.name); |
| 409 var oldConstraint = dependency.constraint; | 344 var oldConstraint = dependency.constraint; |
| 410 dependency.placeConstraint(depender, ref); | 345 dependency.placeConstraint(depender, ref); |
| 411 return _processChange(solver, dependency, oldConstraint); | 346 var newConstraint = dependency.constraint; |
| 412 } | 347 return _processChange(solver, ref.source, ref.description, dependency, |
| 413 | 348 oldConstraint, newConstraint); |
| 414 void undo(VersionSolver solver) { | |
| 415 solver.getDependency(ref.name).removeConstraint(depender); | |
| 416 } | 349 } |
| 417 } | 350 } |
| 418 | 351 |
| 419 /** | 352 /** |
| 420 * [depender] is no longer placing a constraint on [dependent]. | 353 * [depender] is no longer placing a constraint on [dependent]. |
| 421 */ | 354 */ |
| 422 class RemoveConstraint extends ChangeConstraint { | 355 class RemoveConstraint extends ChangeConstraint { |
| 423 /** | 356 /** |
| 424 * The package that was placing a constraint on [dependent]. | 357 * The package that was placing a constraint on [dependent]. |
| 425 */ | 358 */ |
| 426 String depender; | 359 String depender; |
| 427 | 360 |
| 428 /** | 361 /** |
| 429 * The package that was being depended on. | 362 * The package that was being depended on. |
| 430 */ | 363 */ |
| 431 String dependent; | 364 String dependent; |
| 432 | 365 |
| 433 /** The constraint that was removed. */ | |
| 434 PackageRef _removed; | |
| 435 | |
| 436 RemoveConstraint(this.depender, this.dependent); | 366 RemoveConstraint(this.depender, this.dependent); |
| 437 | 367 |
| 438 Future process(VersionSolver solver) { | 368 Future process(VersionSolver solver) { |
| 439 var dependency = solver.getDependency(dependent); | 369 var dependency = solver.getDependency(dependent); |
| 440 var oldConstraint = dependency.constraint; | 370 var oldConstraint = dependency.constraint; |
| 441 var source = dependency.source; | 371 var source = dependency.source; |
| 442 _removed = dependency.removeConstraint(depender); | 372 var description = dependency.description; |
| 443 return _processChange(solver, dependency, oldConstraint); | 373 dependency.removeConstraint(depender); |
| 444 } | 374 var newConstraint = dependency.constraint; |
| 445 | 375 return _processChange(solver, source, description, dependency, |
| 446 void undo() { | 376 oldConstraint, newConstraint); |
| 447 solver.getDependency(dependent).placeConstraint(depender, _removed); | |
| 448 } | 377 } |
| 449 } | 378 } |
| 450 | 379 |
| 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 | |
| 468 // TODO(rnystrom): Instead of always pulling from the source (which will mean | 380 // TODO(rnystrom): Instead of always pulling from the source (which will mean |
| 469 // hitting a server), we should consider caching pubspecs of uninstalled | 381 // hitting a server), we should consider caching pubspecs of uninstalled |
| 470 // packages in the system cache. | 382 // packages in the system cache. |
| 471 /** | 383 /** |
| 472 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting | 384 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting |
| 473 * the same pubspec from the server repeatedly. | 385 * the same pubspec from the server repeatedly. |
| 474 */ | 386 */ |
| 475 class PubspecCache { | 387 class PubspecCache { |
| 476 final SourceRegistry _sources; | 388 final SourceRegistry _sources; |
| 477 final Map<PackageId, Pubspec> _pubspecs; | 389 final Map<PackageId, Pubspec> _pubspecs; |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 533 * according to [source]. | 445 * according to [source]. |
| 534 */ | 446 */ |
| 535 var description; | 447 var description; |
| 536 | 448 |
| 537 /** | 449 /** |
| 538 * The currently-selected best version for this dependency. | 450 * The currently-selected best version for this dependency. |
| 539 */ | 451 */ |
| 540 Version version; | 452 Version version; |
| 541 | 453 |
| 542 /** | 454 /** |
| 543 * Whether this dependency should always select the latest version. | |
| 544 */ | |
| 545 bool useLatestVersion = false; | |
| 546 | |
| 547 /** | |
| 548 * Gets whether or not any other packages are currently depending on this | 455 * Gets whether or not any other packages are currently depending on this |
| 549 * one. If `false`, then it means this package is not part of the dependency | 456 * one. If `false`, then it means this package is not part of the dependency |
| 550 * graph and should be omitted. | 457 * graph and should be omitted. |
| 551 */ | 458 */ |
| 552 bool get isDependedOn() => !_refs.isEmpty(); | 459 bool get isDependedOn() => !_refs.isEmpty(); |
| 553 | 460 |
| 554 /** The names of all the packages that depend on this dependency. */ | |
| 555 Collection<String> get dependers() => _refs.getKeys(); | |
| 556 | |
| 557 /** | 461 /** |
| 558 * Gets the overall constraint that all packages are placing on this one. | 462 * Gets the overall constraint that all packages are placing on this one. |
| 559 * If no packages have a constraint on this one (which can happen when this | 463 * If no packages have a constraint on this one (which can happen when this |
| 560 * package is in the process of being added to the graph), returns `null`. | 464 * package is in the process of being added to the graph), returns `null`. |
| 561 */ | 465 */ |
| 562 VersionConstraint get constraint() { | 466 VersionConstraint get constraint() { |
| 563 if (_refs.isEmpty()) return null; | 467 if (_refs.isEmpty()) return null; |
| 564 return new VersionConstraint.intersect( | 468 return new VersionConstraint.intersect( |
| 565 _refs.getValues().map((ref) => ref.constraint)); | 469 _refs.getValues().map((ref) => ref.constraint)); |
| 566 } | 470 } |
| (...skipping 16 matching lines...) Expand all Loading... |
| 583 throw new DescriptionMismatchException( | 487 throw new DescriptionMismatchException( |
| 584 name, description, ref.description); | 488 name, description, ref.description); |
| 585 } | 489 } |
| 586 | 490 |
| 587 _refs[package] = ref; | 491 _refs[package] = ref; |
| 588 } | 492 } |
| 589 | 493 |
| 590 /** | 494 /** |
| 591 * Removes the constraint from [package] onto this. | 495 * Removes the constraint from [package] onto this. |
| 592 */ | 496 */ |
| 593 PackageRef removeConstraint(String package) { | 497 void removeConstraint(String package) { |
| 594 var removed = _refs.remove(package); | 498 _refs.remove(package); |
| 595 | 499 |
| 596 if (_refs.isEmpty()) { | 500 if (_refs.isEmpty()) { |
| 597 source = null; | 501 source = null; |
| 598 description = null; | 502 description = null; |
| 599 } | 503 } |
| 600 | |
| 601 return removed; | |
| 602 } | 504 } |
| 603 } | 505 } |
| 604 | 506 |
| 605 // TODO(rnystrom): Report the last of depending packages and their constraints. | 507 // TODO(rnystrom): Report the last of depending packages and their constraints. |
| 606 /** | 508 /** |
| 607 * Exception thrown when the [VersionConstraint] used to match a package is | 509 * Exception thrown when the [VersionConstraint] used to match a package is |
| 608 * valid (i.e. non-empty), but there are no released versions of the package | 510 * valid (i.e. non-empty), but there are no released versions of the package |
| 609 * that fit that constraint. | 511 * that fit that constraint. |
| 610 */ | 512 */ |
| 611 class NoVersionException implements Exception { | 513 class NoVersionException implements Exception { |
| 612 final String package; | 514 final String package; |
| 613 final VersionConstraint constraint; | 515 final VersionConstraint constraint; |
| 614 | 516 |
| 615 NoVersionException(this.package, this.constraint); | 517 NoVersionException(this.package, this.constraint); |
| 616 | 518 |
| 617 String toString() => | 519 String toString() => |
| 618 "Package '$package' has no versions that match $constraint."; | 520 "Package '$package' has no versions that match $constraint."; |
| 619 } | 521 } |
| 620 | 522 |
| 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 | |
| 637 // TODO(rnystrom): Report the last of depending packages and their constraints. | 523 // TODO(rnystrom): Report the last of depending packages and their constraints. |
| 638 /** | 524 /** |
| 639 * Exception thrown when the [VersionConstraint] used to match a package is | 525 * Exception thrown when the [VersionConstraint] used to match a package is |
| 640 * the empty set: in other words, multiple packages depend on it and have | 526 * the empty set: in other words, multiple packages depend on it and have |
| 641 * conflicting constraints that have no overlap. | 527 * conflicting constraints that have no overlap. |
| 642 */ | 528 */ |
| 643 class DisjointConstraintException implements Exception { | 529 class DisjointConstraintException implements Exception { |
| 644 final String package; | 530 final String package; |
| 645 | 531 |
| 646 DisjointConstraintException(this.package); | 532 DisjointConstraintException(this.package); |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 686 final description1; | 572 final description1; |
| 687 final description2; | 573 final description2; |
| 688 | 574 |
| 689 DescriptionMismatchException(this.package, this.description1, | 575 DescriptionMismatchException(this.package, this.description1, |
| 690 this.description2); | 576 this.description2); |
| 691 | 577 |
| 692 // TODO(nweiz): Dump to YAML when that's supported | 578 // TODO(nweiz): Dump to YAML when that's supported |
| 693 String toString() => "Package '$package' has conflicting descriptions " | 579 String toString() => "Package '$package' has conflicting descriptions " |
| 694 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'"; | 580 "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'"; |
| 695 } | 581 } |
| OLD | NEW |