Chromium Code Reviews| 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 Future<Map<String, Version>> resolveVersions( | |
|
nweiz
2012/06/18 18:29:19
This could use some docs.
| |
| 48 SourceRegistry sources, Package root) { | |
| 49 return new VersionSolver(sources).solve(root); | |
| 50 } | |
| 51 | |
| 52 class VersionSolver { | |
| 53 final SourceRegistry _sources; | |
| 54 final PubspecCache _pubspecs; | |
| 55 final Map<String, Dependency> _packages; | |
| 56 final Queue<WorkItem> _work; | |
| 57 int _numIterations = 0; | |
| 58 | |
| 59 VersionSolver(SourceRegistry sources) | |
| 60 : _sources = sources, | |
| 61 _pubspecs = new PubspecCache(sources), | |
| 62 _packages = <Dependency>{}, | |
| 63 _work = new Queue<WorkItem>(); | |
| 64 | |
| 65 Future<Map<String, Version>> solve(Package root) { | |
| 66 // Kick off the work by adding the root package at its concrete version to | |
| 67 // the dependency graph. | |
| 68 _pubspecs.cache(root); | |
| 69 enqueue(new ChangeConstraint('(entrypoint)', root.name, root.version)); | |
|
nweiz
2012/06/18 18:29:19
What happens if some other package depends on the
Bob Nystrom
2012/06/20 01:40:04
There TODOs to add tests for that. Added tests. Th
| |
| 70 | |
| 71 Future processNextWorkItem(_) { | |
| 72 while (true) { | |
| 73 // Stop if we are done. | |
| 74 if (_work.isEmpty()) return new Future.immediate(buildResults()); | |
| 75 | |
| 76 // If we appear to be stuck in a loop, then we probably have an unstable | |
| 77 // graph, bail. We guess this based on a rough heuristic that it should | |
| 78 // only take a certain number of steps to solve a graph with a given | |
| 79 // number of connections. | |
| 80 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune | |
| 81 // when we have a better picture of real-world package topologies. | |
|
nweiz
2012/06/18 18:29:19
Are we planning to try to manually catch infinite
Bob Nystrom
2012/06/20 01:40:04
Right now, it's the sole method. Catching loops wi
nweiz
2012/06/20 21:08:48
It seems like a nice thing to do once we run out o
| |
| 82 _numIterations++; | |
| 83 if (_numIterations > Math.max(50, _packages.length * 5)) { | |
| 84 throw new CouldNotSolveException(); | |
| 85 } | |
| 86 | |
| 87 // Run the first work item. | |
| 88 var future = _work.removeFirst().process(this); | |
| 89 | |
| 90 // If we have an async operation to perform, chain the loop to resume | |
| 91 // when it's done. Otherwise, just loop synchronously. | |
|
nweiz
2012/06/18 18:29:19
Wouldn't it be possible to process other work item
Bob Nystrom
2012/06/20 01:40:04
In theory yes, but that adds a lot of complexity I
nweiz
2012/06/20 21:08:48
Probably not... that could also (potentially) make
| |
| 92 if (future != null) { | |
| 93 return future.chain(processNextWorkItem); | |
| 94 } | |
| 95 } | |
| 96 } | |
| 97 | |
| 98 return processNextWorkItem(null); | |
| 99 } | |
| 100 | |
| 101 void enqueue(WorkItem work) { | |
| 102 _work.add(work); | |
| 103 } | |
| 104 | |
| 105 Dependency getDependency(String package) { | |
| 106 // There can be unused dependencies in the graph, so just create an empty | |
| 107 // one if needed. | |
| 108 _packages.putIfAbsent(package, () => new Dependency()); | |
| 109 return _packages[package]; | |
| 110 } | |
| 111 | |
| 112 /** | |
| 113 * Sets the best selected version of [package] to [version]. | |
| 114 */ | |
| 115 void setVersion(String package, Version version) { | |
| 116 _packages[package].version = version; | |
| 117 } | |
| 118 | |
| 119 Map<String, Version> buildResults() { | |
| 120 var results = <Version>{}; | |
| 121 _packages.forEach((name, dependency) { | |
| 122 if (dependency.isDependedOn) { | |
| 123 results[name] = dependency.version; | |
| 124 } | |
| 125 }); | |
| 126 | |
| 127 return results; | |
| 128 } | |
| 129 } | |
| 130 | |
| 131 /** | |
| 132 * The constraint solver works by iteratively processing a queue work items. | |
|
nweiz
2012/06/18 18:29:19
"a queue of work items"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 133 * Each item is a single atomic change to the dependency graph. Handling them | |
| 134 * in a queue lets us handle asynchrony (resolving versions requires information | |
| 135 * from servers) as well as avoiding deeply nest recursion. | |
|
nweiz
2012/06/18 18:29:19
"avoid deeply nested recursion"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 136 */ | |
| 137 interface WorkItem { | |
| 138 /** | |
| 139 * Processes this work item. Returns a future that completes when the work is | |
| 140 * done. If `null` is returned, that means the work has completed | |
| 141 * synchronously and the next item can be started immediately. | |
| 142 */ | |
| 143 Future process(VersionSolver solver); | |
| 144 } | |
| 145 | |
| 146 /** | |
| 147 * The best selected version for [package] has changed to [version]. | |
| 148 * If the previous version of the package is `null`, that means the package is | |
| 149 * being added to the graph. If [version] is `null`, it is being removed. | |
| 150 */ | |
| 151 class ChangeVersion implements WorkItem { | |
| 152 /** | |
| 153 * The package whose version is changing. | |
| 154 */ | |
| 155 final String package; | |
| 156 | |
| 157 /** | |
| 158 * The new selected version. | |
| 159 */ | |
| 160 final Version version; | |
| 161 | |
| 162 ChangeVersion(this.package, this.version); | |
| 163 | |
| 164 Future process(VersionSolver solver) { | |
| 165 var oldVersion = solver.getDependency(package).version; | |
| 166 solver.setVersion(package, version); | |
| 167 | |
| 168 // The dependencies between the old and new version may be different. Walk | |
| 169 // them both and update any constraints that differ between the two. | |
| 170 return getDependencies(solver, oldVersion).chain((oldDependencies) { | |
| 171 return getDependencies(solver, version).transform((newDependencies) { | |
|
nweiz
2012/06/18 18:29:19
You could increase parallelism and reduce nesting
Bob Nystrom
2012/06/20 01:40:04
I think I had that at one point but it felt like o
nweiz
2012/06/20 21:08:48
I think it would be cleaner.
Bob Nystrom
2012/06/20 22:29:54
Done.
| |
| 172 for (var dependency in oldDependencies.getValues()) { | |
| 173 var constraint; | |
| 174 if (newDependencies.containsKey(dependency.name)) { | |
| 175 // The dependency is in both versions of this package, but its | |
| 176 // constraint may have changed. | |
| 177 constraint = newDependencies[dependency.name].version; | |
|
nweiz
2012/06/18 18:29:19
constraint = newDependencies.remove(dependency.nam
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 178 newDependencies.remove(dependency.name); | |
| 179 } else { | |
| 180 // The dependency is not in the new version of the package, so just | |
| 181 // remove its constraint. | |
| 182 constraint = null; | |
| 183 } | |
| 184 | |
| 185 solver.enqueue(new ChangeConstraint( | |
| 186 package, dependency.name, constraint)); | |
| 187 } | |
| 188 | |
| 189 // Everything that's left is a depdendency that's only in the new | |
| 190 // version of the package. | |
| 191 for (var dependency in newDependencies.getValues()) { | |
| 192 solver.enqueue(new ChangeConstraint( | |
| 193 package, dependency.name, dependency.version)); | |
| 194 } | |
| 195 }); | |
| 196 }); | |
| 197 } | |
| 198 | |
| 199 /** | |
| 200 * Get the dependencies that [package] has at [version]. | |
| 201 */ | |
| 202 Future<Map<String, PackageRef>> getDependencies(VersionSolver solver, | |
|
nweiz
2012/06/18 18:29:19
This name is confusing, since the method doesn't h
Bob Nystrom
2012/06/20 01:40:04
Done. What I wouldn't give for a shorter synonym f
nweiz
2012/06/20 21:08:48
You could always use "dep".
Bob Nystrom
2012/06/20 22:29:54
For some reason, that bugs me. Maybe I just don't
nweiz
2012/06/20 23:47:45
You used "ref" ;).
I don't actually care either w
| |
| 203 Version version) { | |
| 204 // If there is no version, it means no package, so no dependencies. | |
| 205 if (version == null) { | |
| 206 return new Future<Map<String, PackageRef>>.immediate(<PackageRef>{}); | |
| 207 } | |
| 208 | |
| 209 return solver._pubspecs.load(package, version).transform((pubspec) { | |
| 210 var dependencies = <PackageRef>{}; | |
| 211 for (var dependency in pubspec.dependencies) { | |
| 212 dependencies[dependency.name] = dependency; | |
| 213 } | |
| 214 return dependencies; | |
| 215 }); | |
| 216 } | |
| 217 } | |
| 218 | |
| 219 /** | |
| 220 * The [VersionConstraint] that [package] places on depdendency [dependent] has | |
| 221 * changed from [from] to [to]. The [from] constraint may be `null` which means | |
| 222 * this is the first time [package] has depended on [dependent]. The [to] | |
| 223 * constraint may be `null` which means [package] no longer depends on | |
| 224 * [dependent]. | |
|
nweiz
2012/06/18 18:29:19
This is inaccurate. "[package]" and "[dependent]"
Bob Nystrom
2012/06/20 01:40:04
Stale comment is stale. This went through a few it
| |
| 225 */ | |
| 226 class ChangeConstraint implements WorkItem { | |
| 227 /** | |
| 228 * The package that has the dependency. | |
| 229 */ | |
| 230 final String from; | |
| 231 | |
| 232 /** | |
| 233 * The package being depended on. | |
| 234 */ | |
| 235 final String to; | |
|
nweiz
2012/06/18 18:29:19
I'm finding "from" and "to" confusing when reading
Bob Nystrom
2012/06/20 01:40:04
Changed to "depender" (which kind of sounds like s
| |
| 236 | |
| 237 /** | |
| 238 * The constraint that [from] places on [to]'s version. | |
| 239 */ | |
| 240 final VersionConstraint version; | |
|
nweiz
2012/06/18 18:29:19
I'd call this "constraint", since it's a VersionCo
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 241 | |
| 242 ChangeConstraint(this.from, this.to, this.version); | |
| 243 | |
| 244 Future process(VersionSolver solver) { | |
| 245 var dependency = solver.getDependency(to); | |
| 246 var oldConstraint = dependency.constraint; | |
| 247 dependency.setConstraint(from, version); | |
| 248 var newConstraint = dependency.constraint; | |
| 249 | |
| 250 // If we the package is over-constrained, i.e. the packages depending | |
|
nweiz
2012/06/18 18:29:19
"If the package is"
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 251 // have disjoint constraints, then stop. | |
| 252 if (newConstraint != null && newConstraint.isEmpty) { | |
| 253 throw new DisjointConstraintException(to); | |
| 254 } | |
| 255 | |
| 256 // If this constraint change didn't cause the overall constraint on the | |
| 257 // package to change, then we don't need to do any further work. | |
| 258 if (oldConstraint == newConstraint) return null; | |
| 259 | |
| 260 // If the dependency has been cut free from the graph, just remove it. | |
| 261 if (!dependency.isDependedOn) { | |
| 262 solver.enqueue(new ChangeVersion(to, null)); | |
| 263 return null; | |
| 264 } | |
| 265 | |
| 266 // The constraint has changed, so see what the best version of the package | |
| 267 // that meets the new constraint is. | |
| 268 // TODO(rnystrom): Should this always be the default source? | |
| 269 var source = solver._sources.defaultSource; | |
| 270 return source.findVersion(to, newConstraint).transform((best) { | |
| 271 // TODO(rnystrom): Better exception. | |
| 272 if (best == null) throw new NoVersionException(to, newConstraint); | |
| 273 | |
| 274 if (dependency.version != best) { | |
| 275 solver.enqueue(new ChangeVersion(to, best)); | |
| 276 } | |
| 277 }); | |
| 278 } | |
| 279 } | |
| 280 | |
| 281 // TODO(rnystrom): Instead of always pulling from the source (which will mean | |
| 282 // hitting a server, we should cache pubspecs of installed packages in the | |
| 283 // system cache. | |
|
nweiz
2012/06/18 18:29:19
Rather than making a separate cache of just pubspe
Bob Nystrom
2012/06/20 01:40:04
Agreed that we should do this.
| |
| 284 /** | |
| 285 * Maintains a synchronous cache of previously-loaded pubspecs. Used to | |
|
nweiz
2012/06/18 18:29:19
What do you mean by "synchronous"?
Bob Nystrom
2012/06/20 01:40:04
Stale comment. This used to have a synchronous "ge
| |
| 286 * avoid requesting the same pubspec from the server repeatedly. | |
| 287 */ | |
| 288 class PubspecCache { | |
| 289 final SourceRegistry _sources; | |
| 290 final Map<String, Map<Version, Pubspec>> _pubspecs; | |
| 291 | |
| 292 PubspecCache(this._sources) | |
| 293 : _pubspecs = <Map<Version, Pubspec>>{}; | |
| 294 | |
| 295 /** | |
| 296 * Adds the already loaded [package] to the cache. | |
| 297 */ | |
| 298 void cache(Package package) { | |
| 299 _pubspecs.putIfAbsent(package.name, () => new Map<Version, Pubspec>()); | |
| 300 _pubspecs[package.name][package.version] = package.pubspec; | |
| 301 } | |
| 302 | |
| 303 /** | |
| 304 * Loads the pubspec for [package] at [version]. | |
| 305 */ | |
| 306 Future<Pubspec> load(String package, Version version) { | |
| 307 // Complete immediately if it's already cached. | |
| 308 if (_pubspecs.containsKey(package) && | |
| 309 _pubspecs[package].containsKey(version)) { | |
| 310 return new Future<Pubspec>.immediate(_pubspecs[package][version]); | |
| 311 } | |
| 312 | |
| 313 // TODO(rnystrom): Should this always be the default source? | |
| 314 var source = _sources.defaultSource; | |
| 315 return source.describe(package, version).transform((pubspec) { | |
| 316 // Cache it. | |
| 317 _pubspecs.putIfAbsent(package, () => new Map<Version, Pubspec>()); | |
| 318 _pubspecs[package][version] = pubspec; | |
| 319 | |
| 320 return pubspec; | |
| 321 }); | |
| 322 } | |
| 323 } | |
| 324 | |
| 325 /** | |
| 326 * Describes one [Package] in the [DependencyGraph] and keeps track of which | |
| 327 * packages depend on it and what [VersionConstraint]s they place on it. | |
| 328 */ | |
| 329 class Dependency { | |
| 330 /** | |
| 331 * The currently selected best version for this dependency. | |
| 332 */ | |
| 333 Version version; | |
| 334 | |
| 335 /** | |
| 336 * The constraints that depending packages have placed on this one. | |
| 337 */ | |
| 338 final Map<String, VersionConstraint> _constraints; | |
| 339 | |
| 340 /** | |
| 341 * Gets whether or not any other packages are currently depending on this | |
| 342 * one. If `false`, then it means this package is not part of the dependency | |
| 343 * graph and should be omitted. | |
| 344 */ | |
| 345 bool get isDependedOn() => !_constraints.isEmpty(); | |
| 346 | |
| 347 /** | |
| 348 * Gets the overall constraint that all packages are placing on this one. | |
| 349 * If no packages have a constraint on this one (which can happen when this | |
| 350 * package is in the process of being added to the graph), returns `null`. | |
| 351 */ | |
| 352 VersionConstraint get constraint() { | |
| 353 if (_constraints.isEmpty()) return null; | |
| 354 return new VersionConstraint.intersect(_constraints.getValues()); | |
| 355 } | |
| 356 | |
| 357 Dependency() | |
| 358 : _constraints = <VersionConstraint>{}; | |
| 359 | |
| 360 /** | |
| 361 * Places [constraint] from [package] onto this. | |
| 362 */ | |
| 363 void setConstraint(String package, VersionConstraint constraint) { | |
|
nweiz
2012/06/18 18:29:19
"addConstraint" would be a clearer name. It's true
Bob Nystrom
2012/06/20 01:40:04
Changed to "placeConstraint". :)
| |
| 364 if (constraint == null) { | |
| 365 _constraints.remove(package); | |
| 366 } else { | |
| 367 _constraints[package] = constraint; | |
| 368 } | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 // TODO(rnystrom): Report the last of depending packages and their constraints. | |
| 373 /** | |
| 374 * Exception thrown when the [VersionConstraint] used to match a package is | |
| 375 * valid (i.e. non-empty), but there are no released versions of the package | |
| 376 * that fit that constraint. | |
| 377 */ | |
| 378 class NoVersionException implements Exception { | |
| 379 final String package; | |
| 380 final VersionConstraint version; | |
|
nweiz
2012/06/18 18:29:19
s/version/constraint/.
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
| 381 | |
| 382 NoVersionException(this.package, this.version); | |
| 383 | |
| 384 String toString() => | |
| 385 "Package '$package' has no versions that match constraint $version."; | |
| 386 } | |
| 387 | |
| 388 // TODO(rnystrom): Report the last of depending packages and their constraints. | |
| 389 /** | |
| 390 * Exception thrown when the [VersionConstraint] used to match a package is | |
| 391 * the empty set: in other words, multiple packages depend on it and have | |
| 392 * conflicting constraints that have no overlap. | |
| 393 */ | |
| 394 class DisjointConstraintException implements Exception { | |
| 395 final String package; | |
| 396 | |
| 397 DisjointConstraintException(this.package); | |
| 398 | |
| 399 String toString() => | |
| 400 "Package '$package' has disjoint constraints."; | |
| 401 } | |
| 402 | |
| 403 /** | |
| 404 * Exception thrown when the [VersionSolver] fails to find a solution after a | |
| 405 * certain number of iterations. | |
| 406 */ | |
| 407 class CouldNotSolveException implements Exception { | |
| 408 CouldNotSolveException(); | |
| 409 | |
| 410 String toString() => | |
| 411 "Could not find a solution that met all version constraints."; | |
| 412 } | |
| OLD | NEW |