OLD | NEW |
(Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /** |
| 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 |
| 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 |
| 10 * reach a solution after a certain number of iterations, it assumes the |
| 11 * dependency graph is unstable and reports and error. |
| 12 * |
| 13 * There are two fundamental operations in the process of iterating over the |
| 14 * graph: |
| 15 * |
| 16 * 1. Changing the selected concrete version of some package. (This includes |
| 17 * adding and removing a package too, which is considering changing the |
| 18 * version to or from "none".) In other words, a node has changed. |
| 19 * 2. Changing the version constraint that one package places on another. In |
| 20 * other words, and edge has changed. |
| 21 * |
| 22 * Both of these events have a corresponding (potentional) async operation and |
| 23 * roughly cycle back and forth between each other. When we change the version |
| 24 * of package changes, we asynchronously load the pubspec for the new version. |
| 25 * When that's done, we compare the dependencies of the new version versus the |
| 26 * old one. For everything that differs, we change those constraints between |
| 27 * this package and that dependency. |
| 28 * |
| 29 * When a constraint on a package changes, we re-calculate the overall |
| 30 * constraint on that package. I.e. with a shared dependency, we intersect all |
| 31 * of the constraints that its depending packages place on it. If that overall |
| 32 * constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently |
| 33 * picked version for that package may fall outside of the new constraint. If |
| 34 * that happens, we find the new best version that meets the updated constraint |
| 35 * and then the change the package to use that version. That cycles back up to |
| 36 * the beginning again. |
| 37 */ |
| 38 #library('version_solver'); |
| 39 |
| 40 #import('package.dart'); |
| 41 #import('pubspec.dart'); |
| 42 #import('source.dart'); |
| 43 #import('source_registry.dart'); |
| 44 #import('utils.dart'); |
| 45 #import('version.dart'); |
| 46 |
| 47 /** |
| 48 * Attempts to select the best concrete versions for all of the transitive |
| 49 * dependencies of [root] taking into account all of the [VersionConstraint]s |
| 50 * that those dependencies place on each other. If successful, completes to a |
| 51 * [Map] that maps package names to the selected version for that package. If |
| 52 * it fails, the future will complete with a [NoVersionException], |
| 53 * [DisjointConstraintException], or [CouldNotSolveException]. |
| 54 */ |
| 55 Future<Map<String, Version>> resolveVersions( |
| 56 SourceRegistry sources, Package root) { |
| 57 return new VersionSolver(sources).solve(root); |
| 58 } |
| 59 |
| 60 class VersionSolver { |
| 61 final SourceRegistry _sources; |
| 62 final PubspecCache _pubspecs; |
| 63 final Map<String, Dependency> _packages; |
| 64 final Queue<WorkItem> _work; |
| 65 int _numIterations = 0; |
| 66 |
| 67 VersionSolver(SourceRegistry sources) |
| 68 : _sources = sources, |
| 69 _pubspecs = new PubspecCache(sources), |
| 70 _packages = <Dependency>{}, |
| 71 _work = new Queue<WorkItem>(); |
| 72 |
| 73 Future<Map<String, Version>> solve(Package root) { |
| 74 // Kick off the work by adding the root package at its concrete version to |
| 75 // the dependency graph. |
| 76 _pubspecs.cache(root); |
| 77 enqueue(new ChangeConstraint('(entrypoint)', root.name, root.version)); |
| 78 |
| 79 Future processNextWorkItem(_) { |
| 80 while (true) { |
| 81 // Stop if we are done. |
| 82 if (_work.isEmpty()) return new Future.immediate(buildResults()); |
| 83 |
| 84 // If we appear to be stuck in a loop, then we probably have an unstable |
| 85 // graph, bail. We guess this based on a rough heuristic that it should |
| 86 // only take a certain number of steps to solve a graph with a given |
| 87 // number of connections. |
| 88 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune |
| 89 // when we have a better picture of real-world package topologies. |
| 90 _numIterations++; |
| 91 if (_numIterations > Math.max(50, _packages.length * 5)) { |
| 92 throw new CouldNotSolveException(); |
| 93 } |
| 94 |
| 95 // Run the first work item. |
| 96 var future = _work.removeFirst().process(this); |
| 97 |
| 98 // If we have an async operation to perform, chain the loop to resume |
| 99 // when it's done. Otherwise, just loop synchronously. |
| 100 if (future != null) { |
| 101 return future.chain(processNextWorkItem); |
| 102 } |
| 103 } |
| 104 } |
| 105 |
| 106 return processNextWorkItem(null); |
| 107 } |
| 108 |
| 109 void enqueue(WorkItem work) { |
| 110 _work.add(work); |
| 111 } |
| 112 |
| 113 Dependency getDependency(String package) { |
| 114 // There can be unused dependencies in the graph, so just create an empty |
| 115 // one if needed. |
| 116 _packages.putIfAbsent(package, () => new Dependency()); |
| 117 return _packages[package]; |
| 118 } |
| 119 |
| 120 /** |
| 121 * Sets the best selected version of [package] to [version]. |
| 122 */ |
| 123 void setVersion(String package, Version version) { |
| 124 _packages[package].version = version; |
| 125 } |
| 126 |
| 127 Map<String, Version> buildResults() { |
| 128 var results = <Version>{}; |
| 129 _packages.forEach((name, dependency) { |
| 130 if (dependency.isDependedOn) { |
| 131 results[name] = dependency.version; |
| 132 } |
| 133 }); |
| 134 |
| 135 return results; |
| 136 } |
| 137 } |
| 138 |
| 139 /** |
| 140 * The constraint solver works by iteratively processing a queue of work items. |
| 141 * Each item is a single atomic change to the dependency graph. Handling them |
| 142 * in a queue lets us handle asynchrony (resolving versions requires information |
| 143 * from servers) as well as avoid deeply nested recursion. |
| 144 */ |
| 145 interface WorkItem { |
| 146 /** |
| 147 * Processes this work item. Returns a future that completes when the work is |
| 148 * done. If `null` is returned, that means the work has completed |
| 149 * synchronously and the next item can be started immediately. |
| 150 */ |
| 151 Future process(VersionSolver solver); |
| 152 } |
| 153 |
| 154 /** |
| 155 * The best selected version for [package] has changed to [version]. |
| 156 * If the previous version of the package is `null`, that means the package is |
| 157 * being added to the graph. If [version] is `null`, it is being removed. |
| 158 */ |
| 159 class ChangeVersion implements WorkItem { |
| 160 /** |
| 161 * The package whose version is changing. |
| 162 */ |
| 163 final String package; |
| 164 |
| 165 /** |
| 166 * The new selected version. |
| 167 */ |
| 168 final Version version; |
| 169 |
| 170 ChangeVersion(this.package, this.version); |
| 171 |
| 172 Future process(VersionSolver solver) { |
| 173 var oldVersion = solver.getDependency(package).version; |
| 174 solver.setVersion(package, version); |
| 175 |
| 176 // The dependencies between the old and new version may be different. Walk |
| 177 // them both and update any constraints that differ between the two. |
| 178 return Futures.wait([ |
| 179 getDependencyRefs(solver, oldVersion), |
| 180 getDependencyRefs(solver, version)]).transform((list) { |
| 181 var oldDependencies = list[0]; |
| 182 var newDependencies = list[1]; |
| 183 |
| 184 for (var dependency in oldDependencies.getValues()) { |
| 185 var constraint; |
| 186 if (newDependencies.containsKey(dependency.name)) { |
| 187 // The dependency is in both versions of this package, but its |
| 188 // constraint may have changed. |
| 189 constraint = newDependencies.remove(dependency.name).constraint; |
| 190 } else { |
| 191 // The dependency is not in the new version of the package, so just |
| 192 // remove its constraint. |
| 193 constraint = null; |
| 194 } |
| 195 |
| 196 solver.enqueue(new ChangeConstraint( |
| 197 package, dependency.name, constraint)); |
| 198 } |
| 199 |
| 200 // Everything that's left is a depdendency that's only in the new |
| 201 // version of the package. |
| 202 for (var dependency in newDependencies.getValues()) { |
| 203 solver.enqueue(new ChangeConstraint( |
| 204 package, dependency.name, dependency.constraint)); |
| 205 } |
| 206 }); |
| 207 } |
| 208 |
| 209 /** |
| 210 * Get the dependencies that [package] has at [version]. |
| 211 */ |
| 212 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, |
| 213 Version version) { |
| 214 // If there is no version, it means no package, so no dependencies. |
| 215 if (version == null) { |
| 216 return new Future<Map<String, PackageRef>>.immediate(<PackageRef>{}); |
| 217 } |
| 218 |
| 219 return solver._pubspecs.load(package, version).transform((pubspec) { |
| 220 var dependencies = <PackageRef>{}; |
| 221 for (var dependency in pubspec.dependencies) { |
| 222 dependencies[dependency.name] = dependency; |
| 223 } |
| 224 return dependencies; |
| 225 }); |
| 226 } |
| 227 } |
| 228 |
| 229 /** |
| 230 * The [VersionConstraint] that [depender] places on [dependent] has changed. |
| 231 */ |
| 232 class ChangeConstraint implements WorkItem { |
| 233 /** |
| 234 * The package that has the dependency. |
| 235 */ |
| 236 final String depender; |
| 237 |
| 238 /** |
| 239 * The package being depended on. |
| 240 */ |
| 241 final String dependent; |
| 242 |
| 243 /** |
| 244 * The constraint that [depender] places on [dependent]'s version. |
| 245 */ |
| 246 final VersionConstraint constraint; |
| 247 |
| 248 ChangeConstraint(this.depender, this.dependent, this.constraint); |
| 249 |
| 250 Future process(VersionSolver solver) { |
| 251 var dependency = solver.getDependency(dependent); |
| 252 var oldConstraint = dependency.constraint; |
| 253 dependency.placeConstraint(depender, constraint); |
| 254 var newConstraint = dependency.constraint; |
| 255 |
| 256 // If the package is over-constrained, i.e. the packages depending have |
| 257 // disjoint constraints, then stop. |
| 258 if (newConstraint != null && newConstraint.isEmpty) { |
| 259 throw new DisjointConstraintException(dependent); |
| 260 } |
| 261 |
| 262 // If this constraint change didn't cause the overall constraint on the |
| 263 // package to change, then we don't need to do any further work. |
| 264 if (oldConstraint == newConstraint) return null; |
| 265 |
| 266 // If the dependency has been cut free from the graph, just remove it. |
| 267 if (!dependency.isDependedOn) { |
| 268 solver.enqueue(new ChangeVersion(dependent, null)); |
| 269 return null; |
| 270 } |
| 271 |
| 272 // The constraint has changed, so see what the best version of the package |
| 273 // that meets the new constraint is. |
| 274 // TODO(rnystrom): Should this always be the default source? |
| 275 var source = solver._sources.defaultSource; |
| 276 return source.getVersions(dependent).transform((versions) { |
| 277 var best = null; |
| 278 for (var version in versions) { |
| 279 if (newConstraint.allows(version)) { |
| 280 if (best == null || version > best) best = version; |
| 281 } |
| 282 } |
| 283 |
| 284 // TODO(rnystrom): Better exception. |
| 285 if (best == null) throw new NoVersionException(dependent, newConstraint); |
| 286 |
| 287 if (dependency.version != best) { |
| 288 solver.enqueue(new ChangeVersion(dependent, best)); |
| 289 } |
| 290 }); |
| 291 } |
| 292 } |
| 293 |
| 294 // TODO(rnystrom): Instead of always pulling from the source (which will mean |
| 295 // hitting a server), we should consider caching pubspecs of uninstalled |
| 296 // packages in the system cache. |
| 297 /** |
| 298 * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting |
| 299 * the same pubspec from the server repeatedly. |
| 300 */ |
| 301 class PubspecCache { |
| 302 final SourceRegistry _sources; |
| 303 final Map<String, Map<Version, Pubspec>> _pubspecs; |
| 304 |
| 305 PubspecCache(this._sources) |
| 306 : _pubspecs = <Map<Version, Pubspec>>{}; |
| 307 |
| 308 /** |
| 309 * Adds the already loaded [package] to the cache. |
| 310 */ |
| 311 void cache(Package package) { |
| 312 _pubspecs.putIfAbsent(package.name, () => new Map<Version, Pubspec>()); |
| 313 _pubspecs[package.name][package.version] = package.pubspec; |
| 314 } |
| 315 |
| 316 /** |
| 317 * Loads the pubspec for [package] at [version]. |
| 318 */ |
| 319 Future<Pubspec> load(String package, Version version) { |
| 320 // Complete immediately if it's already cached. |
| 321 if (_pubspecs.containsKey(package) && |
| 322 _pubspecs[package].containsKey(version)) { |
| 323 return new Future<Pubspec>.immediate(_pubspecs[package][version]); |
| 324 } |
| 325 |
| 326 // TODO(rnystrom): Should this always be the default source? |
| 327 var source = _sources.defaultSource; |
| 328 return source.describe(package, version).transform((pubspec) { |
| 329 // Cache it. |
| 330 _pubspecs.putIfAbsent(package, () => new Map<Version, Pubspec>()); |
| 331 _pubspecs[package][version] = pubspec; |
| 332 |
| 333 return pubspec; |
| 334 }); |
| 335 } |
| 336 } |
| 337 |
| 338 /** |
| 339 * Describes one [Package] in the [DependencyGraph] and keeps track of which |
| 340 * packages depend on it and what [VersionConstraint]s they place on it. |
| 341 */ |
| 342 class Dependency { |
| 343 /** |
| 344 * The currently selected best version for this dependency. |
| 345 */ |
| 346 Version version; |
| 347 |
| 348 /** |
| 349 * The constraints that depending packages have placed on this one. |
| 350 */ |
| 351 final Map<String, VersionConstraint> _constraints; |
| 352 |
| 353 /** |
| 354 * Gets whether or not any other packages are currently depending on this |
| 355 * one. If `false`, then it means this package is not part of the dependency |
| 356 * graph and should be omitted. |
| 357 */ |
| 358 bool get isDependedOn() => !_constraints.isEmpty(); |
| 359 |
| 360 /** |
| 361 * Gets the overall constraint that all packages are placing on this one. |
| 362 * If no packages have a constraint on this one (which can happen when this |
| 363 * package is in the process of being added to the graph), returns `null`. |
| 364 */ |
| 365 VersionConstraint get constraint() { |
| 366 if (_constraints.isEmpty()) return null; |
| 367 return new VersionConstraint.intersect(_constraints.getValues()); |
| 368 } |
| 369 |
| 370 Dependency() |
| 371 : _constraints = <VersionConstraint>{}; |
| 372 |
| 373 /** |
| 374 * Places [constraint] from [package] onto this. |
| 375 */ |
| 376 void placeConstraint(String package, VersionConstraint constraint) { |
| 377 if (constraint == null) { |
| 378 _constraints.remove(package); |
| 379 } else { |
| 380 _constraints[package] = constraint; |
| 381 } |
| 382 } |
| 383 } |
| 384 |
| 385 // TODO(rnystrom): Report the last of depending packages and their constraints. |
| 386 /** |
| 387 * Exception thrown when the [VersionConstraint] used to match a package is |
| 388 * valid (i.e. non-empty), but there are no released versions of the package |
| 389 * that fit that constraint. |
| 390 */ |
| 391 class NoVersionException implements Exception { |
| 392 final String package; |
| 393 final VersionConstraint constraint; |
| 394 |
| 395 NoVersionException(this.package, this.constraint); |
| 396 |
| 397 String toString() => |
| 398 "Package '$package' has no versions that match $constraint."; |
| 399 } |
| 400 |
| 401 // TODO(rnystrom): Report the last of depending packages and their constraints. |
| 402 /** |
| 403 * Exception thrown when the [VersionConstraint] used to match a package is |
| 404 * the empty set: in other words, multiple packages depend on it and have |
| 405 * conflicting constraints that have no overlap. |
| 406 */ |
| 407 class DisjointConstraintException implements Exception { |
| 408 final String package; |
| 409 |
| 410 DisjointConstraintException(this.package); |
| 411 |
| 412 String toString() => |
| 413 "Package '$package' has disjoint constraints."; |
| 414 } |
| 415 |
| 416 /** |
| 417 * Exception thrown when the [VersionSolver] fails to find a solution after a |
| 418 * certain number of iterations. |
| 419 */ |
| 420 class CouldNotSolveException implements Exception { |
| 421 CouldNotSolveException(); |
| 422 |
| 423 String toString() => |
| 424 "Could not find a solution that met all version constraints."; |
| 425 } |
OLD | NEW |