| 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 15 matching lines...) Expand all Loading... |
| 26 /// this package and that dependency. | 26 /// this package and that dependency. |
| 27 /// | 27 /// |
| 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_solver1; |
| 37 | 37 |
| 38 import 'dart:async'; | 38 import 'dart:async'; |
| 39 import 'dart:collection' show Queue; | 39 import 'dart:collection' show Queue; |
| 40 import 'dart:json' as json; | 40 import 'dart:json' as json; |
| 41 import 'dart:math'; | 41 import 'dart:math'; |
| 42 import 'lock_file.dart'; | 42 import 'lock_file.dart'; |
| 43 import 'log.dart' as log; | 43 import 'log.dart' as log; |
| 44 import 'package.dart'; | 44 import 'package.dart'; |
| 45 import 'pubspec.dart'; | 45 import 'pubspec.dart'; |
| 46 import 'source.dart'; | 46 import 'source.dart'; |
| 47 import 'source_registry.dart'; | 47 import 'source_registry.dart'; |
| 48 import 'utils.dart'; | 48 import 'utils.dart'; |
| 49 import 'version.dart'; | 49 import 'version.dart'; |
| 50 import 'version_solver.dart'; |
| 50 | 51 |
| 51 /// Attempts to select the best concrete versions for all of the transitive | 52 class GreedyVersionSolver extends VersionSolver { |
| 52 /// dependencies of [root] taking into account all of the [VersionConstraint]s | 53 final _packages = <String, DependencyNode>{}; |
| 53 /// that those dependencies place on each other and the requirements imposed by | 54 final _work = new Queue<WorkItem>(); |
| 54 /// [lockFile]. If successful, completes to a [Map] that maps package names to | |
| 55 /// the selected version for that package. If it fails, the future will complete | |
| 56 /// with a [NoVersionException], [DisjointConstraintException], or | |
| 57 /// [CouldNotSolveException]. | |
| 58 Future<List<PackageId>> resolveVersions(SourceRegistry sources, Package root, | |
| 59 LockFile lockFile) { | |
| 60 log.message('Resolving dependencies...'); | |
| 61 return new VersionSolver(sources, root, lockFile).solve(); | |
| 62 } | |
| 63 | |
| 64 class VersionSolver { | |
| 65 final SourceRegistry _sources; | |
| 66 final Package _root; | |
| 67 final LockFile lockFile; | |
| 68 final PubspecCache _pubspecs; | |
| 69 final Map<String, Dependency> _packages; | |
| 70 final Queue<WorkItem> _work; | |
| 71 int _numIterations = 0; | 55 int _numIterations = 0; |
| 72 | 56 |
| 73 VersionSolver(SourceRegistry sources, this._root, this.lockFile) | 57 GreedyVersionSolver(SourceRegistry sources, Package root, LockFile lockFile, |
| 74 : _sources = sources, | 58 List<String> useLatest) |
| 75 _pubspecs = new PubspecCache(sources), | 59 : super(sources, root, lockFile, useLatest); |
| 76 _packages = <String, Dependency>{}, | |
| 77 _work = new Queue<WorkItem>(); | |
| 78 | 60 |
| 79 /// Tell the version solver to use the most recent version of [package] that | 61 void forceLatestVersion(String package) { |
| 80 /// exists in whatever source it's installed from. If that version violates | |
| 81 /// constraints imposed by other dependencies, an error will be raised when | |
| 82 /// solving the versions, even if an earlier compatible version exists. | |
| 83 void useLatestVersion(String package) { | |
| 84 // TODO(nweiz): How do we want to detect and handle unknown dependencies | 62 // TODO(nweiz): How do we want to detect and handle unknown dependencies |
| 85 // here? | 63 // here? |
| 86 getDependency(package).useLatestVersion = true; | 64 getDependency(package).useLatestVersion = true; |
| 87 lockFile.packages.remove(package); | |
| 88 } | 65 } |
| 89 | 66 |
| 90 Future<List<PackageId>> solve() { | 67 Future<List<PackageId>> runSolver() { |
| 91 // Kick off the work by adding the root package at its concrete version to | 68 // Kick off the work by adding the root package at its concrete version to |
| 92 // the dependency graph. | 69 // the dependency graph. |
| 93 var ref = new PackageRef.root(_root); | 70 var ref = new PackageRef.root(root); |
| 94 enqueue(new AddConstraint('(entrypoint)', ref)); | 71 enqueue(new AddConstraint('(entrypoint)', ref)); |
| 95 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); | 72 cache.cache(ref.atVersion(root.version), root.pubspec); |
| 96 | 73 |
| 97 Future processNextWorkItem(_) { | 74 Future processNextWorkItem(_) { |
| 98 while (true) { | 75 while (true) { |
| 99 // Stop if we are done. | 76 // Stop if we are done. |
| 100 if (_work.isEmpty) return new Future.immediate(buildResults()); | 77 if (_work.isEmpty) return new Future.immediate(buildResults()); |
| 101 | 78 |
| 102 // If we appear to be stuck in a loop, then we probably have an unstable | 79 // If we appear to be stuck in a loop, then we probably have an unstable |
| 103 // graph, bail. We guess this based on a rough heuristic that it should | 80 // graph, bail. We guess this based on a rough heuristic that it should |
| 104 // only take a certain number of steps to solve a graph with a given | 81 // only take a certain number of steps to solve a graph with a given |
| 105 // number of connections. | 82 // number of connections. |
| (...skipping 15 matching lines...) Expand all Loading... |
| 121 } | 98 } |
| 122 } | 99 } |
| 123 | 100 |
| 124 return processNextWorkItem(null); | 101 return processNextWorkItem(null); |
| 125 } | 102 } |
| 126 | 103 |
| 127 void enqueue(WorkItem work) { | 104 void enqueue(WorkItem work) { |
| 128 _work.add(work); | 105 _work.add(work); |
| 129 } | 106 } |
| 130 | 107 |
| 131 Dependency getDependency(String package) { | 108 DependencyNode getDependency(String package) { |
| 132 // There can be unused dependencies in the graph, so just create an empty | 109 // There can be unused dependencies in the graph, so just create an empty |
| 133 // one if needed. | 110 // one if needed. |
| 134 _packages.putIfAbsent(package, () => new Dependency(package)); | 111 _packages.putIfAbsent(package, () => new DependencyNode(package)); |
| 135 return _packages[package]; | 112 return _packages[package]; |
| 136 } | 113 } |
| 137 | 114 |
| 138 /// Sets the best selected version of [package] to [version]. | 115 /// Sets the best selected version of [package] to [version]. |
| 139 void setVersion(String package, Version version) { | 116 void setVersion(String package, Version version) { |
| 140 _packages[package].version = version; | 117 _packages[package].version = version; |
| 141 } | 118 } |
| 142 | 119 |
| 143 /// Returns the most recent version of [dependency] that satisfies all of its | 120 /// Returns the most recent version of [dependency] that satisfies all of its |
| 144 /// version constraints. | 121 /// version constraints. |
| 145 Future<Version> getBestVersion(Dependency dependency) { | 122 Future<Version> getBestVersion(DependencyNode dependency) { |
| 146 return dependency.getVersions().then((versions) { | 123 return cache.getVersions(dependency.name, |
| 124 dependency.source, dependency.description).then((versions) { |
| 147 var best = null; | 125 var best = null; |
| 148 for (var version in versions) { | 126 for (var ref in versions) { |
| 149 if (dependency.useLatestVersion || | 127 if (dependency.useLatestVersion || |
| 150 dependency.constraint.allows(version)) { | 128 dependency.constraint.allows(ref.version)) { |
| 151 if (best == null || version > best) best = version; | 129 if (best == null || ref.version > best) best = ref.version; |
| 152 } | 130 } |
| 153 } | 131 } |
| 154 | 132 |
| 155 // TODO(rnystrom): Better exception. | 133 // TODO(rnystrom): Better exception. |
| 156 if (best == null) { | 134 if (best == null) { |
| 157 if (tryUnlockDepender(dependency)) return null; | 135 if (tryUnlockDepender(dependency)) return null; |
| 158 throw new NoVersionException(dependency.name, dependency.constraint, | 136 throw new NoVersionException(dependency.name, dependency.constraint, |
| 159 dependency._refs); | 137 dependency.toList()); |
| 160 } else if (!dependency.constraint.allows(best)) { | 138 } else if (!dependency.constraint.allows(best)) { |
| 161 if (tryUnlockDepender(dependency)) return null; | 139 if (tryUnlockDepender(dependency)) return null; |
| 162 throw new CouldNotUpdateException( | 140 throw new CouldNotUpdateException( |
| 163 dependency.name, dependency.constraint, best); | 141 dependency.name, dependency.constraint, best); |
| 164 } | 142 } |
| 165 | 143 |
| 166 return best; | 144 return best; |
| 167 }); | 145 }); |
| 168 } | 146 } |
| 169 | 147 |
| 170 /// Looks for a package that depends (transitively) on [dependency] and has | 148 /// Looks for a package that depends (transitively) on [dependency] and has |
| 171 /// its version locked in the lockfile. If one is found, enqueues an | 149 /// its version locked in the lockfile. If one is found, enqueues an |
| 172 /// [UnlockPackage] work item for it and returns true. Otherwise, returns | 150 /// [UnlockPackage] work item for it and returns true. Otherwise, returns |
| 173 /// false. | 151 /// false. |
| 174 /// | 152 /// |
| 175 /// This does a breadth-first search; immediate dependers will be unlocked | 153 /// This does a breadth-first search; immediate dependers will be unlocked |
| 176 /// first, followed by transitive dependers. | 154 /// first, followed by transitive dependers. |
| 177 bool tryUnlockDepender(Dependency dependency, [Set<String> seen]) { | 155 bool tryUnlockDepender(DependencyNode dependency, [Set<String> seen]) { |
| 178 if (seen == null) seen = new Set(); | 156 if (seen == null) seen = new Set(); |
| 179 // Avoid an infinite loop if there are circular dependencies. | 157 // Avoid an infinite loop if there are circular dependencies. |
| 180 if (seen.contains(dependency.name)) return false; | 158 if (seen.contains(dependency.name)) return false; |
| 181 seen.add(dependency.name); | 159 seen.add(dependency.name); |
| 182 | 160 |
| 183 for (var dependerName in dependency.dependers) { | 161 for (var dependerName in dependency.dependers) { |
| 184 var depender = getDependency(dependerName); | 162 var depender = getDependency(dependerName); |
| 185 var locked = lockFile.packages[dependerName]; | 163 var locked = lockFile.packages[dependerName]; |
| 186 if (locked != null && depender.version == locked.version && | 164 if (locked != null && depender.version == locked.version && |
| 187 depender.source.name == locked.source.name) { | 165 depender.source.name == locked.source.name) { |
| (...skipping 27 matching lines...) Expand all Loading... |
| 215 } | 193 } |
| 216 | 194 |
| 217 /// The constraint solver works by iteratively processing a queue of work items. | 195 /// The constraint solver works by iteratively processing a queue of work items. |
| 218 /// Each item is a single atomic change to the dependency graph. Handling them | 196 /// Each item is a single atomic change to the dependency graph. Handling them |
| 219 /// in a queue lets us handle asynchrony (resolving versions requires | 197 /// in a queue lets us handle asynchrony (resolving versions requires |
| 220 /// information from servers) as well as avoid deeply nested recursion. | 198 /// information from servers) as well as avoid deeply nested recursion. |
| 221 abstract class WorkItem { | 199 abstract class WorkItem { |
| 222 /// Processes this work item. Returns a future that completes when the work is | 200 /// Processes this work item. Returns a future that completes when the work is |
| 223 /// done. If `null` is returned, that means the work has completed | 201 /// done. If `null` is returned, that means the work has completed |
| 224 /// synchronously and the next item can be started immediately. | 202 /// synchronously and the next item can be started immediately. |
| 225 Future process(VersionSolver solver); | 203 Future process(GreedyVersionSolver solver); |
| 226 } | 204 } |
| 227 | 205 |
| 228 /// The best selected version for a package has changed to [version]. If the | 206 /// The best selected version for a package has changed to [version]. If the |
| 229 /// previous version of the package is `null`, that means the package is being | 207 /// previous version of the package is `null`, that means the package is being |
| 230 /// added to the graph. If [version] is `null`, it is being removed. | 208 /// added to the graph. If [version] is `null`, it is being removed. |
| 231 class ChangeVersion implements WorkItem { | 209 class ChangeVersion implements WorkItem { |
| 232 /// The name of the package whose version is being changed. | 210 /// The name of the package whose version is being changed. |
| 233 final String package; | 211 final String package; |
| 234 | 212 |
| 235 /// The source of the package whose version is changing. | 213 /// The source of the package whose version is changing. |
| 236 final Source source; | 214 final Source source; |
| 237 | 215 |
| 238 /// The description identifying the package whose version is changing. | 216 /// The description identifying the package whose version is changing. |
| 239 final description; | 217 final description; |
| 240 | 218 |
| 241 /// The new selected version. | 219 /// The new selected version. |
| 242 final Version version; | 220 final Version version; |
| 243 | 221 |
| 244 ChangeVersion(this.package, this.source, this.description, this.version); | 222 ChangeVersion(this.package, this.source, this.description, this.version); |
| 245 | 223 |
| 246 Future process(VersionSolver solver) { | 224 Future process(GreedyVersionSolver solver) { |
| 247 log.fine("Changing $package to version $version."); | 225 log.fine("Changing $package to version $version."); |
| 248 | 226 |
| 249 var dependency = solver.getDependency(package); | 227 var dependency = solver.getDependency(package); |
| 250 var oldVersion = dependency.version; | 228 var oldVersion = dependency.version; |
| 251 solver.setVersion(package, version); | 229 solver.setVersion(package, version); |
| 252 | 230 |
| 253 // The dependencies between the old and new version may be different. Walk | 231 // The dependencies between the old and new version may be different. Walk |
| 254 // them both and update any constraints that differ between the two. | 232 // them both and update any constraints that differ between the two. |
| 255 return Future.wait([ | 233 return Future.wait([ |
| 256 getDependencyRefs(solver, oldVersion), | 234 getDependencyRefs(solver, oldVersion), |
| (...skipping 25 matching lines...) Expand all Loading... |
| 282 /// Get the dependencies at [version] of the package being changed. | 260 /// Get the dependencies at [version] of the package being changed. |
| 283 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, | 261 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, |
| 284 Version version) { | 262 Version version) { |
| 285 // If there is no version, it means no package, so no dependencies. | 263 // If there is no version, it means no package, so no dependencies. |
| 286 if (version == null) { | 264 if (version == null) { |
| 287 return new Future<Map<String, PackageRef>>.immediate( | 265 return new Future<Map<String, PackageRef>>.immediate( |
| 288 <String, PackageRef>{}); | 266 <String, PackageRef>{}); |
| 289 } | 267 } |
| 290 | 268 |
| 291 var id = new PackageId(package, source, version, description); | 269 var id = new PackageId(package, source, version, description); |
| 292 return solver._pubspecs.load(id).then((pubspec) { | 270 return solver.cache.getPubspec(id).then((pubspec) { |
| 293 var dependencies = <String, PackageRef>{}; | 271 var dependencies = <String, PackageRef>{}; |
| 294 for (var dependency in pubspec.dependencies) { | 272 for (var dependency in pubspec.dependencies) { |
| 295 dependencies[dependency.name] = dependency; | 273 dependencies[dependency.name] = dependency; |
| 296 } | 274 } |
| 297 | 275 |
| 298 // Include dev dependencies only from the root package. | 276 // Include dev dependencies only from the root package. |
| 299 if (id.isRoot) { | 277 if (id.isRoot) { |
| 300 for (var dependency in pubspec.devDependencies) { | 278 for (var dependency in pubspec.devDependencies) { |
| 301 dependencies[dependency.name] = dependency; | 279 dependencies[dependency.name] = dependency; |
| 302 } | 280 } |
| 303 } | 281 } |
| 304 | 282 |
| 305 return dependencies; | 283 return dependencies; |
| 306 }); | 284 }); |
| 307 } | 285 } |
| 308 } | 286 } |
| 309 | 287 |
| 310 /// A constraint that a depending package places on a dependent package has | 288 /// A constraint that a depending package places on a dependent package has |
| 311 /// changed. | 289 /// changed. |
| 312 /// | 290 /// |
| 313 /// This is an abstract class that contains logic for updating the dependency | 291 /// This is an abstract class that contains logic for updating the dependency |
| 314 /// graph once a dependency has changed. Changing the dependency is the | 292 /// graph once a dependency has changed. Changing the dependency is the |
| 315 /// responsibility of subclasses. | 293 /// responsibility of subclasses. |
| 316 abstract class ChangeConstraint implements WorkItem { | 294 abstract class ChangeConstraint implements WorkItem { |
| 317 Future process(VersionSolver solver); | 295 Future process(GreedyVersionSolver solver); |
| 318 | 296 |
| 319 void undo(VersionSolver solver); | 297 void undo(GreedyVersionSolver solver); |
| 320 | 298 |
| 321 Future _processChange(VersionSolver solver, Dependency oldDependency, | 299 Future _processChange(GreedyVersionSolver solver, |
| 322 Dependency newDependency) { | 300 DependencyNode oldDependency, |
| 301 DependencyNode newDependency) { |
| 323 var name = newDependency.name; | 302 var name = newDependency.name; |
| 324 var source = oldDependency.source != null ? | 303 var source = oldDependency.source != null ? |
| 325 oldDependency.source : newDependency.source; | 304 oldDependency.source : newDependency.source; |
| 326 var description = oldDependency.description != null ? | 305 var description = oldDependency.description != null ? |
| 327 oldDependency.description : newDependency.description; | 306 oldDependency.description : newDependency.description; |
| 328 var oldConstraint = oldDependency.constraint; | 307 var oldConstraint = oldDependency.constraint; |
| 329 var newConstraint = newDependency.constraint; | 308 var newConstraint = newDependency.constraint; |
| 330 | 309 |
| 331 // If the package is over-constrained, i.e. the packages depending have | 310 // If the package is over-constrained, i.e. the packages depending have |
| 332 // disjoint constraints, then try unlocking a depender that's locked by the | 311 // disjoint constraints, then try unlocking a depender that's locked by the |
| 333 // lockfile. If there are no remaining locked dependencies, throw an error. | 312 // lockfile. If there are no remaining locked dependencies, throw an error. |
| 334 if (newConstraint != null && newConstraint.isEmpty) { | 313 if (newConstraint != null && newConstraint.isEmpty) { |
| 335 if (solver.tryUnlockDepender(newDependency)) { | 314 if (solver.tryUnlockDepender(newDependency)) { |
| 336 undo(solver); | 315 undo(solver); |
| 337 return null; | 316 return null; |
| 338 } | 317 } |
| 339 | 318 |
| 340 throw new DisjointConstraintException(name, newDependency._refs); | 319 throw new DisjointConstraintException(name, newDependency.toList()); |
| 341 } | 320 } |
| 342 | 321 |
| 343 // If this constraint change didn't cause the overall constraint on the | 322 // If this constraint change didn't cause the overall constraint on the |
| 344 // package to change, then we don't need to do any further work. | 323 // package to change, then we don't need to do any further work. |
| 345 if (oldConstraint == newConstraint) return null; | 324 if (oldConstraint == newConstraint) return null; |
| 346 | 325 |
| 347 // If the dependency has been cut free from the graph, just remove it. | 326 // If the dependency has been cut free from the graph, just remove it. |
| 348 if (!newDependency.isDependedOn) { | 327 if (!newDependency.isDependedOn) { |
| 349 solver.enqueue(new ChangeVersion(name, source, description, null)); | 328 solver.enqueue(new ChangeVersion(name, source, description, null)); |
| 350 return null; | 329 return null; |
| 351 } | 330 } |
| 352 | 331 |
| 353 // If the dependency is on the root package, then we don't need to do | 332 // If the dependency is on the root package, then we don't need to do |
| 354 // anything since it's already at the best version. | 333 // anything since it's already at the best version. |
| 355 if (name == solver._root.name) { | 334 if (name == solver.root.name) { |
| 356 solver.enqueue(new ChangeVersion( | 335 solver.enqueue(new ChangeVersion( |
| 357 name, source, description, solver._root.version)); | 336 name, source, description, solver.root.version)); |
| 358 return null; | 337 return null; |
| 359 } | 338 } |
| 360 | 339 |
| 361 // If the dependency is on a package in the lockfile, use the lockfile's | 340 // If the dependency is on a package in the lockfile, use the lockfile's |
| 362 // version for that package if it's valid given the other constraints. | 341 // version for that package if it's valid given the other constraints. |
| 363 var lockedPackage = solver.lockFile.packages[name]; | 342 var lockedPackage = solver.lockFile.packages[name]; |
| 364 if (lockedPackage != null && newDependency.source == lockedPackage.source) { | 343 if (lockedPackage != null && newDependency.source == lockedPackage.source) { |
| 365 var lockedVersion = lockedPackage.version; | 344 var lockedVersion = lockedPackage.version; |
| 366 if (newConstraint.allows(lockedVersion)) { | 345 if (newConstraint.allows(lockedVersion)) { |
| 367 solver.enqueue( | 346 solver.enqueue( |
| (...skipping 19 matching lines...) Expand all Loading... |
| 387 /// The package that has the dependency. | 366 /// The package that has the dependency. |
| 388 final String depender; | 367 final String depender; |
| 389 | 368 |
| 390 /// The package being depended on and the constraints being placed on it. The | 369 /// The package being depended on and the constraints being placed on it. The |
| 391 /// source, version, and description in this ref are all considered | 370 /// source, version, and description in this ref are all considered |
| 392 /// constraints on the dependent package. | 371 /// constraints on the dependent package. |
| 393 final PackageRef ref; | 372 final PackageRef ref; |
| 394 | 373 |
| 395 AddConstraint(this.depender, this.ref); | 374 AddConstraint(this.depender, this.ref); |
| 396 | 375 |
| 397 Future process(VersionSolver solver) { | 376 Future process(GreedyVersionSolver solver) { |
| 398 log.fine("Adding $depender's constraint $ref."); | 377 log.fine("Adding $depender's constraint $ref."); |
| 399 | 378 |
| 400 var dependency = solver.getDependency(ref.name); | 379 var dependency = solver.getDependency(ref.name); |
| 401 var oldDependency = dependency.clone(); | 380 var oldDependency = dependency.clone(); |
| 402 dependency.placeConstraint(depender, ref); | 381 dependency.placeConstraint(depender, ref); |
| 403 return _processChange(solver, oldDependency, dependency); | 382 return _processChange(solver, oldDependency, dependency); |
| 404 } | 383 } |
| 405 | 384 |
| 406 void undo(VersionSolver solver) { | 385 void undo(GreedyVersionSolver solver) { |
| 407 solver.getDependency(ref.name).removeConstraint(depender); | 386 solver.getDependency(ref.name).removeConstraint(depender); |
| 408 } | 387 } |
| 409 } | 388 } |
| 410 | 389 |
| 411 /// [depender] is no longer placing a constraint on [dependent]. | 390 /// [depender] is no longer placing a constraint on [dependent]. |
| 412 class RemoveConstraint extends ChangeConstraint { | 391 class RemoveConstraint extends ChangeConstraint { |
| 413 /// The package that was placing a constraint on [dependent]. | 392 /// The package that was placing a constraint on [dependent]. |
| 414 String depender; | 393 String depender; |
| 415 | 394 |
| 416 /// The package that was being depended on. | 395 /// The package that was being depended on. |
| 417 String dependent; | 396 String dependent; |
| 418 | 397 |
| 419 /// The constraint that was removed. | 398 /// The constraint that was removed. |
| 420 PackageRef _removed; | 399 PackageRef _removed; |
| 421 | 400 |
| 422 RemoveConstraint(this.depender, this.dependent); | 401 RemoveConstraint(this.depender, this.dependent); |
| 423 | 402 |
| 424 Future process(VersionSolver solver) { | 403 Future process(GreedyVersionSolver solver) { |
| 425 log.fine("Removing $depender's constraint ($_removed) on $dependent."); | 404 log.fine("Removing $depender's constraint ($_removed) on $dependent."); |
| 426 | 405 |
| 427 var dependency = solver.getDependency(dependent); | 406 var dependency = solver.getDependency(dependent); |
| 428 var oldDependency = dependency.clone(); | 407 var oldDependency = dependency.clone(); |
| 429 _removed = dependency.removeConstraint(depender); | 408 _removed = dependency.removeConstraint(depender); |
| 430 return _processChange(solver, oldDependency, dependency); | 409 return _processChange(solver, oldDependency, dependency); |
| 431 } | 410 } |
| 432 | 411 |
| 433 void undo(VersionSolver solver) { | 412 void undo(GreedyVersionSolver solver) { |
| 434 solver.getDependency(dependent).placeConstraint(depender, _removed); | 413 solver.getDependency(dependent).placeConstraint(depender, _removed); |
| 435 } | 414 } |
| 436 } | 415 } |
| 437 | 416 |
| 438 /// [package]'s version is no longer constrained by the lockfile. | 417 /// [package]'s version is no longer constrained by the lockfile. |
| 439 class UnlockPackage implements WorkItem { | 418 class UnlockPackage implements WorkItem { |
| 440 /// The package being unlocked. | 419 /// The package being unlocked. |
| 441 Dependency package; | 420 DependencyNode package; |
| 442 | 421 |
| 443 UnlockPackage(this.package); | 422 UnlockPackage(this.package); |
| 444 | 423 |
| 445 Future process(VersionSolver solver) { | 424 Future process(GreedyVersionSolver solver) { |
| 446 log.fine("Unlocking ${package.name}."); | 425 log.fine("Unlocking ${package.name}."); |
| 447 | 426 |
| 448 solver.lockFile.packages.remove(package.name); | 427 solver.lockFile.packages.remove(package.name); |
| 449 return solver.getBestVersion(package).then((best) { | 428 return solver.getBestVersion(package).then((best) { |
| 450 if (best == null) return null; | 429 if (best == null) return null; |
| 451 solver.enqueue(new ChangeVersion( | 430 solver.enqueue(new ChangeVersion( |
| 452 package.name, package.source, package.description, best)); | 431 package.name, package.source, package.description, best)); |
| 453 }); | 432 }); |
| 454 } | 433 } |
| 455 } | 434 } |
| 456 | 435 |
| 457 // TODO(rnystrom): Instead of always pulling from the source (which will mean | |
| 458 // hitting a server), we should consider caching pubspecs of uninstalled | |
| 459 // packages in the system cache. | |
| 460 /// Maintains a cache of previously-loaded pubspecs. Used to avoid requesting | |
| 461 /// the same pubspec from the server repeatedly. | |
| 462 class PubspecCache { | |
| 463 final SourceRegistry _sources; | |
| 464 final Map<PackageId, Pubspec> _pubspecs; | |
| 465 | |
| 466 PubspecCache(this._sources) | |
| 467 : _pubspecs = new Map<PackageId, Pubspec>(); | |
| 468 | |
| 469 /// Caches [pubspec] as the [Pubspec] for the package identified by [id]. | |
| 470 void cache(PackageId id, Pubspec pubspec) { | |
| 471 _pubspecs[id] = pubspec; | |
| 472 } | |
| 473 | |
| 474 /// Loads the pubspec for the package identified by [id]. | |
| 475 Future<Pubspec> load(PackageId id) { | |
| 476 // Complete immediately if it's already cached. | |
| 477 if (_pubspecs.containsKey(id)) { | |
| 478 return new Future<Pubspec>.immediate(_pubspecs[id]); | |
| 479 } | |
| 480 | |
| 481 return id.describe().then((pubspec) { | |
| 482 // Cache it. | |
| 483 _pubspecs[id] = pubspec; | |
| 484 return pubspec; | |
| 485 }); | |
| 486 } | |
| 487 } | |
| 488 | |
| 489 /// Describes one [Package] in the [DependencyGraph] and keeps track of which | 436 /// Describes one [Package] in the [DependencyGraph] and keeps track of which |
| 490 /// packages depend on it and what constraints they place on it. | 437 /// packages depend on it and what constraints they place on it. |
| 491 class Dependency { | 438 class DependencyNode { |
| 492 /// The name of the this dependency's package. | 439 /// The name of the this dependency's package. |
| 493 final String name; | 440 final String name; |
| 494 | 441 |
| 495 /// The [PackageRefs] that represent constraints that depending packages have | 442 /// The [PackageRefs] that represent constraints that depending packages have |
| 496 /// placed on this one. | 443 /// placed on this one. |
| 497 final Map<String, PackageRef> _refs; | 444 final Map<String, PackageRef> _refs; |
| 498 | 445 |
| 499 /// The currently-selected best version for this dependency. | 446 /// The currently-selected best version for this dependency. |
| 500 Version version; | 447 Version version; |
| 501 | 448 |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 539 /// agree upon. | 486 /// agree upon. |
| 540 PackageRef _canonicalRef() { | 487 PackageRef _canonicalRef() { |
| 541 if (_refs.isEmpty) return null; | 488 if (_refs.isEmpty) return null; |
| 542 var refs = _refs.values; | 489 var refs = _refs.values; |
| 543 for (var ref in refs) { | 490 for (var ref in refs) { |
| 544 if (ref.isRoot) return ref; | 491 if (ref.isRoot) return ref; |
| 545 } | 492 } |
| 546 return refs.first; | 493 return refs.first; |
| 547 } | 494 } |
| 548 | 495 |
| 549 Dependency(this.name) | 496 DependencyNode(this.name) |
| 550 : _refs = <String, PackageRef>{}; | 497 : _refs = <String, PackageRef>{}; |
| 551 | 498 |
| 552 Dependency._clone(Dependency other) | 499 DependencyNode._clone(DependencyNode other) |
| 553 : name = other.name, | 500 : name = other.name, |
| 554 version = other.version, | 501 version = other.version, |
| 555 _refs = new Map<String, PackageRef>.from(other._refs); | 502 _refs = new Map<String, PackageRef>.from(other._refs); |
| 556 | 503 |
| 557 /// Creates a copy of this dependency. | 504 /// Creates a copy of this dependency. |
| 558 Dependency clone() => new Dependency._clone(this); | 505 DependencyNode clone() => new DependencyNode._clone(this); |
| 559 | |
| 560 /// Return a list of available versions for this dependency. | |
| 561 Future<List<Version>> getVersions() => source.getVersions(name, description); | |
| 562 | 506 |
| 563 /// Places [ref] as a constraint from [package] onto this. | 507 /// Places [ref] as a constraint from [package] onto this. |
| 564 void placeConstraint(String package, PackageRef ref) { | 508 void placeConstraint(String package, PackageRef ref) { |
| 565 var requiredDepender = _requiredDepender(); | 509 var requiredDepender = _requiredDepender(); |
| 566 if (requiredDepender != null) { | 510 if (requiredDepender != null) { |
| 567 var required = _refs[requiredDepender]; | 511 var required = _refs[requiredDepender]; |
| 568 if (required.source.name != ref.source.name) { | 512 if (required.source.name != ref.source.name) { |
| 569 throw new SourceMismatchException(name, | 513 throw new SourceMismatchException(name, |
| 570 requiredDepender, required.source, package, ref.source); | 514 requiredDepender, required.source, package, ref.source); |
| 571 } else if (!required.source.descriptionsEqual( | 515 } else if (!required.source.descriptionsEqual( |
| (...skipping 17 matching lines...) Expand all Loading... |
| 589 var depender = dependers[0]; | 533 var depender = dependers[0]; |
| 590 if (_refs[depender].isRoot) return null; | 534 if (_refs[depender].isRoot) return null; |
| 591 return depender; | 535 return depender; |
| 592 } | 536 } |
| 593 | 537 |
| 594 return dependers[1]; | 538 return dependers[1]; |
| 595 } | 539 } |
| 596 | 540 |
| 597 /// Removes the constraint from [package] onto this. | 541 /// Removes the constraint from [package] onto this. |
| 598 PackageRef removeConstraint(String package) => _refs.remove(package); | 542 PackageRef removeConstraint(String package) => _refs.remove(package); |
| 599 } | |
| 600 | 543 |
| 601 /// Exception thrown when the [VersionConstraint] used to match a package is | 544 /// Converts this to a list of [Dependency] objects like the error types |
| 602 /// valid (i.e. non-empty), but there are no released versions of the package | 545 /// expect. |
| 603 /// that fit that constraint. | 546 List<Dependency> toList() { |
| 604 class NoVersionException implements Exception { | 547 var result = <Dependency>[]; |
| 605 final String package; | 548 _refs.forEach((name, ref) { |
| 606 final VersionConstraint constraint; | 549 result.add(new Dependency(name, ref)); |
| 607 final Map<String, PackageRef> _dependencies; | 550 }); |
| 608 | 551 return result; |
| 609 NoVersionException(this.package, this.constraint, this._dependencies); | |
| 610 | |
| 611 String toString() { | |
| 612 var buffer = new StringBuffer(); | |
| 613 buffer.write("Package '$package' has no versions that match $constraint " | |
| 614 "derived from:\n"); | |
| 615 | |
| 616 var keys = new List.from(_dependencies.keys); | |
| 617 keys.sort(); | |
| 618 | |
| 619 for (var key in keys) { | |
| 620 buffer.write("- '$key' depends on version " | |
| 621 "${_dependencies[key].constraint}\n"); | |
| 622 } | |
| 623 | |
| 624 return buffer.toString(); | |
| 625 } | 552 } |
| 626 } | 553 } |
| 627 | |
| 628 // TODO(rnystrom): Report the list of depending packages and their constraints. | |
| 629 /// Exception thrown when the most recent version of [package] must be selected, | |
| 630 /// but doesn't match the [VersionConstraint] imposed on the package. | |
| 631 class CouldNotUpdateException implements Exception { | |
| 632 final String package; | |
| 633 final VersionConstraint constraint; | |
| 634 final Version best; | |
| 635 | |
| 636 CouldNotUpdateException(this.package, this.constraint, this.best); | |
| 637 | |
| 638 String toString() => | |
| 639 "The latest version of '$package', $best, does not match $constraint."; | |
| 640 } | |
| 641 | |
| 642 /// Exception thrown when the [VersionConstraint] used to match a package is | |
| 643 /// the empty set: in other words, multiple packages depend on it and have | |
| 644 /// conflicting constraints that have no overlap. | |
| 645 class DisjointConstraintException implements Exception { | |
| 646 final String package; | |
| 647 final Map<String, PackageRef> _dependencies; | |
| 648 | |
| 649 DisjointConstraintException(this.package, this._dependencies); | |
| 650 | |
| 651 String toString() { | |
| 652 var buffer = new StringBuffer(); | |
| 653 buffer.write("Incompatible version constraints on '$package':\n"); | |
| 654 | |
| 655 var keys = new List.from(_dependencies.keys); | |
| 656 keys.sort(); | |
| 657 | |
| 658 for (var key in keys) { | |
| 659 buffer.write("- '$key' depends on version " | |
| 660 "${_dependencies[key].constraint}\n"); | |
| 661 } | |
| 662 | |
| 663 return buffer.toString(); | |
| 664 } | |
| 665 } | |
| 666 | |
| 667 /// Exception thrown when the [VersionSolver] fails to find a solution after a | |
| 668 /// certain number of iterations. | |
| 669 class CouldNotSolveException implements Exception { | |
| 670 CouldNotSolveException(); | |
| 671 | |
| 672 String toString() => | |
| 673 "Could not find a solution that met all version constraints."; | |
| 674 } | |
| 675 | |
| 676 /// Exception thrown when two packages with the same name but different sources | |
| 677 /// are depended upon. | |
| 678 class SourceMismatchException implements Exception { | |
| 679 final String package; | |
| 680 final String depender1; | |
| 681 final Source source1; | |
| 682 final String depender2; | |
| 683 final Source source2; | |
| 684 | |
| 685 SourceMismatchException(this.package, this.depender1, this.source1, | |
| 686 this.depender2, this.source2); | |
| 687 | |
| 688 String toString() { | |
| 689 return "Incompatible dependencies on '$package':\n" | |
| 690 "- '$depender1' depends on it from source '$source1'\n" | |
| 691 "- '$depender2' depends on it from source '$source2'"; | |
| 692 } | |
| 693 } | |
| 694 | |
| 695 /// Exception thrown when two packages with the same name and source but | |
| 696 /// different descriptions are depended upon. | |
| 697 class DescriptionMismatchException implements Exception { | |
| 698 final String package; | |
| 699 final String depender1; | |
| 700 final description1; | |
| 701 final String depender2; | |
| 702 final description2; | |
| 703 | |
| 704 DescriptionMismatchException(this.package, this.depender1, this.description1, | |
| 705 this.depender2, this.description2); | |
| 706 | |
| 707 String toString() { | |
| 708 // TODO(nweiz): Dump descriptions to YAML when that's supported. | |
| 709 return "Incompatible dependencies on '$package':\n" | |
| 710 "- '$depender1' depends on it with description " | |
| 711 "${json.stringify(description1)}\n" | |
| 712 "- '$depender2' depends on it with description " | |
| 713 "${json.stringify(description2)}"; | |
| 714 } | |
| 715 } | |
| OLD | NEW |