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

Side by Side Diff: utils/pub/solver/version_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
(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 library version_solver;
6
7 import 'dart:async';
8 import 'dart:json' as json;
9
10 import '../lock_file.dart';
11 import '../log.dart' as log;
12 import '../package.dart';
13 import '../pubspec.dart';
14 import '../source.dart';
15 import '../source_registry.dart';
16 import '../version.dart';
17 import 'backtracking_solver.dart';
18 import 'greedy_solver.dart';
19
20 /// Attempts to select the best concrete versions for all of the transitive
21 /// dependencies of [root] taking into account all of the [VersionConstraint]s
22 /// that those dependencies place on each other and the requirements imposed by
23 /// [lockFile].
24 ///
25 /// If [useLatest] is given, then only the latest versions of the referenced
26 /// packages will be used. This is for forcing an update to one or more
27 /// packages.
28 ///
29 /// If [allowBacktracking] is `false` the non-backtracking version solver will
30 /// be used. Otherwise, the backtracking one will be.
31 Future<SolveResult> resolveVersions(SourceRegistry sources, Package root,
32 {LockFile lockFile, bool allowBacktracking, List<PackageRef> useLatest}) {
33 log.message('Resolving dependencies...');
34
35 if (allowBacktracking == null) allowBacktracking = true;
36 if (lockFile == null) lockFile = new LockFile.empty();
37 if (useLatest == null) useLatest = [];
38
39 var solver;
40 if (allowBacktracking) {
41 solver = new BacktrackingVersionSolver(sources, root, lockFile, useLatest);
42 } else {
43 solver = new GreedyVersionSolver(sources, root, lockFile, useLatest);
44 }
45
46 return solver.solve();
47 }
48
49 /// Base class for an implementation of the version constraint solver.
50 class VersionSolver {
51 final SourceRegistry sources;
52 final Package root;
53 final LockFile lockFile;
54 final PubspecCache cache;
55
56 VersionSolver(SourceRegistry sources, this.root, this.lockFile,
57 List<String> useLatest)
58 : sources = sources,
59 cache = new PubspecCache(sources) {
60 for (var package in useLatest) {
61 forceLatestVersion(package);
62 lockFile.packages.remove(package);
63 }
64 }
65
66 /// The number of solutions the solver has tried so far.
67 int get attemptedSolutions;
68
69 /// Force the solver to upgrade [package] to the latest available version.
70 void forceLatestVersion(String package);
71
72 /// Run the solver. Completes with a list of specific package versions if
73 /// successful or an error if it failed to find a solution.
74 Future<SolveResult> solve() {
75 var stopwatch = new Stopwatch();
76 stopwatch.start();
77
78 // Pre-cache the root package's known pubspec.
79 cache.cache(new PackageId.root(root), root.pubspec);
80
81 return runSolver().then((packages) {
82 return new SolveResult(packages, null, attemptedSolutions);
83 }).catchError((error) {
84 if (error is! SolveFailure) throw error;
85
86 // Wrap a failure in a result so we can attach some other data.
87 return new SolveResult(null, error, attemptedSolutions);
88 }).whenComplete(() {
89 // Gather some solving metrics.
90 var buffer = new StringBuffer();
91 buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.');
92 buffer.writeln(
93 '- Requested ${cache.versionCacheMisses} version lists');
94 buffer.writeln(
95 '- Looked up ${cache.versionCacheHits} cached version lists');
96 buffer.writeln(
97 '- Requested ${cache.pubspecCacheMisses} pubspecs');
98 buffer.writeln(
99 '- Looked up ${cache.pubspecCacheHits} cached pubspecs');
100 log.solver(buffer);
101 });
102 }
103
104 /// Entrypoint for subclasses to actually begin solving. External code should
105 /// call [solve()].
106 Future<List<PackageId>> runSolver();
107 }
108
109 /// The result of a version resolution.
110 class SolveResult {
111 /// Whether the solver found a complete solution or failed.
112 bool get succeeded => error == null;
113
114 /// The list of concrete package versions that were selected for each package
115 /// reachable from the root, or `null` if the solver failed.
116 final List<PackageId> packages;
117
118 /// The error that prevented the solver from finding a solution or `null` if
119 /// it was successful.
120 final SolveFailure error;
121
122 /// The number of solutions that were attempted before either finding a
123 /// successful solution or exhausting all options. In other words, one more
124 /// than the number of times it had to backtrack because it found an invalid
125 /// solution.
126 final int attemptedSolutions;
127
128 SolveResult(this.packages, this.error, this.attemptedSolutions);
129
130 String toString() {
131 if (!succeeded) {
132 return 'Failed to solve after $attemptedSolutions attempts:\n'
133 '$error';
134 }
135
136 return 'Took $attemptedSolutions tries to resolve to\n'
137 '- ${packages.join("\n- ")}';
138 }
139 }
140
141 /// Maintains a cache of previously-requested data: pubspecs and version lists.
142 /// Used to avoid requesting the same pubspec from the server repeatedly.
143 class PubspecCache {
144 final SourceRegistry _sources;
145 final _versions = new Map<PackageId, List<PackageId>>();
146 final _pubspecs = new Map<PackageId, Pubspec>();
147
148 /// The number of times a version list was requested and it wasn't cached and
149 /// had to be requested from the source.
150 int versionCacheMisses = 0;
151
152 /// The number of times a version list was requested and the cached version
153 /// was returned.
154 int versionCacheHits = 0;
155
156 /// The number of times a pubspec was requested and it wasn't cached and had
157 /// to be requested from the source.
158 int pubspecCacheMisses = 0;
159
160 /// The number of times a pubspec was requested and the cached version was
161 /// returned.
162 int pubspecCacheHits = 0;
163
164 PubspecCache(this._sources);
165
166 /// Caches [pubspec] as the [Pubspec] for the package identified by [id].
167 void cache(PackageId id, Pubspec pubspec) {
168 _pubspecs[id] = pubspec;
169 }
170
171 /// Loads the pubspec for the package identified by [id].
172 Future<Pubspec> getPubspec(PackageId id) {
173 // Complete immediately if it's already cached.
174 if (_pubspecs.containsKey(id)) {
175 pubspecCacheHits++;
176 return new Future<Pubspec>.value(_pubspecs[id]);
177 }
178
179 pubspecCacheMisses++;
180 return id.describe().then((pubspec) {
181 log.solver('requested $id pubspec');
182
183 // Cache it.
184 _pubspecs[id] = pubspec;
185 return pubspec;
186 });
187 }
188
189 /// Returns the previously cached pubspec for the package identified by [id]
190 /// or returns `null` if not in the cache.
191 Pubspec getCachedPubspec(PackageId id) => _pubspecs[id];
192
193 /// Gets the list of versions for [package] in descending order.
194 Future<List<PackageId>> getVersions(String package, Source source,
195 description) {
196 // Create a fake ID to use as a key.
197 // TODO(rnystrom): Create a separate type for (name, source, description)
198 // without a version.
199 var id = new PackageId(package, source, Version.none, description);
200
201 // See if we have it cached.
202 var versions = _versions[id];
203 if (versions != null) {
204 versionCacheHits++;
205 return new Future.value(versions);
206 }
207
208 versionCacheMisses++;
209 return source.getVersions(package, description).then((versions) {
210 var ids = versions
211 .map((version) => new PackageId(package, source, version,
212 description))
213 .toList();
214
215 // Sort by descending version so we try newer versions first.
216 ids.sort((a, b) => b.version.compareTo(a.version));
217
218 log.solver('requested $package version list');
219 _versions[id] = ids;
220 return ids;
221 });
222 }
223 }
224
225 /// A reference from a depending package to a package that it depends on.
226 class Dependency {
227 /// The name of the package that has this dependency.
228 final String depender;
229
230 /// The referenced dependent package.
231 final PackageRef ref;
232
233 Dependency(this.depender, this.ref);
234
235 String toString() => '$depender -> $ref';
236 }
237
238 /// Base class for all failures that can occur while trying to resolve versions.
239 class SolveFailure implements Exception {
240 /// The name of the package whose version could not be solved. Will be `null`
241 /// if the failure is not specific to one package.
242 final String package;
243
244 /// The known dependencies on [package] at the time of the failure. Will be
245 /// an empty collection if the failure is not specific to one package.
246 final Iterable<Dependency> dependencies;
247
248 SolveFailure(this.package, Iterable<Dependency> dependencies)
249 : dependencies = dependencies != null ? dependencies : <Dependency>[];
250
251 /// Writes [dependencies] to [buffer] as a bullet list. If [describe] is
252 /// passed, it will be called for each dependency and the result will be
253 /// written next to the dependency.
254 void writeDependencies(StringBuffer buffer,
255 [String describe(PackageRef ref)]) {
256 var map = {};
257 for (var dep in dependencies) {
258 map[dep.depender] = dep.ref;
259 }
260
261 var names = map.keys.toList();
262 names.sort();
263
264 for (var name in names) {
265 buffer.writeln("- '$name' ");
266 if (describe != null) {
267 buffer.writeln(describe(map[name]));
268 } else {
269 buffer.writeln("depends on version ${map[name].constraint}");
270 }
271 }
272 }
273
274 String toString() {
275 if (dependencies.isEmpty) return _message;
276
277 var buffer = new StringBuffer();
278 buffer.writeln("$_message:");
279
280 var map = {};
281 for (var dep in dependencies) {
282 map[dep.depender] = dep.ref;
283 }
284
285 var names = map.keys.toList();
286 names.sort();
287
288 for (var name in names) {
289 buffer.writeln("- '$name' ${_describeDependency(map[name])}");
290 }
291
292 return buffer.toString();
293 }
294
295 /// A message describing the specific kind of solve failure.
296 String get _message;
297
298 /// Describes a dependencie's reference in the output message. Override this
299 /// to highlight which aspect of [ref] led to the failure.
300 String _describeDependency(PackageRef ref) =>
301 "depends on version ${ref.constraint}";
302 }
303
304 /// Exception thrown when the [VersionSolver] fails to find a solution after a
305 /// certain number of iterations.
306 class CouldNotSolveException extends SolveFailure {
307 CouldNotSolveException()
308 : super(null, null);
309
310 /// A message describing the specific kind of solve failure.
311 String get _message =>
312 "Could not find a solution that met all constraints.";
313 }
314
315 /// Exception thrown when the [VersionConstraint] used to match a package is
316 /// valid (i.e. non-empty), but there are no available versions of the package
317 /// that fit that constraint.
318 class NoVersionException extends SolveFailure {
319 final VersionConstraint constraint;
320
321 NoVersionException(String package, this.constraint,
322 Iterable<Dependency> dependencies)
323 : super(package, dependencies);
324
325 String get _message => "Package '$package' has no versions that match "
326 "$constraint derived from";
327 }
328
329 // TODO(rnystrom): Report the list of depending packages and their constraints.
330 /// Exception thrown when the most recent version of [package] must be selected,
331 /// but doesn't match the [VersionConstraint] imposed on the package.
332 class CouldNotUpdateException extends SolveFailure {
333 final VersionConstraint constraint;
334 final Version best;
335
336 CouldNotUpdateException(String package, this.constraint, this.best)
337 : super(package, null);
338
339 String get _message =>
340 "The latest version of '$package', $best, does not match $constraint.";
341 }
342
343 /// Exception thrown when the [VersionConstraint] used to match a package is
344 /// the empty set: in other words, multiple packages depend on it and have
345 /// conflicting constraints that have no overlap.
346 class DisjointConstraintException extends SolveFailure {
347 DisjointConstraintException(String package, Iterable<Dependency> dependencies)
348 : super(package, dependencies);
349
350 String get _message => "Incompatible version constraints on '$package'";
351 }
352
353 /// Exception thrown when two packages with the same name but different sources
354 /// are depended upon.
355 class SourceMismatchException extends SolveFailure {
356
357 SourceMismatchException(String package, Iterable<Dependency> dependencies)
358 : super(package, dependencies);
359
360 String get _message => "Incompatible dependencies on '$package'";
361
362 String _describeDependency(PackageRef ref) =>
363 "depends on it from source ${ref.source}";
364 }
365
366 /// Exception thrown when two packages with the same name and source but
367 /// different descriptions are depended upon.
368 class DescriptionMismatchException extends SolveFailure {
369 DescriptionMismatchException(String package,
370 Iterable<Dependency> dependencies)
371 : super(package, dependencies);
372
373 String get _message => "Incompatible dependencies on '$package'";
374
375 String _describeDependency(PackageRef ref) {
376 // TODO(nweiz): Dump descriptions to YAML when that's supported.
377 return "depends on it with description ${json.stringify(ref.description)}";
378 }
379 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698