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

Side by Side Diff: utils/pub/version_solver.dart

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

Powered by Google App Engine
This is Rietveld 408576698