Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(18)

Side by Side Diff: utils/pub/solver/greedy_solver.dart

Issue 13095015: Use backtracking when solving dependency constraints. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Revise, update to latest corelib, and make backtracker default. Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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.value(buildResults()); 76 if (_work.isEmpty) return new Future.value(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 24 matching lines...) Expand all
281 262
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>>.value(<String, PackageRef>{}); 268 return new Future<Map<String, PackageRef>>.value(<String, PackageRef>{});
288 } 269 }
289 270
290 var id = new PackageId(package, source, version, description); 271 var id = new PackageId(package, source, version, description);
291 return solver._pubspecs.load(id).then((pubspec) { 272 return solver.cache.getPubspec(id).then((pubspec) {
292 var dependencies = <String, PackageRef>{}; 273 var dependencies = <String, PackageRef>{};
293 for (var dependency in pubspec.dependencies) { 274 for (var dependency in pubspec.dependencies) {
294 dependencies[dependency.name] = dependency; 275 dependencies[dependency.name] = dependency;
295 } 276 }
296 277
297 // Include dev dependencies only from the root package. 278 // Include dev dependencies only from the root package.
298 if (id.isRoot) { 279 if (id.isRoot) {
299 for (var dependency in pubspec.devDependencies) { 280 for (var dependency in pubspec.devDependencies) {
300 dependencies[dependency.name] = dependency; 281 dependencies[dependency.name] = dependency;
301 } 282 }
302 } 283 }
303 284
304 return dependencies; 285 return dependencies;
305 }); 286 });
306 } 287 }
307 } 288 }
308 289
309 /// A constraint that a depending package places on a dependent package has 290 /// A constraint that a depending package places on a dependent package has
310 /// changed. 291 /// changed.
311 /// 292 ///
312 /// This is an abstract class that contains logic for updating the dependency 293 /// This is an abstract class that contains logic for updating the dependency
313 /// graph once a dependency has changed. Changing the dependency is the 294 /// graph once a dependency has changed. Changing the dependency is the
314 /// responsibility of subclasses. 295 /// responsibility of subclasses.
315 abstract class ChangeConstraint implements WorkItem { 296 abstract class ChangeConstraint implements WorkItem {
316 Future process(VersionSolver solver); 297 Future process(GreedyVersionSolver solver);
317 298
318 void undo(VersionSolver solver); 299 void undo(GreedyVersionSolver solver);
319 300
320 Future _processChange(VersionSolver solver, Dependency oldDependency, 301 Future _processChange(GreedyVersionSolver solver,
321 Dependency newDependency) { 302 DependencyNode oldDependency,
303 DependencyNode newDependency) {
322 var name = newDependency.name; 304 var name = newDependency.name;
323 var source = oldDependency.source != null ? 305 var source = oldDependency.source != null ?
324 oldDependency.source : newDependency.source; 306 oldDependency.source : newDependency.source;
325 var description = oldDependency.description != null ? 307 var description = oldDependency.description != null ?
326 oldDependency.description : newDependency.description; 308 oldDependency.description : newDependency.description;
327 var oldConstraint = oldDependency.constraint; 309 var oldConstraint = oldDependency.constraint;
328 var newConstraint = newDependency.constraint; 310 var newConstraint = newDependency.constraint;
329 311
330 // If the package is over-constrained, i.e. the packages depending have 312 // If the package is over-constrained, i.e. the packages depending have
331 // disjoint constraints, then try unlocking a depender that's locked by the 313 // disjoint constraints, then try unlocking a depender that's locked by the
332 // lockfile. If there are no remaining locked dependencies, throw an error. 314 // lockfile. If there are no remaining locked dependencies, throw an error.
333 if (newConstraint != null && newConstraint.isEmpty) { 315 if (newConstraint != null && newConstraint.isEmpty) {
334 if (solver.tryUnlockDepender(newDependency)) { 316 if (solver.tryUnlockDepender(newDependency)) {
335 undo(solver); 317 undo(solver);
336 return null; 318 return null;
337 } 319 }
338 320
339 throw new DisjointConstraintException(name, newDependency._refs); 321 throw new DisjointConstraintException(name, newDependency.toList());
340 } 322 }
341 323
342 // If this constraint change didn't cause the overall constraint on the 324 // If this constraint change didn't cause the overall constraint on the
343 // package to change, then we don't need to do any further work. 325 // package to change, then we don't need to do any further work.
344 if (oldConstraint == newConstraint) return null; 326 if (oldConstraint == newConstraint) return null;
345 327
346 // If the dependency has been cut free from the graph, just remove it. 328 // If the dependency has been cut free from the graph, just remove it.
347 if (!newDependency.isDependedOn) { 329 if (!newDependency.isDependedOn) {
348 solver.enqueue(new ChangeVersion(name, source, description, null)); 330 solver.enqueue(new ChangeVersion(name, source, description, null));
349 return null; 331 return null;
350 } 332 }
351 333
352 // If the dependency is on the root package, then we don't need to do 334 // If the dependency is on the root package, then we don't need to do
353 // anything since it's already at the best version. 335 // anything since it's already at the best version.
354 if (name == solver._root.name) { 336 if (name == solver.root.name) {
355 solver.enqueue(new ChangeVersion( 337 solver.enqueue(new ChangeVersion(
356 name, source, description, solver._root.version)); 338 name, source, description, solver.root.version));
357 return null; 339 return null;
358 } 340 }
359 341
360 // If the dependency is on a package in the lockfile, use the lockfile's 342 // If the dependency is on a package in the lockfile, use the lockfile's
361 // version for that package if it's valid given the other constraints. 343 // version for that package if it's valid given the other constraints.
362 var lockedPackage = solver.lockFile.packages[name]; 344 var lockedPackage = solver.lockFile.packages[name];
363 if (lockedPackage != null && newDependency.source == lockedPackage.source) { 345 if (lockedPackage != null && newDependency.source == lockedPackage.source) {
364 var lockedVersion = lockedPackage.version; 346 var lockedVersion = lockedPackage.version;
365 if (newConstraint.allows(lockedVersion)) { 347 if (newConstraint.allows(lockedVersion)) {
366 solver.enqueue( 348 solver.enqueue(
(...skipping 19 matching lines...) Expand all
386 /// The package that has the dependency. 368 /// The package that has the dependency.
387 final String depender; 369 final String depender;
388 370
389 /// The package being depended on and the constraints being placed on it. The 371 /// The package being depended on and the constraints being placed on it. The
390 /// source, version, and description in this ref are all considered 372 /// source, version, and description in this ref are all considered
391 /// constraints on the dependent package. 373 /// constraints on the dependent package.
392 final PackageRef ref; 374 final PackageRef ref;
393 375
394 AddConstraint(this.depender, this.ref); 376 AddConstraint(this.depender, this.ref);
395 377
396 Future process(VersionSolver solver) { 378 Future process(GreedyVersionSolver solver) {
397 log.fine("Adding $depender's constraint $ref."); 379 log.fine("Adding $depender's constraint $ref.");
398 380
399 var dependency = solver.getDependency(ref.name); 381 var dependency = solver.getDependency(ref.name);
400 var oldDependency = dependency.clone(); 382 var oldDependency = dependency.clone();
401 dependency.placeConstraint(depender, ref); 383 dependency.placeConstraint(depender, ref);
402 return _processChange(solver, oldDependency, dependency); 384 return _processChange(solver, oldDependency, dependency);
403 } 385 }
404 386
405 void undo(VersionSolver solver) { 387 void undo(GreedyVersionSolver solver) {
406 solver.getDependency(ref.name).removeConstraint(depender); 388 solver.getDependency(ref.name).removeConstraint(depender);
407 } 389 }
408 } 390 }
409 391
410 /// [depender] is no longer placing a constraint on [dependent]. 392 /// [depender] is no longer placing a constraint on [dependent].
411 class RemoveConstraint extends ChangeConstraint { 393 class RemoveConstraint extends ChangeConstraint {
412 /// The package that was placing a constraint on [dependent]. 394 /// The package that was placing a constraint on [dependent].
413 String depender; 395 String depender;
414 396
415 /// The package that was being depended on. 397 /// The package that was being depended on.
416 String dependent; 398 String dependent;
417 399
418 /// The constraint that was removed. 400 /// The constraint that was removed.
419 PackageRef _removed; 401 PackageRef _removed;
420 402
421 RemoveConstraint(this.depender, this.dependent); 403 RemoveConstraint(this.depender, this.dependent);
422 404
423 Future process(VersionSolver solver) { 405 Future process(GreedyVersionSolver solver) {
424 log.fine("Removing $depender's constraint ($_removed) on $dependent."); 406 log.fine("Removing $depender's constraint ($_removed) on $dependent.");
425 407
426 var dependency = solver.getDependency(dependent); 408 var dependency = solver.getDependency(dependent);
427 var oldDependency = dependency.clone(); 409 var oldDependency = dependency.clone();
428 _removed = dependency.removeConstraint(depender); 410 _removed = dependency.removeConstraint(depender);
429 return _processChange(solver, oldDependency, dependency); 411 return _processChange(solver, oldDependency, dependency);
430 } 412 }
431 413
432 void undo(VersionSolver solver) { 414 void undo(GreedyVersionSolver solver) {
433 solver.getDependency(dependent).placeConstraint(depender, _removed); 415 solver.getDependency(dependent).placeConstraint(depender, _removed);
434 } 416 }
435 } 417 }
436 418
437 /// [package]'s version is no longer constrained by the lockfile. 419 /// [package]'s version is no longer constrained by the lockfile.
438 class UnlockPackage implements WorkItem { 420 class UnlockPackage implements WorkItem {
439 /// The package being unlocked. 421 /// The package being unlocked.
440 Dependency package; 422 DependencyNode package;
441 423
442 UnlockPackage(this.package); 424 UnlockPackage(this.package);
443 425
444 Future process(VersionSolver solver) { 426 Future process(GreedyVersionSolver solver) {
445 log.fine("Unlocking ${package.name}."); 427 log.fine("Unlocking ${package.name}.");
446 428
447 solver.lockFile.packages.remove(package.name); 429 solver.lockFile.packages.remove(package.name);
448 return solver.getBestVersion(package).then((best) { 430 return solver.getBestVersion(package).then((best) {
449 if (best == null) return null; 431 if (best == null) return null;
450 solver.enqueue(new ChangeVersion( 432 solver.enqueue(new ChangeVersion(
451 package.name, package.source, package.description, best)); 433 package.name, package.source, package.description, best));
452 }); 434 });
453 } 435 }
454 } 436 }
455 437
456 // TODO(rnystrom): Instead of always pulling from the source (which will mean
457 // hitting a server), we should consider caching pubspecs of uninstalled
458 // packages in the system cache.
459 /// Maintains a cache of previously-loaded pubspecs. Used to avoid requesting
460 /// the same pubspec from the server repeatedly.
461 class PubspecCache {
462 final SourceRegistry _sources;
463 final Map<PackageId, Pubspec> _pubspecs;
464
465 PubspecCache(this._sources)
466 : _pubspecs = new Map<PackageId, Pubspec>();
467
468 /// Caches [pubspec] as the [Pubspec] for the package identified by [id].
469 void cache(PackageId id, Pubspec pubspec) {
470 _pubspecs[id] = pubspec;
471 }
472
473 /// Loads the pubspec for the package identified by [id].
474 Future<Pubspec> load(PackageId id) {
475 // Complete immediately if it's already cached.
476 if (_pubspecs.containsKey(id)) {
477 return new Future<Pubspec>.value(_pubspecs[id]);
478 }
479
480 return id.describe().then((pubspec) {
481 // Cache it.
482 _pubspecs[id] = pubspec;
483 return pubspec;
484 });
485 }
486 }
487
488 /// Describes one [Package] in the [DependencyGraph] and keeps track of which 438 /// Describes one [Package] in the [DependencyGraph] and keeps track of which
489 /// packages depend on it and what constraints they place on it. 439 /// packages depend on it and what constraints they place on it.
490 class Dependency { 440 class DependencyNode {
491 /// The name of the this dependency's package. 441 /// The name of the this dependency's package.
492 final String name; 442 final String name;
493 443
494 /// The [PackageRefs] that represent constraints that depending packages have 444 /// The [PackageRefs] that represent constraints that depending packages have
495 /// placed on this one. 445 /// placed on this one.
496 final Map<String, PackageRef> _refs; 446 final Map<String, PackageRef> _refs;
497 447
498 /// The currently-selected best version for this dependency. 448 /// The currently-selected best version for this dependency.
499 Version version; 449 Version version;
500 450
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
538 /// agree upon. 488 /// agree upon.
539 PackageRef _canonicalRef() { 489 PackageRef _canonicalRef() {
540 if (_refs.isEmpty) return null; 490 if (_refs.isEmpty) return null;
541 var refs = _refs.values; 491 var refs = _refs.values;
542 for (var ref in refs) { 492 for (var ref in refs) {
543 if (ref.isRoot) return ref; 493 if (ref.isRoot) return ref;
544 } 494 }
545 return refs.first; 495 return refs.first;
546 } 496 }
547 497
548 Dependency(this.name) 498 DependencyNode(this.name)
549 : _refs = <String, PackageRef>{}; 499 : _refs = <String, PackageRef>{};
550 500
551 Dependency._clone(Dependency other) 501 DependencyNode._clone(DependencyNode other)
552 : name = other.name, 502 : name = other.name,
553 version = other.version, 503 version = other.version,
554 _refs = new Map<String, PackageRef>.from(other._refs); 504 _refs = new Map<String, PackageRef>.from(other._refs);
555 505
556 /// Creates a copy of this dependency. 506 /// Creates a copy of this dependency.
557 Dependency clone() => new Dependency._clone(this); 507 DependencyNode clone() => new DependencyNode._clone(this);
558
559 /// Return a list of available versions for this dependency.
560 Future<List<Version>> getVersions() => source.getVersions(name, description);
561 508
562 /// Places [ref] as a constraint from [package] onto this. 509 /// Places [ref] as a constraint from [package] onto this.
563 void placeConstraint(String package, PackageRef ref) { 510 void placeConstraint(String package, PackageRef ref) {
564 var requiredDepender = _requiredDepender(); 511 var requiredDepender = _requiredDepender();
565 if (requiredDepender != null) { 512 if (requiredDepender != null) {
566 var required = _refs[requiredDepender]; 513 var required = _refs[requiredDepender];
567 if (required.source.name != ref.source.name) { 514 if (required.source.name != ref.source.name) {
568 throw new SourceMismatchException(name, 515 throw new SourceMismatchException(name, [
569 requiredDepender, required.source, package, ref.source); 516 new Dependency(requiredDepender, required),
570 } else if (!required.source.descriptionsEqual( 517 new Dependency(package, ref)]);
571 required.description, ref.description)) { 518 } else if (!required.descriptionEquals(ref)) {
572 throw new DescriptionMismatchException(name, 519 throw new DescriptionMismatchException(name, [
573 requiredDepender, required.description, package, ref.description); 520 new Dependency(requiredDepender, required),
521 new Dependency(package, ref)]);
574 } 522 }
575 } 523 }
576 524
577 _refs[package] = ref; 525 _refs[package] = ref;
578 } 526 }
579 527
580 /// Returns the name of a package whose constraint source and description 528 /// Returns the name of a package whose constraint source and description
581 /// all other constraints must match. Returns null if there are no 529 /// all other constraints must match. Returns null if there are no
582 /// requirements on new constraints. 530 /// requirements on new constraints.
583 String _requiredDepender() { 531 String _requiredDepender() {
584 if (_refs.isEmpty) return null; 532 if (_refs.isEmpty) return null;
585 533
586 var dependers = _refs.keys.toList(); 534 var dependers = _refs.keys.toList();
587 if (dependers.length == 1) { 535 if (dependers.length == 1) {
588 var depender = dependers[0]; 536 var depender = dependers[0];
589 if (_refs[depender].isRoot) return null; 537 if (_refs[depender].isRoot) return null;
590 return depender; 538 return depender;
591 } 539 }
592 540
593 return dependers[1]; 541 return dependers[1];
594 } 542 }
595 543
596 /// Removes the constraint from [package] onto this. 544 /// Removes the constraint from [package] onto this.
597 PackageRef removeConstraint(String package) => _refs.remove(package); 545 PackageRef removeConstraint(String package) => _refs.remove(package);
598 }
599 546
600 /// Exception thrown when the [VersionConstraint] used to match a package is 547 /// Converts this to a list of [Dependency] objects like the error types
601 /// valid (i.e. non-empty), but there are no released versions of the package 548 /// expect.
602 /// that fit that constraint. 549 List<Dependency> toList() {
603 class NoVersionException implements Exception { 550 var result = <Dependency>[];
604 final String package; 551 _refs.forEach((name, ref) {
605 final VersionConstraint constraint; 552 result.add(new Dependency(name, ref));
606 final Map<String, PackageRef> _dependencies; 553 });
607 554 return result;
608 NoVersionException(this.package, this.constraint, this._dependencies);
609
610 String toString() {
611 var buffer = new StringBuffer();
612 buffer.write("Package '$package' has no versions that match $constraint "
613 "derived from:\n");
614
615 var keys = new List.from(_dependencies.keys);
616 keys.sort();
617
618 for (var key in keys) {
619 buffer.write("- '$key' depends on version "
620 "${_dependencies[key].constraint}\n");
621 }
622
623 return buffer.toString();
624 } 555 }
625 } 556 }
626
627 // TODO(rnystrom): Report the list of depending packages and their constraints.
628 /// Exception thrown when the most recent version of [package] must be selected,
629 /// but doesn't match the [VersionConstraint] imposed on the package.
630 class CouldNotUpdateException implements Exception {
631 final String package;
632 final VersionConstraint constraint;
633 final Version best;
634
635 CouldNotUpdateException(this.package, this.constraint, this.best);
636
637 String toString() =>
638 "The latest version of '$package', $best, does not match $constraint.";
639 }
640
641 /// Exception thrown when the [VersionConstraint] used to match a package is
642 /// the empty set: in other words, multiple packages depend on it and have
643 /// conflicting constraints that have no overlap.
644 class DisjointConstraintException implements Exception {
645 final String package;
646 final Map<String, PackageRef> _dependencies;
647
648 DisjointConstraintException(this.package, this._dependencies);
649
650 String toString() {
651 var buffer = new StringBuffer();
652 buffer.write("Incompatible version constraints on '$package':\n");
653
654 var keys = new List.from(_dependencies.keys);
655 keys.sort();
656
657 for (var key in keys) {
658 buffer.write("- '$key' depends on version "
659 "${_dependencies[key].constraint}\n");
660 }
661
662 return buffer.toString();
663 }
664 }
665
666 /// Exception thrown when the [VersionSolver] fails to find a solution after a
667 /// certain number of iterations.
668 class CouldNotSolveException implements Exception {
669 CouldNotSolveException();
670
671 String toString() =>
672 "Could not find a solution that met all version constraints.";
673 }
674
675 /// Exception thrown when two packages with the same name but different sources
676 /// are depended upon.
677 class SourceMismatchException implements Exception {
678 final String package;
679 final String depender1;
680 final Source source1;
681 final String depender2;
682 final Source source2;
683
684 SourceMismatchException(this.package, this.depender1, this.source1,
685 this.depender2, this.source2);
686
687 String toString() {
688 return "Incompatible dependencies on '$package':\n"
689 "- '$depender1' depends on it from source '$source1'\n"
690 "- '$depender2' depends on it from source '$source2'";
691 }
692 }
693
694 /// Exception thrown when two packages with the same name and source but
695 /// different descriptions are depended upon.
696 class DescriptionMismatchException implements Exception {
697 final String package;
698 final String depender1;
699 final description1;
700 final String depender2;
701 final description2;
702
703 DescriptionMismatchException(this.package, this.depender1, this.description1,
704 this.depender2, this.description2);
705
706 String toString() {
707 // TODO(nweiz): Dump descriptions to YAML when that's supported.
708 return "Incompatible dependencies on '$package':\n"
709 "- '$depender1' depends on it with description "
710 "${json.stringify(description1)}\n"
711 "- '$depender2' depends on it with description "
712 "${json.stringify(description2)}";
713 }
714 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698