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

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: Revised. 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 /// Writes [deps] to [buffer] as a bullet list.
50 void writeDependencies(StringBuffer buffer, List<Dependency> deps) {
nweiz 2013/04/10 22:56:34 Shouldn't this be private?
Bob Nystrom 2013/04/11 00:55:11 Removed in latest patch.
51 var map = {};
52 for (var dep in deps) {
53 map[dep.depender] = dep.ref;
54 }
55
56 var names = map.keys.toList();
57 names.sort();
58
59 for (var name in names) {
60 buffer.writeln("- '$name' depends on version ${map[name].constraint}");
61 }
62 }
63
64 /// Base class for an implementation of the version constraint solver.
65 class VersionSolver {
66 final SourceRegistry sources;
67 final Package root;
68 final LockFile lockFile;
69 final PubspecCache cache;
70
71 VersionSolver(SourceRegistry sources, this.root, this.lockFile,
72 List<String> useLatest)
73 : sources = sources,
74 cache = new PubspecCache(sources) {
75 for (var package in useLatest) {
76 forceLatestVersion(package);
77 lockFile.packages.remove(package);
78 }
79 }
80
81 /// The number of solutions the solver has tried so far.
82 int get attemptedSolutions;
83
84 /// Force the solver to upgrade [package] to the latest available version.
85 void forceLatestVersion(String package);
86
87 /// Run the solver. Completes with a list of specific package versions if
88 /// successful or an error if it failed to find a solution.
89 Future<SolveResult> solve() {
90 var stopwatch = new Stopwatch();
91 stopwatch.start();
92
93 // Pre-cache the root package's known pubspec.
94 cache.cache(new PackageId.root(root), root.pubspec);
95
96 return runSolver().then((packages) {
97 return new SolveResult(packages, null, attemptedSolutions);
98 }).catchError((error) {
99 if (error.error is! SolveFailure) throw error;
100
101 // Wrap a failure in a result so we can attach some other data.
102 return new SolveResult(null, error.error, attemptedSolutions);
103 }).whenComplete(() {
104 // Gather some solving metrics.
105 var buffer = new StringBuffer();
106 buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.');
107 buffer.writeln(
108 '- Requested ${cache.versionCacheMisses} version lists');
109 buffer.writeln(
110 '- Looked up ${cache.versionCacheHits} cached version lists');
111 buffer.writeln(
112 '- Requested ${cache.pubspecCacheMisses} pubspecs');
113 buffer.writeln(
114 '- Looked up ${cache.pubspecCacheHits} cached pubspecs');
115 log.solver(buffer);
116 });
117 }
118
119 /// Entrypoint for subclasses to actually begin solving. External code should
120 /// call [solve()].
121 Future<List<PackageId>> runSolver();
122 }
123
124 /// The result of a successful version resolution. (If resolution fails, a
125 /// [SolveFailure] will be thrown.
nweiz 2013/04/10 22:56:34 This comment is out-of-date; a failing resolution
Bob Nystrom 2013/04/11 00:55:11 Done.
126 class SolveResult {
127 /// Whether the solver found a complete solution or failed.
128 bool get succeeded => error == null;
129
130 /// The list of concrete package versions that were selected for each package
131 /// reachable from the root, or `null` if the solver failed.
132 final List<PackageId> packages;
133
134 /// The error that prevented the solver from finding a solution or `null` if
135 /// it was successful.
136 final SolveFailure error;
137
138 /// The number of solutions that were attempted before a successful one was
139 /// found. In other words, one more than the number of times it had to
140 /// backtrack because it found an invalid solution.
nweiz 2013/04/10 22:56:34 Document what this means if no solution was found.
Bob Nystrom 2013/04/11 00:55:11 Done.
141 final int attemptedSolutions;
142
143 SolveResult(this.packages, this.error, this.attemptedSolutions);
144
145 String toString() {
146 if (!succeeded) {
147 return 'Failed to solve after $attemptedSolutions attempts:\n'
148 '$error';
149 }
150
151 return 'Took $attemptedSolutions tries to resolve to\n'
152 '- ${packages.join("\n- ")}';
153 }
154 }
155
156 /// Maintains a cache of previously-requested data: pubspecs and version lists.
157 /// Used to avoid requesting the same pubspec from the server repeatedly.
158 class PubspecCache {
159 final SourceRegistry _sources;
160 final _versions = new Map<PackageId, List<PackageId>>();
161 final _pubspecs = new Map<PackageId, Pubspec>();
162
163 /// The number of times a version list was requested and it wasn't cached and
164 /// had to be requested from the source.
165 int versionCacheMisses = 0;
166
167 /// The number of times a version list was requested and the cached version
168 /// was returned.
169 int versionCacheHits = 0;
170
171 /// The number of times a pubspec was requested and it wasn't cached and had
172 /// to be requested from the source.
173 int pubspecCacheMisses = 0;
174
175 /// The number of times a pubspec was requested and the cached version was
176 /// returned.
177 int pubspecCacheHits = 0;
178
179 PubspecCache(this._sources);
180
181 /// Caches [pubspec] as the [Pubspec] for the package identified by [id].
182 void cache(PackageId id, Pubspec pubspec) {
183 _pubspecs[id] = pubspec;
184 }
185
186 /// Loads the pubspec for the package identified by [id].
187 Future<Pubspec> getPubspec(PackageId id) {
188 // Complete immediately if it's already cached.
189 if (_pubspecs.containsKey(id)) {
190 pubspecCacheHits++;
191 return new Future<Pubspec>.immediate(_pubspecs[id]);
192 }
193
194 pubspecCacheMisses++;
195 return id.describe().then((pubspec) {
196 log.solver('requested $id pubspec');
197
198 // Cache it.
199 _pubspecs[id] = pubspec;
200 return pubspec;
201 });
202 }
203
204 /// Gets the list of versions for [package] in descending order.
205 Future<List<PackageId>> getVersions(String package, Source source,
206 description) {
207 // Create a fake ID to use as a key.
208 // TODO(rnystrom): Create a separate type for (name, source, description)
209 // without a version.
210 var id = new PackageId(package, source, Version.none, description);
211
212 // See if we have it cached.
213 var versions = _versions[id];
214 if (versions != null) {
215 versionCacheHits++;
216 return new Future.immediate(versions);
217 }
218
219 versionCacheMisses++;
220 return source.getVersions(package, description).then((versions) {
221 var ids = versions
222 .map((version) => new PackageId(package, source, version,
223 description))
224 .toList();
225
226 // Sort by descending version so we try newer versions first.
227 ids.sort((a, b) => b.version.compareTo(a.version));
228
229 log.solver('requested $package version list');
230 _versions[id] = ids;
231 return ids;
232 });
233 }
234 }
235
236 /// A reference from a depending package to a package that it depends on.
237 class Dependency {
238 /// The name of the package that has this dependency.
239 final String depender;
240
241 /// The referenced dependent package.
242 final PackageRef ref;
243
244 Dependency(this.depender, this.ref);
245 }
246
247 /// Base class for all failures that can occur while trying to resolve versions.
248 class SolveFailure implements Exception {}
249
250 /// Exception thrown when the [VersionSolver] fails to find a solution after a
251 /// certain number of iterations.
252 class CouldNotSolveException extends SolveFailure {
253 CouldNotSolveException();
254
255 String toString() =>
256 "Could not find a solution that met all version constraints.";
257 }
258
259 /// Exception thrown when the [VersionConstraint] used to match a package is
260 /// valid (i.e. non-empty), but there are no available versions of the package
261 /// that fit that constraint.
262 class NoVersionException extends SolveFailure {
263 final String package;
264 final VersionConstraint constraint;
265 final List<Dependency> _dependencies;
266
267 NoVersionException(this.package, this.constraint, this._dependencies);
268
269 String toString() {
270 var buffer = new StringBuffer();
271 buffer.write("Package '$package' has no versions that match $constraint "
272 "derived from:\n");
273 writeDependencies(buffer, _dependencies);
274 return buffer.toString();
275 }
276 }
277
278 // TODO(rnystrom): Report the list of depending packages and their constraints.
279 /// Exception thrown when the most recent version of [package] must be selected,
280 /// but doesn't match the [VersionConstraint] imposed on the package.
281 class CouldNotUpdateException extends SolveFailure {
282 final String package;
283 final VersionConstraint constraint;
284 final Version best;
285
286 CouldNotUpdateException(this.package, this.constraint, this.best);
287
288 String toString() =>
289 "The latest version of '$package', $best, does not match $constraint.";
290 }
291
292 /// Exception thrown when the [VersionConstraint] used to match a package is
293 /// the empty set: in other words, multiple packages depend on it and have
294 /// conflicting constraints that have no overlap.
295 class DisjointConstraintException extends SolveFailure {
296 final String package;
297 final List<Dependency> _dependencies;
298
299 DisjointConstraintException(this.package, this._dependencies);
300
301 String toString() {
302 var buffer = new StringBuffer();
303 buffer.write("Incompatible version constraints on '$package':\n");
304 writeDependencies(buffer, _dependencies);
305 return buffer.toString();
306 }
307 }
308
309 /// Exception thrown when two packages with the same name but different sources
310 /// are depended upon.
311 class SourceMismatchException extends SolveFailure {
312 final String package;
313 final String depender1;
314 final Source source1;
315 final String depender2;
316 final Source source2;
317
318 SourceMismatchException(this.package, this.depender1, this.source1,
319 this.depender2, this.source2);
320
321 String toString() {
322 return "Incompatible dependencies on '$package':\n"
323 "- '$depender1' depends on it from source '$source1'\n"
324 "- '$depender2' depends on it from source '$source2'";
325 }
326 }
327
328 /// Exception thrown when two packages with the same name and source but
329 /// different descriptions are depended upon.
330 class DescriptionMismatchException extends SolveFailure {
331 final String package;
332 final String depender1;
333 final description1;
334 final String depender2;
335 final description2;
336
337 DescriptionMismatchException(this.package, this.depender1, this.description1,
338 this.depender2, this.description2);
339
340 String toString() {
341 // TODO(nweiz): Dump descriptions to YAML when that's supported.
342 return "Incompatible dependencies on '$package':\n"
343 "- '$depender1' depends on it with description "
344 "${json.stringify(description1)}\n"
345 "- '$depender2' depends on it with description "
346 "${json.stringify(description2)}";
347 }
348 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698