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

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: Backjumping. 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 `true` the backtracking version solver will be
30 /// used. Otherwise, the non-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 = false;
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.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.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 successful version resolution. (If resolution fails, a
110 /// [SolveFailure] will be thrown.
111 class SolveResult {
112 /// Whether the solver found a complete solution or failed.
113 bool get succeeded => error == null;
114
115 /// The list of concrete package versions that were selected for each package
116 /// reachable from the root, or `null` if the solver failed.
117 final List<PackageId> packages;
118
119 /// The error that prevented the solver from finding a solution or `null` if
120 /// it was successful.
121 final SolveFailure error;
122
123 /// The number of solutions that were attempted before a successful one was
124 /// found. In other words, one more than the number of times it had to
125 /// backtrack because it found an invalid 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>.immediate(_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 /// Gets the list of versions for [package] in descending order.
190 Future<List<PackageId>> getVersions(String package, Source source,
191 description) {
192 // Create a fake ID to use as a key.
193 // TODO(rnystrom): Create a separate type for (name, source, description)
194 // without a version.
195 var id = new PackageId(package, source, Version.none, description);
196
197 // See if we have it cached.
198 var versions = _versions[id];
199 if (versions != null) {
200 versionCacheHits++;
201 return new Future.immediate(versions);
202 }
203
204 versionCacheMisses++;
205 return source.getVersions(package, description).then((versions) {
206 var ids = versions
207 .map((version) => new PackageId(package, source, version,
208 description))
209 .toList();
210
211 // Sort by descending version so we try newer versions first.
212 ids.sort((a, b) => b.version.compareTo(a.version));
213
214 log.solver('requested $package version list');
215 _versions[id] = ids;
216 return ids;
217 });
218 }
219 }
220
221 /// A reference from a depending package to a package that it depends on.
222 class Dependency {
223 /// The name of the package that has this dependency.
224 final String depender;
225
226 /// The referenced dependent package.
227 final PackageRef ref;
228
229 Dependency(this.depender, this.ref);
230
231 String toString() => '$depender -> $ref';
232 }
233
234 /// Base class for all failures that can occur while trying to resolve versions.
235 class SolveFailure implements Exception {
236 /// The name of the package whose version could not be solved. Will be `null`
237 /// if the failure is not specific to one package.
238 final String package;
239
240 /// The known dependencies on [package] at the time of the failure. Will be
241 /// an empty collection if the failure is not specific to one package.
242 final Iterable<Dependency> dependencies;
243
244 SolveFailure(this.package, Iterable<Dependency> dependencies)
245 : dependencies = dependencies != null ? dependencies : <Dependency>[];
246
247 /// Writes [dependencies] to [buffer] as a bullet list. If [describe] is
248 /// passed, it will be called for each dependency and the result will be
249 /// written next to the dependency.
250 void writeDependencies(StringBuffer buffer,
251 [String describe(PackageRef ref)]) {
252 var map = {};
253 for (var dep in dependencies) {
254 map[dep.depender] = dep.ref;
255 }
256
257 var names = map.keys.toList();
258 names.sort();
259
260 for (var name in names) {
261 buffer.writeln("- '$name' ");
262 if (describe != null) {
263 buffer.writeln(describe(map[name]));
264 } else {
265 buffer.writeln("depends on version ${map[name].constraint}");
266 }
267 }
268 }
269
270 String toString() {
271 if (dependencies.isEmpty) return _message;
272
273 var buffer = new StringBuffer();
274 buffer.writeln("$_message:");
275
276 var map = {};
277 for (var dep in dependencies) {
278 map[dep.depender] = dep.ref;
279 }
280
281 var names = map.keys.toList();
282 names.sort();
283
284 for (var name in names) {
285 buffer.writeln("- '$name' ${_describeDependency(map[name])}");
286 }
287
288 return buffer.toString();
289 }
290
291 /// A message describing the specific kind of solve failure.
292 String get _message =>
293 "Could not find a solution that met all constraints.";
nweiz 2013/04/10 22:56:35 Why is the default message the message for CouldNo
Bob Nystrom 2013/04/11 00:55:11 Done.
294
295 /// Describes a dependencie's reference in the output message. Override this
296 /// to highlight which aspect of [ref] led to the failure.
297 String _describeDependency(PackageRef ref) =>
298 "depends on version ${ref.constraint}";
299 }
300
301 /// Exception thrown when the [VersionSolver] fails to find a solution after a
302 /// certain number of iterations.
303 class CouldNotSolveException extends SolveFailure {
304 CouldNotSolveException()
305 : super(null, null);
306 }
307
308 /// Exception thrown when the [VersionConstraint] used to match a package is
309 /// valid (i.e. non-empty), but there are no available versions of the package
310 /// that fit that constraint.
311 class NoVersionException extends SolveFailure {
312 final VersionConstraint constraint;
313
314 NoVersionException(String package, this.constraint,
315 Iterable<Dependency> dependencies)
316 : super(package, dependencies);
317
318 String get _message => "Package '$package' has no versions that match "
319 "$constraint derived from";
320 }
321
322 // TODO(rnystrom): Report the list of depending packages and their constraints.
323 /// Exception thrown when the most recent version of [package] must be selected,
324 /// but doesn't match the [VersionConstraint] imposed on the package.
325 class CouldNotUpdateException extends SolveFailure {
326 final VersionConstraint constraint;
327 final Version best;
328
329 CouldNotUpdateException(String package, this.constraint, this.best)
330 : super(package, null);
331
332 String get _message =>
333 "The latest version of '$package', $best, does not match $constraint.";
334 }
335
336 /// Exception thrown when the [VersionConstraint] used to match a package is
337 /// the empty set: in other words, multiple packages depend on it and have
338 /// conflicting constraints that have no overlap.
339 class DisjointConstraintException extends SolveFailure {
340 DisjointConstraintException(String package, Iterable<Dependency> dependencies)
341 : super(package, dependencies);
342
343 String get _message => "Incompatible version constraints on '$package'";
344 }
345
346 /// Exception thrown when two packages with the same name but different sources
347 /// are depended upon.
348 class SourceMismatchException extends SolveFailure {
349
350 SourceMismatchException(String package, Iterable<Dependency> dependencies)
351 : super(package, dependencies);
352
353 String get _message => "Incompatible dependency sources on '$package'";
354
355 String _describeDependency(PackageRef ref) =>
356 "depends on it from source ${ref.source}";
357 }
358
359 /// Exception thrown when two packages with the same name and source but
360 /// different descriptions are depended upon.
361 class DescriptionMismatchException extends SolveFailure {
362 DescriptionMismatchException(String package,
363 Iterable<Dependency> dependencies)
364 : super(package, dependencies);
365
366 String get _message => "Incompatible dependency descriptions on '$package'";
nweiz 2013/04/10 22:56:35 I'm skeptical that users will understand what "des
Bob Nystrom 2013/04/11 00:55:11 Changed back to old message.
367
368 String _describeDependency(PackageRef ref) {
369 // TODO(nweiz): Dump descriptions to YAML when that's supported.
370 return "depends on it with description ${json.stringify(ref.description)}";
371 }
372 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698