| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library dart_style.benchmark.benchmark; | 5 library dart_style.benchmark.benchmark; |
| 6 | 6 |
| 7 import 'dart:io'; | 7 import 'dart:io'; |
| 8 | 8 |
| 9 import 'package:path/path.dart' as p; |
| 10 |
| 9 import 'package:dart_style/dart_style.dart'; | 11 import 'package:dart_style/dart_style.dart'; |
| 10 | 12 |
| 11 const NUM_TRIALS = 100; | 13 const NUM_TRIALS = 100; |
| 12 const FORMATS_PER_TRIAL = 30; | 14 const FORMATS_PER_TRIAL = 30; |
| 13 | 15 |
| 16 /// Note, these files use ".txt" because while they can be *parsed* correctly, |
| 17 /// they don't resolve without error. That's OK because the formatter doesn't |
| 18 /// care about that. |
| 19 final source = loadFile("before.dart.txt"); |
| 20 final expected = loadFile("after.dart.txt"); |
| 21 |
| 14 void main(List<String> args) { | 22 void main(List<String> args) { |
| 15 var best = 99999999.0; | 23 var best = 99999999.0; |
| 16 | 24 |
| 17 // Run the benchmark several times. This ensures the VM is warmed up and lets | 25 // Run the benchmark several times. This ensures the VM is warmed up and lets |
| 18 // us see how much variance there is. | 26 // us see how much variance there is. |
| 19 for (var i = 0; i <= NUM_TRIALS; i++) { | 27 for (var i = 0; i <= NUM_TRIALS; i++) { |
| 20 var start = new DateTime.now(); | 28 var start = new DateTime.now(); |
| 21 | 29 |
| 22 // For a single benchmark, format the source multiple times. | 30 // For a single benchmark, format the source multiple times. |
| 31 var result; |
| 23 for (var j = 0; j < FORMATS_PER_TRIAL; j++) { | 32 for (var j = 0; j < FORMATS_PER_TRIAL; j++) { |
| 24 formatSource(); | 33 result = formatSource(); |
| 25 } | 34 } |
| 26 | 35 |
| 27 var elapsed = new DateTime.now() | 36 var elapsed = new DateTime.now() |
| 28 .difference(start).inMilliseconds / FORMATS_PER_TRIAL; | 37 .difference(start).inMilliseconds / FORMATS_PER_TRIAL; |
| 29 | 38 |
| 30 // Keep track of the best run so far. | 39 // Keep track of the best run so far. |
| 31 if (elapsed >= best) continue; | 40 if (elapsed >= best) continue; |
| 32 best = elapsed; | 41 best = elapsed; |
| 33 | 42 |
| 43 // Sanity check to make sure the output is what we expect and to make sure |
| 44 // the VM doesn't optimize "dead" code away. |
| 45 if (result != expected) { |
| 46 print("Incorrect output:\n$result"); |
| 47 exit(1); |
| 48 } |
| 49 |
| 34 // Don't print the first run. It's always terrible since the VM hasn't | 50 // Don't print the first run. It's always terrible since the VM hasn't |
| 35 // warmed up yet. | 51 // warmed up yet. |
| 36 if (i == 0) continue; | 52 if (i == 0) continue; |
| 37 printResult("Run ${padLeft('#$i', 3)}", elapsed); | 53 printResult("Run ${padLeft('#$i', 3)}", elapsed); |
| 38 } | 54 } |
| 39 | 55 |
| 40 printResult("Best ", best); | 56 printResult("Best ", best); |
| 41 } | 57 } |
| 42 | 58 |
| 59 String loadFile(String name) { |
| 60 var path = p.join(p.dirname(p.fromUri(Platform.script)), name); |
| 61 return new File(path).readAsStringSync(); |
| 62 } |
| 63 |
| 43 void printResult(String label, double time) { | 64 void printResult(String label, double time) { |
| 44 print("$label: ${padLeft(time.toStringAsFixed(2), 4)}ms " | 65 print("$label: ${padLeft(time.toStringAsFixed(2), 4)}ms " |
| 45 "${'=' * ((time * 5).toInt())}"); | 66 "${'=' * ((time * 5).toInt())}"); |
| 46 } | 67 } |
| 47 | 68 |
| 48 String padLeft(input, int length) { | 69 String padLeft(input, int length) { |
| 49 var result = input.toString(); | 70 var result = input.toString(); |
| 50 if (result.length < length) { | 71 if (result.length < length) { |
| 51 result = " " * (length - result.length) + result; | 72 result = " " * (length - result.length) + result; |
| 52 } | 73 } |
| 53 | 74 |
| 54 return result; | 75 return result; |
| 55 } | 76 } |
| 56 | 77 |
| 57 void formatSource() { | 78 String formatSource() { |
| 58 var formatter = new DartFormatter(); | 79 var formatter = new DartFormatter(); |
| 59 var result = formatter.format(source); | 80 return formatter.format(source); |
| 60 | |
| 61 // Sanity check to make sure the output is what we expect and to make sure | |
| 62 // the VM doesn't optimize "dead" code away. | |
| 63 if (result.length != 29791) { | |
| 64 print("Incorrect output (length ${result.length}):\n$result"); | |
| 65 exit(1); | |
| 66 } | |
| 67 } | 81 } |
| 68 | |
| 69 const source = r""" | |
| 70 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 71 // for details. All rights reserved. Use of this source code is governed by a | |
| 72 // BSD-style license that can be found in the LICENSE file. | |
| 73 | |
| 74 library pub.solver.backtracking_solver; | |
| 75 | |
| 76 import 'dart:async'; | |
| 77 import 'dart:collection' show Queue; | |
| 78 | |
| 79 import '../barback.dart' as barback; | |
| 80 import '../exceptions.dart'; | |
| 81 import '../lock_file.dart'; | |
| 82 import '../log.dart' as log; | |
| 83 import '../package.dart'; | |
| 84 import '../pubspec.dart'; | |
| 85 import '../sdk.dart' as sdk; | |
| 86 import '../source_registry.dart'; | |
| 87 import '../source/unknown.dart'; | |
| 88 import '../utils.dart'; | |
| 89 import '../version.dart'; | |
| 90 import 'dependency_queue.dart'; | |
| 91 import 'version_queue.dart'; | |
| 92 import 'version_solver.dart'; | |
| 93 | |
| 94 /// The top-level solver. | |
| 95 /// | |
| 96 /// Keeps track of the current potential solution, and the other possible | |
| 97 /// versions for speculative package selections. Backtracks and advances to the | |
| 98 /// next potential solution in the case of a failure. | |
| 99 class BacktrackingSolver { | |
| 100 final SolveType type; | |
| 101 final SourceRegistry sources; | |
| 102 final Package root; | |
| 103 | |
| 104 /// The lockfile that was present before solving. | |
| 105 final LockFile lockFile; | |
| 106 | |
| 107 final PubspecCache cache; | |
| 108 | |
| 109 /// The set of packages that are being explicitly upgraded. | |
| 110 /// | |
| 111 /// The solver will only allow the very latest version for each of these | |
| 112 /// packages. | |
| 113 final _forceLatest = new Set<String>(); | |
| 114 | |
| 115 /// The set of packages whose dependecy is being overridden by the root | |
| 116 /// package, keyed by the name of the package. | |
| 117 /// | |
| 118 /// Any dependency on a package that appears in this map will be overriden | |
| 119 /// to use the one here. | |
| 120 final _overrides = new Map<String, PackageDep>(); | |
| 121 | |
| 122 /// The package versions currently selected by the solver, along with the | |
| 123 /// versions which are remaining to be tried. | |
| 124 /// | |
| 125 /// Every time a package is encountered when traversing the dependency graph, | |
| 126 /// the solver must select a version for it, sometimes when multiple versions | |
| 127 /// are valid. This keeps track of which versions have been selected so far | |
| 128 /// and which remain to be tried. | |
| 129 /// | |
| 130 /// Each entry in the list is a [VersionQueue], which is an ordered queue of | |
| 131 /// versions to try for a single package. It maintains the currently selected | |
| 132 /// version for that package. When a new dependency is encountered, a queue | |
| 133 /// of versions of that dependency is pushed onto the end of the list. A | |
| 134 /// queue is removed from the list once it's empty, indicating that none of | |
| 135 /// the versions provided a solution. | |
| 136 /// | |
| 137 /// The solver tries versions in depth-first order, so only the last queue in | |
| 138 /// the list will have items removed from it. When a new constraint is placed | |
| 139 /// on an already-selected package, and that constraint doesn't match the | |
| 140 /// selected version, that will cause the current solution to fail and | |
| 141 /// trigger backtracking. | |
| 142 final _selected = <VersionQueue>[]; | |
| 143 | |
| 144 /// The number of solutions the solver has tried so far. | |
| 145 int get attemptedSolutions => _attemptedSolutions; | |
| 146 var _attemptedSolutions = 1; | |
| 147 | |
| 148 BacktrackingSolver(SolveType type, SourceRegistry sources, this.root, | |
| 149 this.lockFile, List<String> useLatest) | |
| 150 : type = type, | |
| 151 sources = sources, | |
| 152 cache = new PubspecCache(type, sources) { | |
| 153 for (var package in useLatest) { | |
| 154 _forceLatest.add(package); | |
| 155 } | |
| 156 | |
| 157 for (var override in root.dependencyOverrides) { | |
| 158 _overrides[override.name] = override; | |
| 159 } | |
| 160 | |
| 161 // A deeply nested statement that's hard on the formatter. | |
| 162 isTwoWay = !isEvent && bindings.isWhole && (isCustomTag || | |
| 163 tag == 'input' && (name == 'value' || name =='checked') || | |
| 164 tag == 'select' && (name == 'selectedindex' || name == 'value') || | |
| 165 tag == 'textarea' && name == 'value'); | |
| 166 } | |
| 167 | |
| 168 /// Run the solver. | |
| 169 /// | |
| 170 /// Completes with a list of specific package versions if successful or an | |
| 171 /// error if it failed to find a solution. | |
| 172 Future<SolveResult> solve() { | |
| 173 var stopwatch = new Stopwatch(); | |
| 174 | |
| 175 _logParameters(); | |
| 176 | |
| 177 // Sort the overrides by package name to make sure they're deterministic. | |
| 178 var overrides = _overrides.values.toList(); | |
| 179 overrides.sort((a, b) => a.name.compareTo(b.name)); | |
| 180 | |
| 181 return newFuture(() { | |
| 182 stopwatch.start(); | |
| 183 | |
| 184 // Pre-cache the root package's known pubspec. | |
| 185 cache.cache(new PackageId.root(root), root.pubspec); | |
| 186 | |
| 187 _validateSdkConstraint(root.pubspec); | |
| 188 return _traverseSolution(); | |
| 189 }).then((packages) { | |
| 190 var pubspecs = new Map.fromIterable( | |
| 191 packages, | |
| 192 key: (id) => id.name, | |
| 193 value: (id) => cache.getCachedPubspec(id)); | |
| 194 | |
| 195 return new SolveResult.success( | |
| 196 sources, | |
| 197 root, | |
| 198 lockFile, | |
| 199 packages, | |
| 200 overrides, | |
| 201 pubspecs, | |
| 202 _getAvailableVersions(packages), | |
| 203 attemptedSolutions); | |
| 204 }).catchError((error) { | |
| 205 if (error is! SolveFailure) throw error; | |
| 206 | |
| 207 // Wrap a failure in a result so we can attach some other data. | |
| 208 return new SolveResult.failure( | |
| 209 sources, | |
| 210 root, | |
| 211 lockFile, | |
| 212 overrides, | |
| 213 error, | |
| 214 attemptedSolutions); | |
| 215 }).whenComplete(() { | |
| 216 // Gather some solving metrics. | |
| 217 var buffer = new StringBuffer(); | |
| 218 buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.'); | |
| 219 buffer.writeln(cache.describeResults()); | |
| 220 log.solver(buffer); | |
| 221 }); | |
| 222 } | |
| 223 | |
| 224 /// Generates a map containing all of the known available versions for each | |
| 225 /// package in [packages]. | |
| 226 /// | |
| 227 /// The version list may not always be complete. The the package is the root | |
| 228 /// root package, or its a package that we didn't unlock while solving | |
| 229 /// because we weren't trying to upgrade it, we will just know the current | |
| 230 /// version. | |
| 231 Map<String, List<Version>> _getAvailableVersions(List<PackageId> packages) { | |
| 232 var availableVersions = new Map<String, List<Version>>(); | |
| 233 for (var package in packages) { | |
| 234 var cached = cache.getCachedVersions(package.toRef()); | |
| 235 var versions; | |
| 236 if (cached != null) { | |
| 237 versions = cached.map((id) => id.version).toList(); | |
| 238 } else { | |
| 239 // If the version list was never requested, just use the one known | |
| 240 // version. | |
| 241 versions = [package.version]; | |
| 242 } | |
| 243 | |
| 244 availableVersions[package.name] = versions; | |
| 245 } | |
| 246 | |
| 247 return availableVersions; | |
| 248 } | |
| 249 | |
| 250 /// Adds [versions], which is the list of all allowed versions of a given | |
| 251 /// package, to the set of versions to consider for solutions. | |
| 252 /// | |
| 253 /// The first item in the list will be the currently selected version of that | |
| 254 /// package. Subsequent items will be tried if it the current selection fails. | |
| 255 /// Returns the first selected version. | |
| 256 PackageId select(VersionQueue versions) { | |
| 257 _selected.add(versions); | |
| 258 logSolve(); | |
| 259 return versions.current; | |
| 260 } | |
| 261 | |
| 262 /// Returns the the currently selected id for the package [name] or `null` if | |
| 263 /// no concrete version has been selected for that package yet. | |
| 264 PackageId getSelected(String name) { | |
| 265 // Always prefer the root package. | |
| 266 if (root.name == name) return new PackageId.root(root); | |
| 267 | |
| 268 // Look through the current selections. | |
| 269 for (var i = _selected.length - 1; i >= 0; i--) { | |
| 270 if (_selected[i].current.name == name) return _selected[i].current; | |
| 271 } | |
| 272 | |
| 273 return null; | |
| 274 } | |
| 275 | |
| 276 /// Gets the version of [package] currently locked in the lock file. | |
| 277 /// | |
| 278 /// Returns `null` if it isn't in the lockfile (or has been unlocked). | |
| 279 PackageId getLocked(String package) { | |
| 280 if (type == SolveType.GET) return lockFile.packages[package]; | |
| 281 | |
| 282 // When downgrading, we don't want to force the latest versions of | |
| 283 // non-hosted packages, since they don't support multiple versions and thus | |
| 284 // can't be downgraded. | |
| 285 if (type == SolveType.DOWNGRADE) { | |
| 286 var locked = lockFile.packages[package]; | |
| 287 if (locked != null && !sources[locked.source].hasMultipleVersions) { | |
| 288 return locked; | |
| 289 } | |
| 290 } | |
| 291 | |
| 292 if (_forceLatest.isEmpty || _forceLatest.contains(package)) return null; | |
| 293 return lockFile.packages[package]; | |
| 294 } | |
| 295 | |
| 296 /// Traverses the root package's dependency graph using the current potential | |
| 297 /// solution. | |
| 298 /// | |
| 299 /// If successful, completes to the solution. If not, backtracks to the most | |
| 300 /// recently selected version of a package and tries the next version of it. | |
| 301 /// If there are no more versions, continues to backtrack to previous | |
| 302 /// selections, and so on. If there is nothing left to backtrack to, | |
| 303 /// completes to the last failure that occurred. | |
| 304 Future<List<PackageId>> _traverseSolution() => resetStack(() { | |
| 305 return new Traverser(this).traverse().catchError((error) { | |
| 306 if (error is! SolveFailure) throw error; | |
| 307 | |
| 308 return _backtrack(error).then((canTry) { | |
| 309 if (canTry) { | |
| 310 _attemptedSolutions++; | |
| 311 return _traverseSolution(); | |
| 312 } | |
| 313 | |
| 314 // All out of solutions, so fail. | |
| 315 throw error; | |
| 316 }); | |
| 317 }); | |
| 318 }); | |
| 319 | |
| 320 /// Backtracks from the current failed solution and determines the next | |
| 321 /// solution to try. | |
| 322 /// | |
| 323 /// If possible, it will backjump based on the cause of the [failure] to | |
| 324 /// minize backtracking. Otherwise, it will simply backtrack to the next | |
| 325 /// possible solution. | |
| 326 /// | |
| 327 /// Returns `true` if there is a new solution to try. | |
| 328 Future<bool> _backtrack(SolveFailure failure) { | |
| 329 // Bail if there is nothing to backtrack to. | |
| 330 if (_selected.isEmpty) return new Future.value(false); | |
| 331 | |
| 332 // Mark any packages that may have led to this failure so that we know to | |
| 333 // consider them when backtracking. | |
| 334 var dependers = _getTransitiveDependers(failure.package); | |
| 335 | |
| 336 for (var selected in _selected) { | |
| 337 if (dependers.contains(selected.current.name)) { | |
| 338 selected.fail(); | |
| 339 } | |
| 340 } | |
| 341 | |
| 342 // Advance past the current version of the leaf-most package. | |
| 343 advanceVersion() { | |
| 344 _backjump(failure); | |
| 345 var previous = _selected.last.current; | |
| 346 return _selected.last.advance().then((success) { | |
| 347 if (success) { | |
| 348 logSolve(); | |
| 349 return true; | |
| 350 } | |
| 351 | |
| 352 logSolve('$previous is last version, backtracking'); | |
| 353 | |
| 354 // That package has no more versions, so pop it and try the next one. | |
| 355 _selected.removeLast(); | |
| 356 if (_selected.isEmpty) return false; | |
| 357 | |
| 358 // If we got here, the leafmost package was discarded so we need to | |
| 359 // advance the next one. | |
| 360 return advanceVersion(); | |
| 361 }); | |
| 362 } | |
| 363 | |
| 364 return advanceVersion(); | |
| 365 } | |
| 366 | |
| 367 /// Walks the selected packages from most to least recent to determine which | |
| 368 /// ones can be ignored and jumped over by the backtracker. | |
| 369 /// | |
| 370 /// The only packages we need to backtrack to are ones that led (possibly | |
| 371 /// indirectly) to the failure. Everything else can be skipped. | |
| 372 void _backjump(SolveFailure failure) { | |
| 373 for (var i = _selected.length - 1; i >= 0; i--) { | |
| 374 // Each queue will never be empty since it gets discarded by _backtrack() | |
| 375 // when that happens. | |
| 376 var selected = _selected[i].current; | |
| 377 | |
| 378 // If the failure is a disjoint version range, then no possible versions | |
| 379 // for that package can match and there's no reason to try them. Instead, | |
| 380 // just backjump past it. | |
| 381 if (failure is DisjointConstraintException && | |
| 382 selected.name == failure.package) { | |
| 383 logSolve("skipping past disjoint selected ${selected.name}"); | |
| 384 continue; | |
| 385 } | |
| 386 | |
| 387 if (_selected[i].hasFailed) { | |
| 388 logSolve('backjump to ${selected.name}'); | |
| 389 _selected.removeRange(i + 1, _selected.length); | |
| 390 return; | |
| 391 } | |
| 392 } | |
| 393 | |
| 394 // If we got here, we walked the entire list without finding a package that | |
| 395 // could lead to another solution, so discard everything. This will happen | |
| 396 // if every package that led to the failure has no other versions that it | |
| 397 // can try to select. | |
| 398 _selected.removeRange(1, _selected.length); | |
| 399 } | |
| 400 | |
| 401 /// Gets the set of currently selected packages that depend on [dependency] | |
| 402 /// either directly or indirectly. | |
| 403 /// | |
| 404 /// When backtracking, it's only useful to consider changing the version of | |
| 405 /// packages who have a dependency on the failed package that triggered | |
| 406 /// backtracking. This is used to determine those packages. | |
| 407 /// | |
| 408 /// We calculate the full set up front before backtracking because during | |
| 409 /// backtracking, we will unselect packages and start to lose this | |
| 410 /// information in the middle of the process. | |
| 411 /// | |
| 412 /// For example, consider dependencies A -> B -> C. We've selected A and B | |
| 413 /// then encounter a problem with C. We start backtracking. B has no more | |
| 414 /// versions so we discard it and keep backtracking to A. When we get there, | |
| 415 /// since we've unselected B, we no longer realize that A had a transitive | |
| 416 /// dependency on C. We would end up backjumping over A and failing. | |
| 417 /// | |
| 418 /// Calculating the dependency set up front before we start backtracking | |
| 419 /// solves that. | |
| 420 Set<String> _getTransitiveDependers(String dependency) { | |
| 421 // Generate a reverse dependency graph. For each package, create edges to | |
| 422 // each package that depends on it. | |
| 423 var dependers = new Map<String, Set<String>>(); | |
| 424 | |
| 425 addDependencies(name, deps) { | |
| 426 dependers.putIfAbsent(name, () => new Set<String>()); | |
| 427 for (var dep in deps) { | |
| 428 dependers.putIfAbsent(dep.name, () => new Set<String>()).add(name); | |
| 429 } | |
| 430 } | |
| 431 | |
| 432 for (var i = 0; i < _selected.length; i++) { | |
| 433 var id = _selected[i].current; | |
| 434 var pubspec = cache.getCachedPubspec(id); | |
| 435 if (pubspec != null) addDependencies(id.name, pubspec.dependencies); | |
| 436 } | |
| 437 | |
| 438 // Include the root package's dependencies. | |
| 439 addDependencies(root.name, root.immediateDependencies); | |
| 440 | |
| 441 // Now walk the depending graph to see which packages transitively depend | |
| 442 // on [dependency]. | |
| 443 var visited = new Set<String>(); | |
| 444 walk(String package) { | |
| 445 // Don't get stuck in cycles. | |
| 446 if (visited.contains(package)) return; | |
| 447 visited.add(package); | |
| 448 var depender = dependers[package].forEach(walk); | |
| 449 } | |
| 450 | |
| 451 walk(dependency); | |
| 452 return visited; | |
| 453 } | |
| 454 | |
| 455 /// Logs the initial parameters to the solver. | |
| 456 void _logParameters() { | |
| 457 var buffer = new StringBuffer(); | |
| 458 buffer.writeln("Solving dependencies:"); | |
| 459 for (var package in root.dependencies) { | |
| 460 buffer.write("- $package"); | |
| 461 var locked = getLocked(package.name); | |
| 462 if (_forceLatest.contains(package.name)) { | |
| 463 buffer.write(" (use latest)"); | |
| 464 } else if (locked != null) { | |
| 465 var version = locked.version; | |
| 466 buffer.write(" (locked to $version)"); | |
| 467 } | |
| 468 buffer.writeln(); | |
| 469 } | |
| 470 log.solver(buffer.toString().trim()); | |
| 471 } | |
| 472 | |
| 473 /// Logs [message] in the context of the current selected packages. | |
| 474 /// | |
| 475 /// If [message] is omitted, just logs a description of leaf-most selection. | |
| 476 void logSolve([String message]) { | |
| 477 if (message == null) { | |
| 478 if (_selected.isEmpty) { | |
| 479 message = "* start at root"; | |
| 480 } else { | |
| 481 message = "* select ${_selected.last.current}"; | |
| 482 } | |
| 483 } else { | |
| 484 // Otherwise, indent it under the current selected package. | |
| 485 message = prefixLines(message); | |
| 486 } | |
| 487 | |
| 488 // Indent for the previous selections. | |
| 489 var prefix = _selected.skip(1).map((_) => '| ').join(); | |
| 490 log.solver(prefixLines(message, prefix: prefix)); | |
| 491 } | |
| 492 } | |
| 493 | |
| 494 /// Given the solver's current set of selected package versions, this tries to | |
| 495 /// traverse the dependency graph and see if a complete set of valid versions | |
| 496 /// has been chosen. | |
| 497 /// | |
| 498 /// If it reaches a conflict, it fails and stops traversing. If it reaches a | |
| 499 /// package that isn't selected, it refines the solution by adding that | |
| 500 /// package's set of allowed versions to the solver and then select the best | |
| 501 /// one and continuing. | |
| 502 class Traverser { | |
| 503 final BacktrackingSolver _solver; | |
| 504 | |
| 505 /// The queue of packages left to traverse. | |
| 506 /// | |
| 507 /// We do a breadth-first traversal using an explicit queue just to avoid the | |
| 508 /// code complexity of a recursive asynchronous traversal. | |
| 509 final _packages = new Queue<PackageId>(); | |
| 510 | |
| 511 /// The packages we have already traversed. | |
| 512 /// | |
| 513 /// Used to avoid traversing the same package multiple times, and to build | |
| 514 /// the complete solution results. | |
| 515 final _visited = new Set<PackageId>(); | |
| 516 | |
| 517 /// The dependencies visited so far in the traversal. | |
| 518 /// | |
| 519 /// For each package name (the map key) we track the list of dependencies | |
| 520 /// that other packages have placed on it so that we can calculate the | |
| 521 /// complete constraint for shared dependencies. | |
| 522 final _dependencies = <String, List<Dependency>>{}; | |
| 523 | |
| 524 Traverser(this._solver); | |
| 525 | |
| 526 /// Walks the dependency graph starting at the root package and validates | |
| 527 /// that each reached package has a valid version selected. | |
| 528 Future<List<PackageId>> traverse() { | |
| 529 // Start at the root. | |
| 530 _packages.add(new PackageId.root(_solver.root)); | |
| 531 return _traversePackage(); | |
| 532 } | |
| 533 | |
| 534 /// Traverses the next package in the queue. | |
| 535 /// | |
| 536 /// Completes to a list of package IDs if the traversal completed | |
| 537 /// successfully and found a solution. Completes to an error if the traversal | |
| 538 /// failed. Otherwise, recurses to the next package in the queue, etc. | |
| 539 Future<List<PackageId>> _traversePackage() { | |
| 540 if (_packages.isEmpty) { | |
| 541 // We traversed the whole graph. If we got here, we successfully found | |
| 542 // a solution. | |
| 543 return new Future<List<PackageId>>.value(_visited.toList()); | |
| 544 } | |
| 545 | |
| 546 var id = _packages.removeFirst(); | |
| 547 | |
| 548 // Don't visit the same package twice. | |
| 549 if (_visited.contains(id)) { | |
| 550 return _traversePackage(); | |
| 551 } | |
| 552 _visited.add(id); | |
| 553 | |
| 554 return _solver.cache.getPubspec(id).then((pubspec) { | |
| 555 _validateSdkConstraint(pubspec); | |
| 556 | |
| 557 var deps = pubspec.dependencies.toSet(); | |
| 558 | |
| 559 if (id.isRoot) { | |
| 560 // Include dev dependencies of the root package. | |
| 561 deps.addAll(pubspec.devDependencies); | |
| 562 | |
| 563 // Add all overrides. This ensures a dependency only present as an | |
| 564 // override is still included. | |
| 565 deps.addAll(_solver._overrides.values); | |
| 566 } | |
| 567 | |
| 568 // Replace any overridden dependencies. | |
| 569 deps = deps.map((dep) { | |
| 570 var override = _solver._overrides[dep.name]; | |
| 571 if (override != null) return override; | |
| 572 | |
| 573 // Not overridden. | |
| 574 return dep; | |
| 575 }).toSet(); | |
| 576 | |
| 577 // Make sure the package doesn't have any bad dependencies. | |
| 578 for (var dep in deps) { | |
| 579 if (!dep.isRoot && _solver.sources[dep.source] is UnknownSource) { | |
| 580 throw new UnknownSourceException( | |
| 581 id.name, | |
| 582 [new Dependency(id.name, id.version, dep)]); | |
| 583 } | |
| 584 } | |
| 585 | |
| 586 return _traverseDeps(id, new DependencyQueue(_solver, deps)); | |
| 587 }).catchError((error) { | |
| 588 if (error is! PackageNotFoundException) throw error; | |
| 589 | |
| 590 // We can only get here if the lockfile refers to a specific package | |
| 591 // version that doesn't exist (probably because it was yanked). | |
| 592 throw new NoVersionException(id.name, null, id.version, []); | |
| 593 }); | |
| 594 } | |
| 595 | |
| 596 /// Traverses the references that [depender] depends on, stored in [deps]. | |
| 597 /// | |
| 598 /// Desctructively modifies [deps]. Completes to a list of packages if the | |
| 599 /// traversal is complete. Completes it to an error if a failure occurred. | |
| 600 /// Otherwise, recurses. | |
| 601 Future<List<PackageId>> _traverseDeps(PackageId depender, | |
| 602 DependencyQueue deps) { | |
| 603 // Move onto the next package if we've traversed all of these references. | |
| 604 if (deps.isEmpty) return _traversePackage(); | |
| 605 | |
| 606 return resetStack(() { | |
| 607 return deps.advance().then((dep) { | |
| 608 var dependency = new Dependency(depender.name, depender.version, dep); | |
| 609 return _registerDependency(dependency).then((_) { | |
| 610 if (dep.name == "barback") return _addImplicitDependencies(); | |
| 611 }); | |
| 612 }).then((_) => _traverseDeps(depender, deps)); | |
| 613 }); | |
| 614 } | |
| 615 | |
| 616 /// Register [dependency]'s constraints on the package it depends on and | |
| 617 /// enqueues the package for processing if necessary. | |
| 618 Future _registerDependency(Dependency dependency) { | |
| 619 return new Future.sync(() { | |
| 620 _validateDependency(dependency); | |
| 621 | |
| 622 var dep = dependency.dep; | |
| 623 var dependencies = _getDependencies(dep.name); | |
| 624 dependencies.add(dependency); | |
| 625 | |
| 626 var constraint = _getConstraint(dep.name); | |
| 627 | |
| 628 // See if it's possible for a package to match that constraint. | |
| 629 if (constraint.isEmpty) { | |
| 630 var constraints = dependencies.map( | |
| 631 (dep) => " ${dep.dep.constraint} from ${dep.depender}").join('\n'); | |
| 632 _solver.logSolve('disjoint constraints on ${dep.name}:\n$constraints'); | |
| 633 throw new DisjointConstraintException(dep.name, dependencies); | |
| 634 } | |
| 635 | |
| 636 var selected = _validateSelected(dep, constraint); | |
| 637 if (selected != null) { | |
| 638 // The selected package version is good, so enqueue it to traverse | |
| 639 // into it. | |
| 640 _packages.add(selected); | |
| 641 return null; | |
| 642 } | |
| 643 | |
| 644 // We haven't selected a version. Try all of the versions that match | |
| 645 // the constraints we currently have for this package. | |
| 646 var locked = _getValidLocked(dep.name); | |
| 647 | |
| 648 return VersionQueue.create(locked, () { | |
| 649 return _getAllowedVersions(dep); | |
| 650 }).then((versions) => _packages.add(_solver.select(versions))); | |
| 651 }); | |
| 652 } | |
| 653 | |
| 654 /// Gets all versions of [dep] that match the current constraints placed on | |
| 655 /// it. | |
| 656 Future<Iterable<PackageId>> _getAllowedVersions(PackageDep dep) { | |
| 657 var constraint = _getConstraint(dep.name); | |
| 658 return _solver.cache.getVersions(dep.toRef()).then((versions) { | |
| 659 var allowed = versions.where((id) => constraint.allows(id.version)); | |
| 660 | |
| 661 if (allowed.isEmpty) { | |
| 662 _solver.logSolve('no versions for ${dep.name} match $constraint'); | |
| 663 throw new NoVersionException( | |
| 664 dep.name, | |
| 665 null, | |
| 666 constraint, | |
| 667 _getDependencies(dep.name)); | |
| 668 } | |
| 669 | |
| 670 // If we're doing an upgrade on this package, only allow the latest | |
| 671 // version. | |
| 672 if (_solver._forceLatest.contains(dep.name)) allowed = [allowed.first]; | |
| 673 | |
| 674 // Remove the locked version, if any, since that was already handled. | |
| 675 var locked = _getValidLocked(dep.name); | |
| 676 if (locked != null) { | |
| 677 allowed = allowed.where((dep) => dep.version != locked.version); | |
| 678 } | |
| 679 | |
| 680 return allowed; | |
| 681 }).catchError((error, stackTrace) { | |
| 682 if (error is PackageNotFoundException) { | |
| 683 // Show the user why the package was being requested. | |
| 684 throw new DependencyNotFoundException( | |
| 685 dep.name, | |
| 686 error, | |
| 687 _getDependencies(dep.name)); | |
| 688 } | |
| 689 | |
| 690 throw error; | |
| 691 }); | |
| 692 } | |
| 693 | |
| 694 /// Ensures that dependency [dep] from [depender] is consistent with the | |
| 695 /// other dependencies on the same package. | |
| 696 /// | |
| 697 /// Throws a [SolveFailure] exception if not. Only validates sources and | |
| 698 /// descriptions, not the version. | |
| 699 void _validateDependency(Dependency dependency) { | |
| 700 var dep = dependency.dep; | |
| 701 | |
| 702 // Make sure the dependencies agree on source and description. | |
| 703 var required = _getRequired(dep.name); | |
| 704 if (required == null) return; | |
| 705 | |
| 706 // Make sure all of the existing sources match the new reference. | |
| 707 if (required.dep.source != dep.source) { | |
| 708 _solver.logSolve( | |
| 709 'source mismatch on ${dep.name}: ${required.dep.source} ' '!= ${dep.so
urce}'); | |
| 710 throw new SourceMismatchException(dep.name, [required, dependency]); | |
| 711 } | |
| 712 | |
| 713 // Make sure all of the existing descriptions match the new reference. | |
| 714 var source = _solver.sources[dep.source]; | |
| 715 if (!source.descriptionsEqual(dep.description, required.dep.description)) { | |
| 716 _solver.logSolve( | |
| 717 'description mismatch on ${dep.name}: ' | |
| 718 '${required.dep.description} != ${dep.description}'); | |
| 719 throw new DescriptionMismatchException(dep.name, [required, dependency]); | |
| 720 } | |
| 721 } | |
| 722 | |
| 723 /// Validates the currently selected package against the new dependency that | |
| 724 /// [dep] and [constraint] place on it. | |
| 725 /// | |
| 726 /// Returns `null` if there is no currently selected package, throws a | |
| 727 /// [SolveFailure] if the new reference it not does not allow the previously | |
| 728 /// selected version, or returns the selected package if successful. | |
| 729 PackageId _validateSelected(PackageDep dep, VersionConstraint constraint) { | |
| 730 var selected = _solver.getSelected(dep.name); | |
| 731 if (selected == null) return null; | |
| 732 | |
| 733 // Make sure it meets the constraint. | |
| 734 if (!dep.constraint.allows(selected.version)) { | |
| 735 _solver.logSolve('selection $selected does not match $constraint'); | |
| 736 throw new NoVersionException( | |
| 737 dep.name, | |
| 738 selected.version, | |
| 739 constraint, | |
| 740 _getDependencies(dep.name)); | |
| 741 } | |
| 742 | |
| 743 return selected; | |
| 744 } | |
| 745 | |
| 746 /// Register pub's implicit dependencies. | |
| 747 /// | |
| 748 /// Pub has an implicit version constraint on barback and various other | |
| 749 /// packages used in barback's plugin isolate. | |
| 750 Future _addImplicitDependencies() { | |
| 751 /// Ensure we only add the barback dependency once. | |
| 752 if (_getDependencies("barback").length != 1) return new Future.value(); | |
| 753 | |
| 754 return Future.wait(barback.pubConstraints.keys.map((depName) { | |
| 755 var constraint = barback.pubConstraints[depName]; | |
| 756 _solver.logSolve( | |
| 757 'add implicit $constraint pub dependency on ' '$depName'); | |
| 758 | |
| 759 var override = _solver._overrides[depName]; | |
| 760 | |
| 761 // Use the same source and description as the dependency override if one | |
| 762 // exists. This is mainly used by the pkgbuild tests, which use dependency | |
| 763 // overrides for all repo packages. | |
| 764 var pubDep = override == null ? | |
| 765 new PackageDep(depName, "hosted", constraint, depName) : | |
| 766 override.withConstraint(constraint); | |
| 767 return _registerDependency( | |
| 768 new Dependency("pub itself", Version.none, pubDep)); | |
| 769 })); | |
| 770 } | |
| 771 | |
| 772 /// Gets the list of dependencies for package [name]. | |
| 773 /// | |
| 774 /// Creates an empty list if needed. | |
| 775 List<Dependency> _getDependencies(String name) { | |
| 776 return _dependencies.putIfAbsent(name, () => <Dependency>[]); | |
| 777 } | |
| 778 | |
| 779 /// Gets a "required" reference to the package [name]. | |
| 780 /// | |
| 781 /// This is the first non-root dependency on that package. All dependencies | |
| 782 /// on a package must agree on source and description, except for references | |
| 783 /// to the root package. This will return a reference to that "canonical" | |
| 784 /// source and description, or `null` if there is no required reference yet. | |
| 785 /// | |
| 786 /// This is required because you may have a circular dependency back onto the | |
| 787 /// root package. That second dependency won't be a root dependency and it's | |
| 788 /// *that* one that other dependencies need to agree on. In other words, you | |
| 789 /// can have a bunch of dependencies back onto the root package as long as | |
| 790 /// they all agree with each other. | |
| 791 Dependency _getRequired(String name) { | |
| 792 return _getDependencies( | |
| 793 name).firstWhere((dep) => !dep.dep.isRoot, orElse: () => null); | |
| 794 } | |
| 795 | |
| 796 /// Gets the combined [VersionConstraint] currently being placed on package | |
| 797 /// [name]. | |
| 798 VersionConstraint _getConstraint(String name) { | |
| 799 var constraint = _getDependencies( | |
| 800 name).map( | |
| 801 (dep) => | |
| 802 dep.dep.constraint).fold(VersionConstraint.any, (a, b) => a.inte
rsect(b)); | |
| 803 | |
| 804 return constraint; | |
| 805 } | |
| 806 | |
| 807 /// Gets the package [name] that's currently contained in the lockfile if it | |
| 808 /// meets [constraint] and has the same source and description as other | |
| 809 /// references to that package. | |
| 810 /// | |
| 811 /// Returns `null` otherwise. | |
| 812 PackageId _getValidLocked(String name) { | |
| 813 var package = _solver.getLocked(name); | |
| 814 if (package == null) return null; | |
| 815 | |
| 816 var constraint = _getConstraint(name); | |
| 817 if (!constraint.allows(package.version)) { | |
| 818 _solver.logSolve('$package is locked but does not match $constraint'); | |
| 819 return null; | |
| 820 } else { | |
| 821 _solver.logSolve('$package is locked'); | |
| 822 } | |
| 823 | |
| 824 var required = _getRequired(name); | |
| 825 if (required != null) { | |
| 826 if (package.source != required.dep.source) return null; | |
| 827 | |
| 828 var source = _solver.sources[package.source]; | |
| 829 if (!source.descriptionsEqual( | |
| 830 package.description, | |
| 831 required.dep.description)) return null; | |
| 832 } | |
| 833 | |
| 834 return package; | |
| 835 } | |
| 836 | |
| 837 /// Run the dart2js compiler. | |
| 838 Future _doCompilation(Transform transform) { | |
| 839 var provider = new _BarbackCompilerProvider(_environment, transform, | |
| 840 generateSourceMaps: _generateSourceMaps); | |
| 841 | |
| 842 // Create a "path" to the entrypoint script. The entrypoint may not actually | |
| 843 // be on disk, but this gives dart2js a root to resolve relative paths | |
| 844 // against. | |
| 845 var id = transform.primaryInput.id; | |
| 846 | |
| 847 var entrypoint = _environment.graph.packages[id.package].path(id.path); | |
| 848 | |
| 849 // Should have more sophisticated error-handling here. Need | |
| 850 // to report compile errors to the user in an easily visible way. Need to | |
| 851 // make sure paths in errors are mapped to the original source path so they | |
| 852 // can understand them. | |
| 853 return dart.compile( | |
| 854 entrypoint, provider, | |
| 855 commandLineOptions: _configCommandLineOptions, | |
| 856 csp: _configBool('csp'), | |
| 857 checked: _configBool('checked'), | |
| 858 minify: _configBool( | |
| 859 'minify', defaultsTo: _settings.mode == BarbackMode.RELEASE), | |
| 860 verbose: _configBool('verbose'), | |
| 861 environment: _configEnvironment, | |
| 862 packageRoot: _environment.rootPackage.path("packages"), | |
| 863 analyzeAll: _configBool('analyzeAll'), | |
| 864 suppressWarnings: _configBool('suppressWarnings'), | |
| 865 suppressHints: _configBool('suppressHints'), | |
| 866 suppressPackageWarnings: _configBool( | |
| 867 'suppressPackageWarnings', defaultsTo: true), | |
| 868 terse: _configBool('terse'), | |
| 869 includeSourceMapUrls: _settings.mode != BarbackMode.RELEASE); | |
| 870 } | |
| 871 } | |
| 872 | |
| 873 /// Ensures that if [pubspec] has an SDK constraint, then it is compatible | |
| 874 /// with the current SDK. | |
| 875 /// | |
| 876 /// Throws a [SolveFailure] if not. | |
| 877 void _validateSdkConstraint(Pubspec pubspec) { | |
| 878 if (pubspec.environment.sdkVersion.allows(sdk.version)) return; | |
| 879 | |
| 880 throw new BadSdkVersionException( | |
| 881 pubspec.name, | |
| 882 'Package ${pubspec.name} requires SDK version ' | |
| 883 '${pubspec.environment.sdkVersion} but the current SDK is ' '${sdk.ver
sion}.'); | |
| 884 } | |
| 885 """; | |
| OLD | NEW |