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:math' as math; |
41 import 'dart:math'; | |
42 import 'lock_file.dart'; | |
43 import 'log.dart' as log; | |
44 import 'package.dart'; | |
45 import 'pubspec.dart'; | |
46 import 'source.dart'; | |
47 import 'source_registry.dart'; | |
48 import 'utils.dart'; | |
49 import 'version.dart'; | |
50 | 41 |
51 /// Attempts to select the best concrete versions for all of the transitive | 42 import '../lock_file.dart'; |
52 /// dependencies of [root] taking into account all of the [VersionConstraint]s | 43 import '../log.dart' as log; |
53 /// that those dependencies place on each other and the requirements imposed by | 44 import '../package.dart'; |
54 /// [lockFile]. If successful, completes to a [Map] that maps package names to | 45 import '../source.dart'; |
55 /// the selected version for that package. If it fails, the future will complete | 46 import '../source_registry.dart'; |
56 /// with a [NoVersionException], [DisjointConstraintException], or | 47 import '../version.dart'; |
57 /// [CouldNotSolveException]. | 48 import 'version_solver.dart'; |
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 | 49 |
64 class VersionSolver { | 50 class GreedyVersionSolver extends VersionSolver { |
65 final SourceRegistry _sources; | 51 final _packages = <String, DependencyNode>{}; |
66 final Package _root; | 52 final _work = new Queue<WorkItem>(); |
67 final LockFile lockFile; | |
68 final PubspecCache _pubspecs; | |
69 final Map<String, Dependency> _packages; | |
70 final Queue<WorkItem> _work; | |
71 int _numIterations = 0; | 53 int _numIterations = 0; |
72 | 54 |
73 VersionSolver(SourceRegistry sources, this._root, this.lockFile) | 55 GreedyVersionSolver(SourceRegistry sources, Package root, LockFile lockFile, |
74 : _sources = sources, | 56 List<String> useLatest) |
75 _pubspecs = new PubspecCache(sources), | 57 : super(sources, root, lockFile, useLatest); |
76 _packages = <String, Dependency>{}, | |
77 _work = new Queue<WorkItem>(); | |
78 | 58 |
79 /// Tell the version solver to use the most recent version of [package] that | 59 /// The non-backtracking solver always only tries one solution. |
80 /// exists in whatever source it's installed from. If that version violates | 60 int get attemptedSolutions => 1; |
81 /// constraints imposed by other dependencies, an error will be raised when | 61 |
82 /// solving the versions, even if an earlier compatible version exists. | 62 void forceLatestVersion(String package) { |
83 void useLatestVersion(String package) { | |
84 // TODO(nweiz): How do we want to detect and handle unknown dependencies | 63 // TODO(nweiz): How do we want to detect and handle unknown dependencies |
85 // here? | 64 // here? |
86 getDependency(package).useLatestVersion = true; | 65 getDependency(package).useLatestVersion = true; |
87 lockFile.packages.remove(package); | |
88 } | 66 } |
89 | 67 |
90 Future<List<PackageId>> solve() { | 68 Future<List<PackageId>> runSolver() { |
91 // Kick off the work by adding the root package at its concrete version to | 69 // Kick off the work by adding the root package at its concrete version to |
92 // the dependency graph. | 70 // the dependency graph. |
93 var ref = new PackageRef.root(_root); | 71 enqueue(new AddConstraint('(entrypoint)', new PackageRef.root(root))); |
94 enqueue(new AddConstraint('(entrypoint)', ref)); | |
95 _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec); | |
96 | 72 |
97 Future processNextWorkItem(_) { | 73 Future processNextWorkItem(_) { |
98 while (true) { | 74 while (true) { |
99 // Stop if we are done. | 75 // Stop if we are done. |
100 if (_work.isEmpty) return new Future.immediate(buildResults()); | 76 if (_work.isEmpty) return new Future.immediate(buildResults()); |
101 | 77 |
102 // If we appear to be stuck in a loop, then we probably have an unstable | 78 // 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 | 79 // 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 | 80 // only take a certain number of steps to solve a graph with a given |
105 // number of connections. | 81 // number of connections. |
106 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune | 82 // TODO(rnystrom): These numbers here are magic and arbitrary. Tune |
107 // when we have a better picture of real-world package topologies. | 83 // when we have a better picture of real-world package topologies. |
108 _numIterations++; | 84 _numIterations++; |
109 if (_numIterations > max(50, _packages.length * 5)) { | 85 if (_numIterations > math.max(50, _packages.length * 5)) { |
110 throw new CouldNotSolveException(); | 86 throw new CouldNotSolveException(); |
111 } | 87 } |
112 | 88 |
113 // Run the first work item. | 89 // Run the first work item. |
114 var future = _work.removeFirst().process(this); | 90 var future = _work.removeFirst().process(this); |
115 | 91 |
116 // If we have an async operation to perform, chain the loop to resume | 92 // If we have an async operation to perform, chain the loop to resume |
117 // when it's done. Otherwise, just loop synchronously. | 93 // when it's done. Otherwise, just loop synchronously. |
118 if (future != null) { | 94 if (future != null) { |
119 return future.then(processNextWorkItem); | 95 return future.then(processNextWorkItem); |
120 } | 96 } |
121 } | 97 } |
122 } | 98 } |
123 | 99 |
124 return processNextWorkItem(null); | 100 return processNextWorkItem(null); |
125 } | 101 } |
126 | 102 |
127 void enqueue(WorkItem work) { | 103 void enqueue(WorkItem work) { |
128 _work.add(work); | 104 _work.add(work); |
129 } | 105 } |
130 | 106 |
131 Dependency getDependency(String package) { | 107 DependencyNode getDependency(String package) { |
132 // There can be unused dependencies in the graph, so just create an empty | 108 // There can be unused dependencies in the graph, so just create an empty |
133 // one if needed. | 109 // one if needed. |
134 _packages.putIfAbsent(package, () => new Dependency(package)); | 110 _packages.putIfAbsent(package, () => new DependencyNode(package)); |
135 return _packages[package]; | 111 return _packages[package]; |
136 } | 112 } |
137 | 113 |
138 /// Sets the best selected version of [package] to [version]. | 114 /// Sets the best selected version of [package] to [version]. |
139 void setVersion(String package, Version version) { | 115 void setVersion(String package, Version version) { |
140 _packages[package].version = version; | 116 _packages[package].version = version; |
141 } | 117 } |
142 | 118 |
143 /// Returns the most recent version of [dependency] that satisfies all of its | 119 /// Returns the most recent version of [dependency] that satisfies all of its |
144 /// version constraints. | 120 /// version constraints. |
145 Future<Version> getBestVersion(Dependency dependency) { | 121 Future<Version> getBestVersion(DependencyNode dependency) { |
146 return dependency.getVersions().then((versions) { | 122 return cache.getVersions(dependency.name, |
| 123 dependency.source, dependency.description).then((versions) { |
147 var best = null; | 124 var best = null; |
148 for (var version in versions) { | 125 for (var ref in versions) { |
149 if (dependency.useLatestVersion || | 126 if (dependency.useLatestVersion || |
150 dependency.constraint.allows(version)) { | 127 dependency.constraint.allows(ref.version)) { |
151 if (best == null || version > best) best = version; | 128 if (best == null || ref.version > best) best = ref.version; |
152 } | 129 } |
153 } | 130 } |
154 | 131 |
155 // TODO(rnystrom): Better exception. | 132 // TODO(rnystrom): Better exception. |
156 if (best == null) { | 133 if (best == null) { |
157 if (tryUnlockDepender(dependency)) return null; | 134 if (tryUnlockDepender(dependency)) return null; |
158 throw new NoVersionException(dependency.name, dependency.constraint, | 135 throw new NoVersionException(dependency.name, dependency.constraint, |
159 dependency._refs); | 136 dependency.toList()); |
160 } else if (!dependency.constraint.allows(best)) { | 137 } else if (!dependency.constraint.allows(best)) { |
161 if (tryUnlockDepender(dependency)) return null; | 138 if (tryUnlockDepender(dependency)) return null; |
162 throw new CouldNotUpdateException( | 139 throw new CouldNotUpdateException( |
163 dependency.name, dependency.constraint, best); | 140 dependency.name, dependency.constraint, best); |
164 } | 141 } |
165 | 142 |
166 return best; | 143 return best; |
167 }); | 144 }); |
168 } | 145 } |
169 | 146 |
170 /// Looks for a package that depends (transitively) on [dependency] and has | 147 /// Looks for a package that depends (transitively) on [dependency] and has |
171 /// its version locked in the lockfile. If one is found, enqueues an | 148 /// its version locked in the lockfile. If one is found, enqueues an |
172 /// [UnlockPackage] work item for it and returns true. Otherwise, returns | 149 /// [UnlockPackage] work item for it and returns true. Otherwise, returns |
173 /// false. | 150 /// false. |
174 /// | 151 /// |
175 /// This does a breadth-first search; immediate dependers will be unlocked | 152 /// This does a breadth-first search; immediate dependers will be unlocked |
176 /// first, followed by transitive dependers. | 153 /// first, followed by transitive dependers. |
177 bool tryUnlockDepender(Dependency dependency, [Set<String> seen]) { | 154 bool tryUnlockDepender(DependencyNode dependency, [Set<String> seen]) { |
178 if (seen == null) seen = new Set(); | 155 if (seen == null) seen = new Set(); |
179 // Avoid an infinite loop if there are circular dependencies. | 156 // Avoid an infinite loop if there are circular dependencies. |
180 if (seen.contains(dependency.name)) return false; | 157 if (seen.contains(dependency.name)) return false; |
181 seen.add(dependency.name); | 158 seen.add(dependency.name); |
182 | 159 |
183 for (var dependerName in dependency.dependers) { | 160 for (var dependerName in dependency.dependers) { |
184 var depender = getDependency(dependerName); | 161 var depender = getDependency(dependerName); |
185 var locked = lockFile.packages[dependerName]; | 162 var locked = lockFile.packages[dependerName]; |
186 if (locked != null && depender.version == locked.version && | 163 if (locked != null && depender.version == locked.version && |
187 depender.source.name == locked.source.name) { | 164 depender.source.name == locked.source.name) { |
188 enqueue(new UnlockPackage(depender)); | 165 enqueue(new UnlockPackage(depender)); |
189 return true; | 166 return true; |
190 } | 167 } |
191 } | 168 } |
192 | 169 |
193 return dependency.dependers.map(getDependency).any((subdependency) => | 170 return dependency.dependers.map(getDependency).any((subdependency) => |
194 tryUnlockDepender(subdependency, seen)); | 171 tryUnlockDepender(subdependency, seen)); |
195 } | 172 } |
196 | 173 |
197 List<PackageId> buildResults() { | 174 List<PackageId> buildResults() { |
198 return _packages.values.where((dep) => dep.isDependedOn).map((dep) { | 175 return _packages.values |
199 var description = dep.description; | 176 .where((dep) => dep.isDependedOn) |
| 177 .map(_dependencyToPackageId) |
| 178 .toList(); |
| 179 } |
200 | 180 |
201 // If the lockfile contains a fully-resolved description for the package, | 181 PackageId _dependencyToPackageId(DependencyNode dep) { |
202 // use that. This allows e.g. Git to ensure that the same commit is used. | 182 var description = dep.description; |
203 var lockedPackage = lockFile.packages[dep.name]; | |
204 if (lockedPackage != null && lockedPackage.version == dep.version && | |
205 lockedPackage.source.name == dep.source.name && | |
206 dep.source.descriptionsEqual( | |
207 description, lockedPackage.description)) { | |
208 description = lockedPackage.description; | |
209 } | |
210 | 183 |
211 return new PackageId(dep.name, dep.source, dep.version, description); | 184 // If the lockfile contains a fully-resolved description for the package, |
212 }) | 185 // use that. This allows e.g. Git to ensure that the same commit is used. |
213 .toList(); | 186 var lockedPackage = lockFile.packages[dep.name]; |
| 187 if (lockedPackage != null && lockedPackage.version == dep.version && |
| 188 lockedPackage.source.name == dep.source.name && |
| 189 dep.source.descriptionsEqual( |
| 190 description, lockedPackage.description)) { |
| 191 description = lockedPackage.description; |
| 192 } |
| 193 |
| 194 return new PackageId(dep.name, dep.source, dep.version, description); |
214 } | 195 } |
215 } | 196 } |
216 | 197 |
217 /// The constraint solver works by iteratively processing a queue of work items. | 198 /// 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 | 199 /// Each item is a single atomic change to the dependency graph. Handling them |
219 /// in a queue lets us handle asynchrony (resolving versions requires | 200 /// in a queue lets us handle asynchrony (resolving versions requires |
220 /// information from servers) as well as avoid deeply nested recursion. | 201 /// information from servers) as well as avoid deeply nested recursion. |
221 abstract class WorkItem { | 202 abstract class WorkItem { |
222 /// Processes this work item. Returns a future that completes when the work is | 203 /// 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 | 204 /// done. If `null` is returned, that means the work has completed |
224 /// synchronously and the next item can be started immediately. | 205 /// synchronously and the next item can be started immediately. |
225 Future process(VersionSolver solver); | 206 Future process(GreedyVersionSolver solver); |
226 } | 207 } |
227 | 208 |
228 /// The best selected version for a package has changed to [version]. If the | 209 /// 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 | 210 /// 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. | 211 /// added to the graph. If [version] is `null`, it is being removed. |
231 class ChangeVersion implements WorkItem { | 212 class ChangeVersion implements WorkItem { |
232 /// The name of the package whose version is being changed. | 213 /// The name of the package whose version is being changed. |
233 final String package; | 214 final String package; |
234 | 215 |
235 /// The source of the package whose version is changing. | 216 /// The source of the package whose version is changing. |
236 final Source source; | 217 final Source source; |
237 | 218 |
238 /// The description identifying the package whose version is changing. | 219 /// The description identifying the package whose version is changing. |
239 final description; | 220 final description; |
240 | 221 |
241 /// The new selected version. | 222 /// The new selected version. |
242 final Version version; | 223 final Version version; |
243 | 224 |
244 ChangeVersion(this.package, this.source, this.description, this.version); | 225 ChangeVersion(this.package, this.source, this.description, this.version); |
245 | 226 |
246 Future process(VersionSolver solver) { | 227 Future process(GreedyVersionSolver solver) { |
247 log.fine("Changing $package to version $version."); | 228 log.fine("Changing $package to version $version."); |
248 | 229 |
249 var dependency = solver.getDependency(package); | 230 var dependency = solver.getDependency(package); |
250 var oldVersion = dependency.version; | 231 var oldVersion = dependency.version; |
251 solver.setVersion(package, version); | 232 solver.setVersion(package, version); |
252 | 233 |
253 // The dependencies between the old and new version may be different. Walk | 234 // The dependencies between the old and new version may be different. Walk |
254 // them both and update any constraints that differ between the two. | 235 // them both and update any constraints that differ between the two. |
255 return Future.wait([ | 236 return Future.wait([ |
256 getDependencyRefs(solver, oldVersion), | 237 getDependencyRefs(solver, oldVersion), |
(...skipping 25 matching lines...) Expand all Loading... |
282 /// Get the dependencies at [version] of the package being changed. | 263 /// Get the dependencies at [version] of the package being changed. |
283 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, | 264 Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, |
284 Version version) { | 265 Version version) { |
285 // If there is no version, it means no package, so no dependencies. | 266 // If there is no version, it means no package, so no dependencies. |
286 if (version == null) { | 267 if (version == null) { |
287 return new Future<Map<String, PackageRef>>.immediate( | 268 return new Future<Map<String, PackageRef>>.immediate( |
288 <String, PackageRef>{}); | 269 <String, PackageRef>{}); |
289 } | 270 } |
290 | 271 |
291 var id = new PackageId(package, source, version, description); | 272 var id = new PackageId(package, source, version, description); |
292 return solver._pubspecs.load(id).then((pubspec) { | 273 return solver.cache.getPubspec(id).then((pubspec) { |
293 var dependencies = <String, PackageRef>{}; | 274 var dependencies = <String, PackageRef>{}; |
294 for (var dependency in pubspec.dependencies) { | 275 for (var dependency in pubspec.dependencies) { |
295 dependencies[dependency.name] = dependency; | 276 dependencies[dependency.name] = dependency; |
296 } | 277 } |
297 | 278 |
298 // Include dev dependencies only from the root package. | 279 // Include dev dependencies only from the root package. |
299 if (id.isRoot) { | 280 if (id.isRoot) { |
300 for (var dependency in pubspec.devDependencies) { | 281 for (var dependency in pubspec.devDependencies) { |
301 dependencies[dependency.name] = dependency; | 282 dependencies[dependency.name] = dependency; |
302 } | 283 } |
303 } | 284 } |
304 | 285 |
305 return dependencies; | 286 return dependencies; |
306 }); | 287 }); |
307 } | 288 } |
308 } | 289 } |
309 | 290 |
310 /// A constraint that a depending package places on a dependent package has | 291 /// A constraint that a depending package places on a dependent package has |
311 /// changed. | 292 /// changed. |
312 /// | 293 /// |
313 /// This is an abstract class that contains logic for updating the dependency | 294 /// This is an abstract class that contains logic for updating the dependency |
314 /// graph once a dependency has changed. Changing the dependency is the | 295 /// graph once a dependency has changed. Changing the dependency is the |
315 /// responsibility of subclasses. | 296 /// responsibility of subclasses. |
316 abstract class ChangeConstraint implements WorkItem { | 297 abstract class ChangeConstraint implements WorkItem { |
317 Future process(VersionSolver solver); | 298 Future process(GreedyVersionSolver solver); |
318 | 299 |
319 void undo(VersionSolver solver); | 300 void undo(GreedyVersionSolver solver); |
320 | 301 |
321 Future _processChange(VersionSolver solver, Dependency oldDependency, | 302 Future _processChange(GreedyVersionSolver solver, |
322 Dependency newDependency) { | 303 DependencyNode oldDependency, |
| 304 DependencyNode newDependency) { |
323 var name = newDependency.name; | 305 var name = newDependency.name; |
324 var source = oldDependency.source != null ? | 306 var source = oldDependency.source != null ? |
325 oldDependency.source : newDependency.source; | 307 oldDependency.source : newDependency.source; |
326 var description = oldDependency.description != null ? | 308 var description = oldDependency.description != null ? |
327 oldDependency.description : newDependency.description; | 309 oldDependency.description : newDependency.description; |
328 var oldConstraint = oldDependency.constraint; | 310 var oldConstraint = oldDependency.constraint; |
329 var newConstraint = newDependency.constraint; | 311 var newConstraint = newDependency.constraint; |
330 | 312 |
331 // If the package is over-constrained, i.e. the packages depending have | 313 // 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 | 314 // disjoint constraints, then try unlocking a depender that's locked by the |
333 // lockfile. If there are no remaining locked dependencies, throw an error. | 315 // lockfile. If there are no remaining locked dependencies, throw an error. |
334 if (newConstraint != null && newConstraint.isEmpty) { | 316 if (newConstraint != null && newConstraint.isEmpty) { |
335 if (solver.tryUnlockDepender(newDependency)) { | 317 if (solver.tryUnlockDepender(newDependency)) { |
336 undo(solver); | 318 undo(solver); |
337 return null; | 319 return null; |
338 } | 320 } |
339 | 321 |
340 throw new DisjointConstraintException(name, newDependency._refs); | 322 throw new DisjointConstraintException(name, newDependency.toList()); |
341 } | 323 } |
342 | 324 |
343 // If this constraint change didn't cause the overall constraint on the | 325 // 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. | 326 // package to change, then we don't need to do any further work. |
345 if (oldConstraint == newConstraint) return null; | 327 if (oldConstraint == newConstraint) return null; |
346 | 328 |
347 // If the dependency has been cut free from the graph, just remove it. | 329 // If the dependency has been cut free from the graph, just remove it. |
348 if (!newDependency.isDependedOn) { | 330 if (!newDependency.isDependedOn) { |
349 solver.enqueue(new ChangeVersion(name, source, description, null)); | 331 solver.enqueue(new ChangeVersion(name, source, description, null)); |
350 return null; | 332 return null; |
351 } | 333 } |
352 | 334 |
353 // If the dependency is on the root package, then we don't need to do | 335 // 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. | 336 // anything since it's already at the best version. |
355 if (name == solver._root.name) { | 337 if (name == solver.root.name) { |
356 solver.enqueue(new ChangeVersion( | 338 solver.enqueue(new ChangeVersion( |
357 name, source, description, solver._root.version)); | 339 name, source, description, solver.root.version)); |
358 return null; | 340 return null; |
359 } | 341 } |
360 | 342 |
361 // If the dependency is on a package in the lockfile, use the lockfile's | 343 // 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. | 344 // version for that package if it's valid given the other constraints. |
363 var lockedPackage = solver.lockFile.packages[name]; | 345 var lockedPackage = solver.lockFile.packages[name]; |
364 if (lockedPackage != null && newDependency.source == lockedPackage.source) { | 346 if (lockedPackage != null && newDependency.source == lockedPackage.source) { |
365 var lockedVersion = lockedPackage.version; | 347 var lockedVersion = lockedPackage.version; |
366 if (newConstraint.allows(lockedVersion)) { | 348 if (newConstraint.allows(lockedVersion)) { |
367 solver.enqueue( | 349 solver.enqueue( |
(...skipping 19 matching lines...) Expand all Loading... |
387 /// The package that has the dependency. | 369 /// The package that has the dependency. |
388 final String depender; | 370 final String depender; |
389 | 371 |
390 /// The package being depended on and the constraints being placed on it. The | 372 /// The package being depended on and the constraints being placed on it. The |
391 /// source, version, and description in this ref are all considered | 373 /// source, version, and description in this ref are all considered |
392 /// constraints on the dependent package. | 374 /// constraints on the dependent package. |
393 final PackageRef ref; | 375 final PackageRef ref; |
394 | 376 |
395 AddConstraint(this.depender, this.ref); | 377 AddConstraint(this.depender, this.ref); |
396 | 378 |
397 Future process(VersionSolver solver) { | 379 Future process(GreedyVersionSolver solver) { |
398 log.fine("Adding $depender's constraint $ref."); | 380 log.fine("Adding $depender's constraint $ref."); |
399 | 381 |
400 var dependency = solver.getDependency(ref.name); | 382 var dependency = solver.getDependency(ref.name); |
401 var oldDependency = dependency.clone(); | 383 var oldDependency = dependency.clone(); |
402 dependency.placeConstraint(depender, ref); | 384 dependency.placeConstraint(depender, ref); |
403 return _processChange(solver, oldDependency, dependency); | 385 return _processChange(solver, oldDependency, dependency); |
404 } | 386 } |
405 | 387 |
406 void undo(VersionSolver solver) { | 388 void undo(GreedyVersionSolver solver) { |
407 solver.getDependency(ref.name).removeConstraint(depender); | 389 solver.getDependency(ref.name).removeConstraint(depender); |
408 } | 390 } |
409 } | 391 } |
410 | 392 |
411 /// [depender] is no longer placing a constraint on [dependent]. | 393 /// [depender] is no longer placing a constraint on [dependent]. |
412 class RemoveConstraint extends ChangeConstraint { | 394 class RemoveConstraint extends ChangeConstraint { |
413 /// The package that was placing a constraint on [dependent]. | 395 /// The package that was placing a constraint on [dependent]. |
414 String depender; | 396 String depender; |
415 | 397 |
416 /// The package that was being depended on. | 398 /// The package that was being depended on. |
417 String dependent; | 399 String dependent; |
418 | 400 |
419 /// The constraint that was removed. | 401 /// The constraint that was removed. |
420 PackageRef _removed; | 402 PackageRef _removed; |
421 | 403 |
422 RemoveConstraint(this.depender, this.dependent); | 404 RemoveConstraint(this.depender, this.dependent); |
423 | 405 |
424 Future process(VersionSolver solver) { | 406 Future process(GreedyVersionSolver solver) { |
425 log.fine("Removing $depender's constraint ($_removed) on $dependent."); | 407 log.fine("Removing $depender's constraint ($_removed) on $dependent."); |
426 | 408 |
427 var dependency = solver.getDependency(dependent); | 409 var dependency = solver.getDependency(dependent); |
428 var oldDependency = dependency.clone(); | 410 var oldDependency = dependency.clone(); |
429 _removed = dependency.removeConstraint(depender); | 411 _removed = dependency.removeConstraint(depender); |
430 return _processChange(solver, oldDependency, dependency); | 412 return _processChange(solver, oldDependency, dependency); |
431 } | 413 } |
432 | 414 |
433 void undo(VersionSolver solver) { | 415 void undo(GreedyVersionSolver solver) { |
434 solver.getDependency(dependent).placeConstraint(depender, _removed); | 416 solver.getDependency(dependent).placeConstraint(depender, _removed); |
435 } | 417 } |
436 } | 418 } |
437 | 419 |
438 /// [package]'s version is no longer constrained by the lockfile. | 420 /// [package]'s version is no longer constrained by the lockfile. |
439 class UnlockPackage implements WorkItem { | 421 class UnlockPackage implements WorkItem { |
440 /// The package being unlocked. | 422 /// The package being unlocked. |
441 Dependency package; | 423 DependencyNode package; |
442 | 424 |
443 UnlockPackage(this.package); | 425 UnlockPackage(this.package); |
444 | 426 |
445 Future process(VersionSolver solver) { | 427 Future process(GreedyVersionSolver solver) { |
446 log.fine("Unlocking ${package.name}."); | 428 log.fine("Unlocking ${package.name}."); |
447 | 429 |
448 solver.lockFile.packages.remove(package.name); | 430 solver.lockFile.packages.remove(package.name); |
449 return solver.getBestVersion(package).then((best) { | 431 return solver.getBestVersion(package).then((best) { |
450 if (best == null) return null; | 432 if (best == null) return null; |
451 solver.enqueue(new ChangeVersion( | 433 solver.enqueue(new ChangeVersion( |
452 package.name, package.source, package.description, best)); | 434 package.name, package.source, package.description, best)); |
453 }); | 435 }); |
454 } | 436 } |
455 } | 437 } |
456 | 438 |
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 | 439 /// Describes one [Package] in the [DependencyGraph] and keeps track of which |
490 /// packages depend on it and what constraints they place on it. | 440 /// packages depend on it and what constraints they place on it. |
491 class Dependency { | 441 class DependencyNode { |
492 /// The name of the this dependency's package. | 442 /// The name of the this dependency's package. |
493 final String name; | 443 final String name; |
494 | 444 |
495 /// The [PackageRefs] that represent constraints that depending packages have | 445 /// The [PackageRefs] that represent constraints that depending packages have |
496 /// placed on this one. | 446 /// placed on this one. |
497 final Map<String, PackageRef> _refs; | 447 final Map<String, PackageRef> _refs; |
498 | 448 |
499 /// The currently-selected best version for this dependency. | 449 /// The currently-selected best version for this dependency. |
500 Version version; | 450 Version version; |
501 | 451 |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
539 /// agree upon. | 489 /// agree upon. |
540 PackageRef _canonicalRef() { | 490 PackageRef _canonicalRef() { |
541 if (_refs.isEmpty) return null; | 491 if (_refs.isEmpty) return null; |
542 var refs = _refs.values; | 492 var refs = _refs.values; |
543 for (var ref in refs) { | 493 for (var ref in refs) { |
544 if (ref.isRoot) return ref; | 494 if (ref.isRoot) return ref; |
545 } | 495 } |
546 return refs.first; | 496 return refs.first; |
547 } | 497 } |
548 | 498 |
549 Dependency(this.name) | 499 DependencyNode(this.name) |
550 : _refs = <String, PackageRef>{}; | 500 : _refs = <String, PackageRef>{}; |
551 | 501 |
552 Dependency._clone(Dependency other) | 502 DependencyNode._clone(DependencyNode other) |
553 : name = other.name, | 503 : name = other.name, |
554 version = other.version, | 504 version = other.version, |
555 _refs = new Map<String, PackageRef>.from(other._refs); | 505 _refs = new Map<String, PackageRef>.from(other._refs); |
556 | 506 |
557 /// Creates a copy of this dependency. | 507 /// Creates a copy of this dependency. |
558 Dependency clone() => new Dependency._clone(this); | 508 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 | 509 |
563 /// Places [ref] as a constraint from [package] onto this. | 510 /// Places [ref] as a constraint from [package] onto this. |
564 void placeConstraint(String package, PackageRef ref) { | 511 void placeConstraint(String package, PackageRef ref) { |
565 var requiredDepender = _requiredDepender(); | 512 var requiredDepender = _requiredDepender(); |
566 if (requiredDepender != null) { | 513 if (requiredDepender != null) { |
567 var required = _refs[requiredDepender]; | 514 var required = _refs[requiredDepender]; |
568 if (required.source.name != ref.source.name) { | 515 if (required.source.name != ref.source.name) { |
569 throw new SourceMismatchException(name, | 516 throw new SourceMismatchException(name, [ |
570 requiredDepender, required.source, package, ref.source); | 517 new Dependency(requiredDepender, required), |
571 } else if (!required.source.descriptionsEqual( | 518 new Dependency(package, ref)]); |
572 required.description, ref.description)) { | 519 } else if (!required.descriptionEquals(ref)) { |
573 throw new DescriptionMismatchException(name, | 520 throw new DescriptionMismatchException(name, [ |
574 requiredDepender, required.description, package, ref.description); | 521 new Dependency(requiredDepender, required), |
| 522 new Dependency(package, ref)]); |
575 } | 523 } |
576 } | 524 } |
577 | 525 |
578 _refs[package] = ref; | 526 _refs[package] = ref; |
579 } | 527 } |
580 | 528 |
581 /// Returns the name of a package whose constraint source and description | 529 /// Returns the name of a package whose constraint source and description |
582 /// all other constraints must match. Returns null if there are no | 530 /// all other constraints must match. Returns null if there are no |
583 /// requirements on new constraints. | 531 /// requirements on new constraints. |
584 String _requiredDepender() { | 532 String _requiredDepender() { |
585 if (_refs.isEmpty) return null; | 533 if (_refs.isEmpty) return null; |
586 | 534 |
587 var dependers = _refs.keys.toList(); | 535 var dependers = _refs.keys.toList(); |
588 if (dependers.length == 1) { | 536 if (dependers.length == 1) { |
589 var depender = dependers[0]; | 537 var depender = dependers[0]; |
590 if (_refs[depender].isRoot) return null; | 538 if (_refs[depender].isRoot) return null; |
591 return depender; | 539 return depender; |
592 } | 540 } |
593 | 541 |
594 return dependers[1]; | 542 return dependers[1]; |
595 } | 543 } |
596 | 544 |
597 /// Removes the constraint from [package] onto this. | 545 /// Removes the constraint from [package] onto this. |
598 PackageRef removeConstraint(String package) => _refs.remove(package); | 546 PackageRef removeConstraint(String package) => _refs.remove(package); |
599 } | |
600 | 547 |
601 /// Exception thrown when the [VersionConstraint] used to match a package is | 548 /// 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 | 549 /// expect. |
603 /// that fit that constraint. | 550 List<Dependency> toList() { |
604 class NoVersionException implements Exception { | 551 var result = <Dependency>[]; |
605 final String package; | 552 _refs.forEach((name, ref) { |
606 final VersionConstraint constraint; | 553 result.add(new Dependency(name, ref)); |
607 final Map<String, PackageRef> _dependencies; | 554 }); |
608 | 555 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 } | 556 } |
626 } | 557 } |
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 |