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

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

Powered by Google App Engine
This is Rietveld 408576698