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 /// Attempts to resolve a set of version constraints for a package dependency | 5 /// Attempts to resolve a set of version constraints for a package dependency |
6 /// graph and select an appropriate set of best specific versions for all | 6 /// graph and select an appropriate set of best specific versions for all |
7 /// dependent packages. It works iteratively and tries to reach a stable | 7 /// dependent packages. It works iteratively and tries to reach a stable |
8 /// solution where the constraints of all dependencies are met. If it fails to | 8 /// solution where the constraints of all dependencies are met. If it fails to |
9 /// reach a solution after a certain number of iterations, it assumes the | 9 /// reach a solution after a certain number of iterations, it assumes the |
10 /// dependency graph is unstable and reports and error. | 10 /// dependency graph is unstable and reports and error. |
(...skipping 17 matching lines...) Expand all Loading... |
28 /// When a constraint on a package changes, we re-calculate the overall | 28 /// When a constraint on a package changes, we re-calculate the overall |
29 /// constraint on that package. I.e. with a shared dependency, we intersect all | 29 /// constraint on that package. I.e. with a shared dependency, we intersect all |
30 /// of the constraints that its depending packages place on it. If that overall | 30 /// of the constraints that its depending packages place on it. If that overall |
31 /// constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently | 31 /// constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently |
32 /// picked version for that package may fall outside of the new constraint. If | 32 /// picked version for that package may fall outside of the new constraint. If |
33 /// that happens, we find the new best version that meets the updated constraint | 33 /// that happens, we find the new best version that meets the updated constraint |
34 /// and then the change the package to use that version. That cycles back up to | 34 /// and then the change the package to use that version. That cycles back up to |
35 /// the beginning again. | 35 /// the beginning again. |
36 library version_solver; | 36 library version_solver; |
37 | 37 |
38 import 'dart:json'; | 38 import 'dart:async'; |
| 39 import 'dart:json' as json; |
39 import 'dart:math'; | 40 import 'dart:math'; |
40 import 'lock_file.dart'; | 41 import 'lock_file.dart'; |
41 import 'log.dart' as log; | 42 import 'log.dart' as log; |
42 import 'package.dart'; | 43 import 'package.dart'; |
43 import 'pubspec.dart'; | 44 import 'pubspec.dart'; |
44 import 'root_source.dart'; | 45 import 'root_source.dart'; |
45 import 'source.dart'; | 46 import 'source.dart'; |
46 import 'source_registry.dart'; | 47 import 'source_registry.dart'; |
47 import 'utils.dart'; | 48 import 'utils.dart'; |
48 import 'version.dart'; | 49 import 'version.dart'; |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
109 if (_numIterations > max(50, _packages.length * 5)) { | 110 if (_numIterations > max(50, _packages.length * 5)) { |
110 throw new CouldNotSolveException(); | 111 throw new CouldNotSolveException(); |
111 } | 112 } |
112 | 113 |
113 // Run the first work item. | 114 // Run the first work item. |
114 var future = _work.removeFirst().process(this); | 115 var future = _work.removeFirst().process(this); |
115 | 116 |
116 // If we have an async operation to perform, chain the loop to resume | 117 // If we have an async operation to perform, chain the loop to resume |
117 // when it's done. Otherwise, just loop synchronously. | 118 // when it's done. Otherwise, just loop synchronously. |
118 if (future != null) { | 119 if (future != null) { |
119 return future.chain(processNextWorkItem); | 120 return future.then(processNextWorkItem); |
120 } | 121 } |
121 } | 122 } |
122 } | 123 } |
123 | 124 |
124 return processNextWorkItem(null); | 125 return processNextWorkItem(null); |
125 } | 126 } |
126 | 127 |
127 void enqueue(WorkItem work) { | 128 void enqueue(WorkItem work) { |
128 _work.add(work); | 129 _work.add(work); |
129 } | 130 } |
130 | 131 |
131 Dependency getDependency(String package) { | 132 Dependency getDependency(String package) { |
132 // There can be unused dependencies in the graph, so just create an empty | 133 // There can be unused dependencies in the graph, so just create an empty |
133 // one if needed. | 134 // one if needed. |
134 _packages.putIfAbsent(package, () => new Dependency(package)); | 135 _packages.putIfAbsent(package, () => new Dependency(package)); |
135 return _packages[package]; | 136 return _packages[package]; |
136 } | 137 } |
137 | 138 |
138 /// Sets the best selected version of [package] to [version]. | 139 /// Sets the best selected version of [package] to [version]. |
139 void setVersion(String package, Version version) { | 140 void setVersion(String package, Version version) { |
140 _packages[package].version = version; | 141 _packages[package].version = version; |
141 } | 142 } |
142 | 143 |
143 /// Returns the most recent version of [dependency] that satisfies all of its | 144 /// Returns the most recent version of [dependency] that satisfies all of its |
144 /// version constraints. | 145 /// version constraints. |
145 Future<Version> getBestVersion(Dependency dependency) { | 146 Future<Version> getBestVersion(Dependency dependency) { |
146 return dependency.getVersions().transform((versions) { | 147 return dependency.getVersions().then((versions) { |
147 var best = null; | 148 var best = null; |
148 for (var version in versions) { | 149 for (var version in versions) { |
149 if (dependency.useLatestVersion || | 150 if (dependency.useLatestVersion || |
150 dependency.constraint.allows(version)) { | 151 dependency.constraint.allows(version)) { |
151 if (best == null || version > best) best = version; | 152 if (best == null || version > best) best = version; |
152 } | 153 } |
153 } | 154 } |
154 | 155 |
155 // TODO(rnystrom): Better exception. | 156 // TODO(rnystrom): Better exception. |
156 if (best == null) { | 157 if (best == null) { |
(...skipping 25 matching lines...) Expand all Loading... |
182 | 183 |
183 for (var dependerName in dependency.dependers) { | 184 for (var dependerName in dependency.dependers) { |
184 var depender = getDependency(dependerName); | 185 var depender = getDependency(dependerName); |
185 var locked = lockFile.packages[dependerName]; | 186 var locked = lockFile.packages[dependerName]; |
186 if (locked != null && depender.version == locked.version) { | 187 if (locked != null && depender.version == locked.version) { |
187 enqueue(new UnlockPackage(depender)); | 188 enqueue(new UnlockPackage(depender)); |
188 return true; | 189 return true; |
189 } | 190 } |
190 } | 191 } |
191 | 192 |
192 return dependency.dependers.map(getDependency).some((subdependency) => | 193 return dependency.dependers.mappedBy(getDependency).any((subdependency) => |
193 tryUnlockDepender(subdependency, seen)); | 194 tryUnlockDepender(subdependency, seen)); |
194 } | 195 } |
195 | 196 |
196 List<PackageId> buildResults() { | 197 List<PackageId> buildResults() { |
197 return _packages.values.filter((dep) => dep.isDependedOn).map((dep) { | 198 return _packages.values.where((dep) => dep.isDependedOn).mappedBy((dep) { |
198 var description = dep.description; | 199 var description = dep.description; |
199 | 200 |
200 // If the lockfile contains a fully-resolved description for the package, | 201 // If the lockfile contains a fully-resolved description for the package, |
201 // 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. |
202 var lockedPackage = lockFile.packages[dep.name]; | 203 var lockedPackage = lockFile.packages[dep.name]; |
203 if (lockedPackage != null && lockedPackage.version == dep.version && | 204 if (lockedPackage != null && lockedPackage.version == dep.version && |
204 lockedPackage.source.name == dep.source.name && | 205 lockedPackage.source.name == dep.source.name && |
205 dep.source.descriptionsEqual( | 206 dep.source.descriptionsEqual( |
206 description, lockedPackage.description)) { | 207 description, lockedPackage.description)) { |
207 description = lockedPackage.description; | 208 description = lockedPackage.description; |
208 } | 209 } |
209 | 210 |
210 return new PackageId(dep.name, dep.source, dep.version, description); | 211 return new PackageId(dep.name, dep.source, dep.version, description); |
211 }); | 212 }) |
| 213 .toList(); |
212 } | 214 } |
213 } | 215 } |
214 | 216 |
215 /// The constraint solver works by iteratively processing a queue of work items. | 217 /// The constraint solver works by iteratively processing a queue of work items. |
216 /// Each item is a single atomic change to the dependency graph. Handling them | 218 /// Each item is a single atomic change to the dependency graph. Handling them |
217 /// in a queue lets us handle asynchrony (resolving versions requires | 219 /// in a queue lets us handle asynchrony (resolving versions requires |
218 /// information from servers) as well as avoid deeply nested recursion. | 220 /// information from servers) as well as avoid deeply nested recursion. |
219 abstract class WorkItem { | 221 abstract class WorkItem { |
220 /// Processes this work item. Returns a future that completes when the work is | 222 /// Processes this work item. Returns a future that completes when the work is |
221 /// done. If `null` is returned, that means the work has completed | 223 /// done. If `null` is returned, that means the work has completed |
(...skipping 25 matching lines...) Expand all Loading... |
247 log.fine("Changing $package to version $version."); | 249 log.fine("Changing $package to version $version."); |
248 | 250 |
249 var dependency = solver.getDependency(package); | 251 var dependency = solver.getDependency(package); |
250 var oldVersion = dependency.version; | 252 var oldVersion = dependency.version; |
251 solver.setVersion(package, version); | 253 solver.setVersion(package, version); |
252 | 254 |
253 // The dependencies between the old and new version may be different. Walk | 255 // The dependencies between the old and new version may be different. Walk |
254 // them both and update any constraints that differ between the two. | 256 // them both and update any constraints that differ between the two. |
255 return Futures.wait([ | 257 return Futures.wait([ |
256 getDependencyRefs(solver, oldVersion), | 258 getDependencyRefs(solver, oldVersion), |
257 getDependencyRefs(solver, version)]).transform((list) { | 259 getDependencyRefs(solver, version)]).then((list) { |
258 var oldDependencyRefs = list[0]; | 260 var oldDependencyRefs = list[0]; |
259 var newDependencyRefs = list[1]; | 261 var newDependencyRefs = list[1]; |
260 | 262 |
261 for (var oldRef in oldDependencyRefs.values) { | 263 for (var oldRef in oldDependencyRefs.values) { |
262 if (newDependencyRefs.containsKey(oldRef.name)) { | 264 if (newDependencyRefs.containsKey(oldRef.name)) { |
263 // The dependency is in both versions of this package, but its | 265 // The dependency is in both versions of this package, but its |
264 // constraint may have changed. | 266 // constraint may have changed. |
265 var newRef = newDependencyRefs.remove(oldRef.name); | 267 var newRef = newDependencyRefs.remove(oldRef.name); |
266 solver.enqueue(new AddConstraint(package, newRef)); | 268 solver.enqueue(new AddConstraint(package, newRef)); |
267 } else { | 269 } else { |
(...skipping 14 matching lines...) Expand all Loading... |
282 /// Get the dependencies at [version] of the package being changed. | 284 /// Get the dependencies at [version] of the package being changed. |
283 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, | 285 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, |
284 Version version) { | 286 Version version) { |
285 // If there is no version, it means no package, so no dependencies. | 287 // If there is no version, it means no package, so no dependencies. |
286 if (version == null) { | 288 if (version == null) { |
287 return | 289 return |
288 new Future<Map<String, PackageRef>>.immediate(<String, PackageRef>{}); | 290 new Future<Map<String, PackageRef>>.immediate(<String, PackageRef>{}); |
289 } | 291 } |
290 | 292 |
291 var id = new PackageId(package, source, version, description); | 293 var id = new PackageId(package, source, version, description); |
292 return solver._pubspecs.load(id).transform((pubspec) { | 294 return solver._pubspecs.load(id).then((pubspec) { |
293 var dependencies = <String, PackageRef>{}; | 295 var dependencies = <String, PackageRef>{}; |
294 for (var dependency in pubspec.dependencies) { | 296 for (var dependency in pubspec.dependencies) { |
295 dependencies[dependency.name] = dependency; | 297 dependencies[dependency.name] = dependency; |
296 } | 298 } |
297 return dependencies; | 299 return dependencies; |
298 }); | 300 }); |
299 } | 301 } |
300 } | 302 } |
301 | 303 |
302 /// A constraint that a depending package places on a dependent package has | 304 /// A constraint that a depending package places on a dependent package has |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
357 var lockedVersion = lockedPackage.version; | 359 var lockedVersion = lockedPackage.version; |
358 if (newConstraint.allows(lockedVersion)) { | 360 if (newConstraint.allows(lockedVersion)) { |
359 solver.enqueue( | 361 solver.enqueue( |
360 new ChangeVersion(name, source, description, lockedVersion)); | 362 new ChangeVersion(name, source, description, lockedVersion)); |
361 return null; | 363 return null; |
362 } | 364 } |
363 } | 365 } |
364 | 366 |
365 // The constraint has changed, so see what the best version of the package | 367 // The constraint has changed, so see what the best version of the package |
366 // that meets the new constraint is. | 368 // that meets the new constraint is. |
367 return solver.getBestVersion(newDependency).transform((best) { | 369 return solver.getBestVersion(newDependency).then((best) { |
368 if (best == null) { | 370 if (best == null) { |
369 undo(solver); | 371 undo(solver); |
370 } else if (newDependency.version != best) { | 372 } else if (newDependency.version != best) { |
371 solver.enqueue(new ChangeVersion(name, source, description, best)); | 373 solver.enqueue(new ChangeVersion(name, source, description, best)); |
372 } | 374 } |
373 }); | 375 }); |
374 } | 376 } |
375 } | 377 } |
376 | 378 |
377 /// The constraint given by [ref] is being placed by [depender]. | 379 /// The constraint given by [ref] is being placed by [depender]. |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
431 class UnlockPackage implements WorkItem { | 433 class UnlockPackage implements WorkItem { |
432 /// The package being unlocked. | 434 /// The package being unlocked. |
433 Dependency package; | 435 Dependency package; |
434 | 436 |
435 UnlockPackage(this.package); | 437 UnlockPackage(this.package); |
436 | 438 |
437 Future process(VersionSolver solver) { | 439 Future process(VersionSolver solver) { |
438 log.fine("Unlocking ${package.name}."); | 440 log.fine("Unlocking ${package.name}."); |
439 | 441 |
440 solver.lockFile.packages.remove(package.name); | 442 solver.lockFile.packages.remove(package.name); |
441 return solver.getBestVersion(package).transform((best) { | 443 return solver.getBestVersion(package).then((best) { |
442 if (best == null) return null; | 444 if (best == null) return null; |
443 solver.enqueue(new ChangeVersion( | 445 solver.enqueue(new ChangeVersion( |
444 package.name, package.source, package.description, best)); | 446 package.name, package.source, package.description, best)); |
445 }); | 447 }); |
446 } | 448 } |
447 } | 449 } |
448 | 450 |
449 // TODO(rnystrom): Instead of always pulling from the source (which will mean | 451 // TODO(rnystrom): Instead of always pulling from the source (which will mean |
450 // hitting a server), we should consider caching pubspecs of uninstalled | 452 // hitting a server), we should consider caching pubspecs of uninstalled |
451 // packages in the system cache. | 453 // packages in the system cache. |
(...skipping 11 matching lines...) Expand all Loading... |
463 _pubspecs[id] = pubspec; | 465 _pubspecs[id] = pubspec; |
464 } | 466 } |
465 | 467 |
466 /// Loads the pubspec for the package identified by [id]. | 468 /// Loads the pubspec for the package identified by [id]. |
467 Future<Pubspec> load(PackageId id) { | 469 Future<Pubspec> load(PackageId id) { |
468 // Complete immediately if it's already cached. | 470 // Complete immediately if it's already cached. |
469 if (_pubspecs.containsKey(id)) { | 471 if (_pubspecs.containsKey(id)) { |
470 return new Future<Pubspec>.immediate(_pubspecs[id]); | 472 return new Future<Pubspec>.immediate(_pubspecs[id]); |
471 } | 473 } |
472 | 474 |
473 return id.describe().transform((pubspec) { | 475 return id.describe().then((pubspec) { |
474 // Cache it. | 476 // Cache it. |
475 _pubspecs[id] = pubspec; | 477 _pubspecs[id] = pubspec; |
476 return pubspec; | 478 return pubspec; |
477 }); | 479 }); |
478 } | 480 } |
479 } | 481 } |
480 | 482 |
481 /// Describes one [Package] in the [DependencyGraph] and keeps track of which | 483 /// Describes one [Package] in the [DependencyGraph] and keeps track of which |
482 /// packages depend on it and what constraints they place on it. | 484 /// packages depend on it and what constraints they place on it. |
483 class Dependency { | 485 class Dependency { |
484 /// The name of the this dependency's package. | 486 /// The name of the this dependency's package. |
485 final String name; | 487 final String name; |
486 | 488 |
487 /// The [PackageRefs] that represent constraints that depending packages have | 489 /// The [PackageRefs] that represent constraints that depending packages have |
488 /// placed on this one. | 490 /// placed on this one. |
489 final Map<String, PackageRef> _refs; | 491 final Map<String, PackageRef> _refs; |
490 | 492 |
491 /// The currently-selected best version for this dependency. | 493 /// The currently-selected best version for this dependency. |
492 Version version; | 494 Version version; |
493 | 495 |
494 /// Whether this dependency should always select the latest version. | 496 /// Whether this dependency should always select the latest version. |
495 bool useLatestVersion = false; | 497 bool useLatestVersion = false; |
496 | 498 |
497 /// Gets whether or not any other packages are currently depending on this | 499 /// Gets whether or not any other packages are currently depending on this |
498 /// one. If `false`, then it means this package is not part of the dependency | 500 /// one. If `false`, then it means this package is not part of the dependency |
499 /// graph and should be omitted. | 501 /// graph and should be omitted. |
500 bool get isDependedOn => !_refs.isEmpty; | 502 bool get isDependedOn => !_refs.isEmpty; |
501 | 503 |
502 /// The names of all the packages that depend on this dependency. | 504 /// The names of all the packages that depend on this dependency. |
503 Collection<String> get dependers => _refs.keys; | 505 Iterable<String> get dependers => _refs.keys; |
504 | 506 |
505 /// Gets the overall constraint that all packages are placing on this one. | 507 /// Gets the overall constraint that all packages are placing on this one. |
506 /// If no packages have a constraint on this one (which can happen when this | 508 /// If no packages have a constraint on this one (which can happen when this |
507 /// package is in the process of being added to the graph), returns `null`. | 509 /// package is in the process of being added to the graph), returns `null`. |
508 VersionConstraint get constraint { | 510 VersionConstraint get constraint { |
509 if (_refs.isEmpty) return null; | 511 if (_refs.isEmpty) return null; |
510 return new VersionConstraint.intersection( | 512 return new VersionConstraint.intersection( |
511 _refs.values.map((ref) => ref.constraint)); | 513 _refs.values.mappedBy((ref) => ref.constraint)); |
512 } | 514 } |
513 | 515 |
514 /// The source of this dependency's package. | 516 /// The source of this dependency's package. |
515 Source get source { | 517 Source get source { |
516 var canonical = _canonicalRef(); | 518 var canonical = _canonicalRef(); |
517 if (canonical == null) return null; | 519 if (canonical == null) return null; |
518 return canonical.source; | 520 return canonical.source; |
519 } | 521 } |
520 | 522 |
521 /// The description of this dependency's package. | 523 /// The description of this dependency's package. |
522 get description { | 524 get description { |
523 var canonical = _canonicalRef(); | 525 var canonical = _canonicalRef(); |
524 if (canonical == null) return null; | 526 if (canonical == null) return null; |
525 return canonical.description; | 527 return canonical.description; |
526 } | 528 } |
527 | 529 |
528 /// Return the PackageRef that has the canonical source and description for | 530 /// Return the PackageRef that has the canonical source and description for |
529 /// this package. If any dependency requires that this package come from a | 531 /// this package. If any dependency requires that this package come from a |
530 /// [RootSource], that will be used; otherwise, it will be the source and | 532 /// [RootSource], that will be used; otherwise, it will be the source and |
531 /// description that all dependencies agree upon. | 533 /// description that all dependencies agree upon. |
532 PackageRef _canonicalRef() { | 534 PackageRef _canonicalRef() { |
533 if (_refs.isEmpty) return null; | 535 if (_refs.isEmpty) return null; |
534 var refs = _refs.values; | 536 var refs = _refs.values; |
535 for (var ref in refs) { | 537 for (var ref in refs) { |
536 if (ref is RootSource) return ref; | 538 if (ref is RootSource) return ref; |
537 } | 539 } |
538 return refs[0]; | 540 return refs.first; |
539 } | 541 } |
540 | 542 |
541 Dependency(this.name) | 543 Dependency(this.name) |
542 : _refs = <String, PackageRef>{}; | 544 : _refs = <String, PackageRef>{}; |
543 | 545 |
544 Dependency._clone(Dependency other) | 546 Dependency._clone(Dependency other) |
545 : name = other.name, | 547 : name = other.name, |
546 version = other.version, | 548 version = other.version, |
547 _refs = new Map<String, PackageRef>.from(other._refs); | 549 _refs = new Map<String, PackageRef>.from(other._refs); |
548 | 550 |
(...skipping 20 matching lines...) Expand all Loading... |
569 | 571 |
570 _refs[package] = ref; | 572 _refs[package] = ref; |
571 } | 573 } |
572 | 574 |
573 /// Returns the name of a package whose constraint source and description | 575 /// Returns the name of a package whose constraint source and description |
574 /// all other constraints must match. Returns null if there are no | 576 /// all other constraints must match. Returns null if there are no |
575 /// requirements on new constraints. | 577 /// requirements on new constraints. |
576 String _requiredDepender() { | 578 String _requiredDepender() { |
577 if (_refs.isEmpty) return null; | 579 if (_refs.isEmpty) return null; |
578 | 580 |
579 var dependers = _refs.keys; | 581 var dependers = _refs.keys.toList(); |
580 if (dependers.length == 1) { | 582 if (dependers.length == 1) { |
581 var depender = dependers[0]; | 583 var depender = dependers[0]; |
582 if (_refs[depender].source is RootSource) return null; | 584 if (_refs[depender].source is RootSource) return null; |
583 return depender; | 585 return depender; |
584 } | 586 } |
585 | 587 |
586 return dependers[1]; | 588 return dependers[1]; |
587 } | 589 } |
588 | 590 |
589 /// Removes the constraint from [package] onto this. | 591 /// Removes the constraint from [package] onto this. |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
693 final String depender2; | 695 final String depender2; |
694 final description2; | 696 final description2; |
695 | 697 |
696 DescriptionMismatchException(this.package, this.depender1, this.description1, | 698 DescriptionMismatchException(this.package, this.depender1, this.description1, |
697 this.depender2, this.description2); | 699 this.depender2, this.description2); |
698 | 700 |
699 String toString() { | 701 String toString() { |
700 // TODO(nweiz): Dump descriptions to YAML when that's supported. | 702 // TODO(nweiz): Dump descriptions to YAML when that's supported. |
701 return "Incompatible dependencies on '$package':\n" | 703 return "Incompatible dependencies on '$package':\n" |
702 "- '$depender1' depends on it with description " | 704 "- '$depender1' depends on it with description " |
703 "${JSON.stringify(description1)}\n" | 705 "${json.stringify(description1)}\n" |
704 "- '$depender2' depends on it with description " | 706 "- '$depender2' depends on it with description " |
705 "${JSON.stringify(description2)}"; | 707 "${json.stringify(description2)}"; |
706 } | 708 } |
707 } | 709 } |
OLD | NEW |