Index: utils/pub/solver/greedy_solver.dart |
diff --git a/utils/pub/solver/greedy_solver.dart b/utils/pub/solver/greedy_solver.dart |
deleted file mode 100644 |
index e664ea2d07b3a180e10fe89c9b03c87114615895..0000000000000000000000000000000000000000 |
--- a/utils/pub/solver/greedy_solver.dart |
+++ /dev/null |
@@ -1,556 +0,0 @@ |
-// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
-// for details. All rights reserved. Use of this source code is governed by a |
-// BSD-style license that can be found in the LICENSE file. |
- |
-/// Attempts to resolve a set of version constraints for a package dependency |
-/// graph and select an appropriate set of best specific versions for all |
-/// dependent packages. It works iteratively and tries to reach a stable |
-/// solution where the constraints of all dependencies are met. If it fails to |
-/// reach a solution after a certain number of iterations, it assumes the |
-/// dependency graph is unstable and reports and error. |
-/// |
-/// There are two fundamental operations in the process of iterating over the |
-/// graph: |
-/// |
-/// 1. Changing the selected concrete version of some package. (This includes |
-/// adding and removing a package too, which is considering changing the |
-/// version to or from "none".) In other words, a node has changed. |
-/// 2. Changing the version constraint that one package places on another. In |
-/// other words, and edge has changed. |
-/// |
-/// Both of these events have a corresponding (potentional) async operation and |
-/// roughly cycle back and forth between each other. When we change the version |
-/// of package changes, we asynchronously load the pubspec for the new version. |
-/// When that's done, we compare the dependencies of the new version versus the |
-/// old one. For everything that differs, we change those constraints between |
-/// this package and that dependency. |
-/// |
-/// When a constraint on a package changes, we re-calculate the overall |
-/// constraint on that package. I.e. with a shared dependency, we intersect all |
-/// of the constraints that its depending packages place on it. If that overall |
-/// constraint changes (say from "<3.0.0" to "<2.5.0"), then the currently |
-/// picked version for that package may fall outside of the new constraint. If |
-/// that happens, we find the new best version that meets the updated constraint |
-/// and then the change the package to use that version. That cycles back up to |
-/// the beginning again. |
-library version_solver1; |
- |
-import 'dart:async'; |
-import 'dart:collection' show Queue; |
-import 'dart:math' as math; |
- |
-import '../lock_file.dart'; |
-import '../log.dart' as log; |
-import '../package.dart'; |
-import '../source.dart'; |
-import '../source_registry.dart'; |
-import '../version.dart'; |
-import 'version_solver.dart'; |
- |
-class GreedyVersionSolver extends VersionSolver { |
- final _packages = <String, DependencyNode>{}; |
- final _work = new Queue<WorkItem>(); |
- int _numIterations = 0; |
- |
- GreedyVersionSolver(SourceRegistry sources, Package root, LockFile lockFile, |
- List<String> useLatest) |
- : super(sources, root, lockFile, useLatest); |
- |
- /// The non-backtracking solver always only tries one solution. |
- int get attemptedSolutions => 1; |
- |
- void forceLatestVersion(String package) { |
- // TODO(nweiz): How do we want to detect and handle unknown dependencies |
- // here? |
- getDependency(package).useLatestVersion = true; |
- } |
- |
- Future<List<PackageId>> runSolver() { |
- // Kick off the work by adding the root package at its concrete version to |
- // the dependency graph. |
- enqueue(new AddConstraint('(entrypoint)', new PackageRef.root(root))); |
- |
- Future processNextWorkItem(_) { |
- while (true) { |
- // Stop if we are done. |
- if (_work.isEmpty) return new Future.value(buildResults()); |
- |
- // If we appear to be stuck in a loop, then we probably have an unstable |
- // graph, bail. We guess this based on a rough heuristic that it should |
- // only take a certain number of steps to solve a graph with a given |
- // number of connections. |
- // TODO(rnystrom): These numbers here are magic and arbitrary. Tune |
- // when we have a better picture of real-world package topologies. |
- _numIterations++; |
- if (_numIterations > math.max(50, _packages.length * 5)) { |
- throw new CouldNotSolveException(); |
- } |
- |
- // Run the first work item. |
- var future = _work.removeFirst().process(this); |
- |
- // If we have an async operation to perform, chain the loop to resume |
- // when it's done. Otherwise, just loop synchronously. |
- if (future != null) { |
- return future.then(processNextWorkItem); |
- } |
- } |
- } |
- |
- return processNextWorkItem(null); |
- } |
- |
- void enqueue(WorkItem work) { |
- _work.add(work); |
- } |
- |
- DependencyNode getDependency(String package) { |
- // There can be unused dependencies in the graph, so just create an empty |
- // one if needed. |
- _packages.putIfAbsent(package, () => new DependencyNode(package)); |
- return _packages[package]; |
- } |
- |
- /// Sets the best selected version of [package] to [version]. |
- void setVersion(String package, Version version) { |
- _packages[package].version = version; |
- } |
- |
- /// Returns the most recent version of [dependency] that satisfies all of its |
- /// version constraints. |
- Future<Version> getBestVersion(DependencyNode dependency) { |
- return cache.getVersions(dependency.name, |
- dependency.source, dependency.description).then((versions) { |
- var best = null; |
- for (var ref in versions) { |
- if (dependency.useLatestVersion || |
- dependency.constraint.allows(ref.version)) { |
- if (best == null || ref.version > best) best = ref.version; |
- } |
- } |
- |
- // TODO(rnystrom): Better exception. |
- if (best == null) { |
- if (tryUnlockDepender(dependency)) return null; |
- throw new NoVersionException(dependency.name, dependency.constraint, |
- dependency.toList()); |
- } else if (!dependency.constraint.allows(best)) { |
- if (tryUnlockDepender(dependency)) return null; |
- throw new CouldNotUpdateException( |
- dependency.name, dependency.constraint, best); |
- } |
- |
- return best; |
- }); |
- } |
- |
- /// Looks for a package that depends (transitively) on [dependency] and has |
- /// its version locked in the lockfile. If one is found, enqueues an |
- /// [UnlockPackage] work item for it and returns true. Otherwise, returns |
- /// false. |
- /// |
- /// This does a breadth-first search; immediate dependers will be unlocked |
- /// first, followed by transitive dependers. |
- bool tryUnlockDepender(DependencyNode dependency, [Set<String> seen]) { |
- if (seen == null) seen = new Set(); |
- // Avoid an infinite loop if there are circular dependencies. |
- if (seen.contains(dependency.name)) return false; |
- seen.add(dependency.name); |
- |
- for (var dependerName in dependency.dependers) { |
- var depender = getDependency(dependerName); |
- var locked = lockFile.packages[dependerName]; |
- if (locked != null && depender.version == locked.version && |
- depender.source.name == locked.source.name) { |
- enqueue(new UnlockPackage(depender)); |
- return true; |
- } |
- } |
- |
- return dependency.dependers.map(getDependency).any((subdependency) => |
- tryUnlockDepender(subdependency, seen)); |
- } |
- |
- List<PackageId> buildResults() { |
- return _packages.values |
- .where((dep) => dep.isDependedOn) |
- .map(_dependencyToPackageId) |
- .toList(); |
- } |
- |
- PackageId _dependencyToPackageId(DependencyNode dep) { |
- var description = dep.description; |
- |
- // If the lockfile contains a fully-resolved description for the package, |
- // use that. This allows e.g. Git to ensure that the same commit is used. |
- var lockedPackage = lockFile.packages[dep.name]; |
- if (lockedPackage != null && lockedPackage.version == dep.version && |
- lockedPackage.source.name == dep.source.name && |
- dep.source.descriptionsEqual( |
- description, lockedPackage.description)) { |
- description = lockedPackage.description; |
- } |
- |
- return new PackageId(dep.name, dep.source, dep.version, description); |
- } |
-} |
- |
-/// The constraint solver works by iteratively processing a queue of work items. |
-/// Each item is a single atomic change to the dependency graph. Handling them |
-/// in a queue lets us handle asynchrony (resolving versions requires |
-/// information from servers) as well as avoid deeply nested recursion. |
-abstract class WorkItem { |
- /// Processes this work item. Returns a future that completes when the work is |
- /// done. If `null` is returned, that means the work has completed |
- /// synchronously and the next item can be started immediately. |
- Future process(GreedyVersionSolver solver); |
-} |
- |
-/// The best selected version for a package has changed to [version]. If the |
-/// previous version of the package is `null`, that means the package is being |
-/// added to the graph. If [version] is `null`, it is being removed. |
-class ChangeVersion implements WorkItem { |
- /// The name of the package whose version is being changed. |
- final String package; |
- |
- /// The source of the package whose version is changing. |
- final Source source; |
- |
- /// The description identifying the package whose version is changing. |
- final description; |
- |
- /// The new selected version. |
- final Version version; |
- |
- ChangeVersion(this.package, this.source, this.description, this.version); |
- |
- Future process(GreedyVersionSolver solver) { |
- log.fine("Changing $package to version $version."); |
- |
- var dependency = solver.getDependency(package); |
- var oldVersion = dependency.version; |
- solver.setVersion(package, version); |
- |
- // The dependencies between the old and new version may be different. Walk |
- // them both and update any constraints that differ between the two. |
- return Future.wait([ |
- getDependencyRefs(solver, oldVersion), |
- getDependencyRefs(solver, version)]).then((list) { |
- var oldDependencyRefs = list[0]; |
- var newDependencyRefs = list[1]; |
- |
- for (var oldRef in oldDependencyRefs.values) { |
- if (newDependencyRefs.containsKey(oldRef.name)) { |
- // The dependency is in both versions of this package, but its |
- // constraint may have changed. |
- var newRef = newDependencyRefs.remove(oldRef.name); |
- solver.enqueue(new AddConstraint(package, newRef)); |
- } else { |
- // The dependency is not in the new version of the package, so just |
- // remove its constraint. |
- solver.enqueue(new RemoveConstraint(package, oldRef.name)); |
- } |
- } |
- |
- // Everything that's left is a depdendency that's only in the new |
- // version of the package. |
- for (var newRef in newDependencyRefs.values) { |
- solver.enqueue(new AddConstraint(package, newRef)); |
- } |
- }); |
- } |
- |
- /// Get the dependencies at [version] of the package being changed. |
- Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver, |
- Version version) { |
- // If there is no version, it means no package, so no dependencies. |
- if (version == null) { |
- return new Future<Map<String, PackageRef>>.value(<String, PackageRef>{}); |
- } |
- |
- var id = new PackageId(package, source, version, description); |
- return solver.cache.getPubspec(id).then((pubspec) { |
- var dependencies = <String, PackageRef>{}; |
- for (var dependency in pubspec.dependencies) { |
- dependencies[dependency.name] = dependency; |
- } |
- |
- // Include dev dependencies only from the root package. |
- if (id.isRoot) { |
- for (var dependency in pubspec.devDependencies) { |
- dependencies[dependency.name] = dependency; |
- } |
- } |
- |
- return dependencies; |
- }); |
- } |
-} |
- |
-/// A constraint that a depending package places on a dependent package has |
-/// changed. |
-/// |
-/// This is an abstract class that contains logic for updating the dependency |
-/// graph once a dependency has changed. Changing the dependency is the |
-/// responsibility of subclasses. |
-abstract class ChangeConstraint implements WorkItem { |
- Future process(GreedyVersionSolver solver); |
- |
- void undo(GreedyVersionSolver solver); |
- |
- Future _processChange(GreedyVersionSolver solver, |
- DependencyNode oldDependency, |
- DependencyNode newDependency) { |
- var name = newDependency.name; |
- var source = oldDependency.source != null ? |
- oldDependency.source : newDependency.source; |
- var description = oldDependency.description != null ? |
- oldDependency.description : newDependency.description; |
- var oldConstraint = oldDependency.constraint; |
- var newConstraint = newDependency.constraint; |
- |
- // If the package is over-constrained, i.e. the packages depending have |
- // disjoint constraints, then try unlocking a depender that's locked by the |
- // lockfile. If there are no remaining locked dependencies, throw an error. |
- if (newConstraint != null && newConstraint.isEmpty) { |
- if (solver.tryUnlockDepender(newDependency)) { |
- undo(solver); |
- return null; |
- } |
- |
- throw new DisjointConstraintException(name, newDependency.toList()); |
- } |
- |
- // If this constraint change didn't cause the overall constraint on the |
- // package to change, then we don't need to do any further work. |
- if (oldConstraint == newConstraint) return null; |
- |
- // If the dependency has been cut free from the graph, just remove it. |
- if (!newDependency.isDependedOn) { |
- solver.enqueue(new ChangeVersion(name, source, description, null)); |
- return null; |
- } |
- |
- // If the dependency is on the root package, then we don't need to do |
- // anything since it's already at the best version. |
- if (name == solver.root.name) { |
- solver.enqueue(new ChangeVersion( |
- name, source, description, solver.root.version)); |
- return null; |
- } |
- |
- // If the dependency is on a package in the lockfile, use the lockfile's |
- // version for that package if it's valid given the other constraints. |
- var lockedPackage = solver.lockFile.packages[name]; |
- if (lockedPackage != null && newDependency.source == lockedPackage.source) { |
- var lockedVersion = lockedPackage.version; |
- if (newConstraint.allows(lockedVersion)) { |
- solver.enqueue( |
- new ChangeVersion(name, source, description, lockedVersion)); |
- return null; |
- } |
- } |
- |
- // The constraint has changed, so see what the best version of the package |
- // that meets the new constraint is. |
- return solver.getBestVersion(newDependency).then((best) { |
- if (best == null) { |
- undo(solver); |
- } else if (newDependency.version != best) { |
- solver.enqueue(new ChangeVersion(name, source, description, best)); |
- } |
- }); |
- } |
-} |
- |
-/// The constraint given by [ref] is being placed by [depender]. |
-class AddConstraint extends ChangeConstraint { |
- /// The package that has the dependency. |
- final String depender; |
- |
- /// The package being depended on and the constraints being placed on it. The |
- /// source, version, and description in this ref are all considered |
- /// constraints on the dependent package. |
- final PackageRef ref; |
- |
- AddConstraint(this.depender, this.ref); |
- |
- Future process(GreedyVersionSolver solver) { |
- log.fine("Adding $depender's constraint $ref."); |
- |
- var dependency = solver.getDependency(ref.name); |
- var oldDependency = dependency.clone(); |
- dependency.placeConstraint(depender, ref); |
- return _processChange(solver, oldDependency, dependency); |
- } |
- |
- void undo(GreedyVersionSolver solver) { |
- solver.getDependency(ref.name).removeConstraint(depender); |
- } |
-} |
- |
-/// [depender] is no longer placing a constraint on [dependent]. |
-class RemoveConstraint extends ChangeConstraint { |
- /// The package that was placing a constraint on [dependent]. |
- String depender; |
- |
- /// The package that was being depended on. |
- String dependent; |
- |
- /// The constraint that was removed. |
- PackageRef _removed; |
- |
- RemoveConstraint(this.depender, this.dependent); |
- |
- Future process(GreedyVersionSolver solver) { |
- log.fine("Removing $depender's constraint ($_removed) on $dependent."); |
- |
- var dependency = solver.getDependency(dependent); |
- var oldDependency = dependency.clone(); |
- _removed = dependency.removeConstraint(depender); |
- return _processChange(solver, oldDependency, dependency); |
- } |
- |
- void undo(GreedyVersionSolver solver) { |
- solver.getDependency(dependent).placeConstraint(depender, _removed); |
- } |
-} |
- |
-/// [package]'s version is no longer constrained by the lockfile. |
-class UnlockPackage implements WorkItem { |
- /// The package being unlocked. |
- DependencyNode package; |
- |
- UnlockPackage(this.package); |
- |
- Future process(GreedyVersionSolver solver) { |
- log.fine("Unlocking ${package.name}."); |
- |
- solver.lockFile.packages.remove(package.name); |
- return solver.getBestVersion(package).then((best) { |
- if (best == null) return null; |
- solver.enqueue(new ChangeVersion( |
- package.name, package.source, package.description, best)); |
- }); |
- } |
-} |
- |
-/// Describes one [Package] in the [DependencyGraph] and keeps track of which |
-/// packages depend on it and what constraints they place on it. |
-class DependencyNode { |
- /// The name of the this dependency's package. |
- final String name; |
- |
- /// The [PackageRefs] that represent constraints that depending packages have |
- /// placed on this one. |
- final Map<String, PackageRef> _refs; |
- |
- /// The currently-selected best version for this dependency. |
- Version version; |
- |
- /// Whether this dependency should always select the latest version. |
- bool useLatestVersion = false; |
- |
- /// Gets whether or not any other packages are currently depending on this |
- /// one. If `false`, then it means this package is not part of the dependency |
- /// graph and should be omitted. |
- bool get isDependedOn => !_refs.isEmpty; |
- |
- /// The names of all the packages that depend on this dependency. |
- Iterable<String> get dependers => _refs.keys; |
- |
- /// Gets the overall constraint that all packages are placing on this one. |
- /// If no packages have a constraint on this one (which can happen when this |
- /// package is in the process of being added to the graph), returns `null`. |
- VersionConstraint get constraint { |
- if (_refs.isEmpty) return null; |
- return new VersionConstraint.intersection( |
- _refs.values.map((ref) => ref.constraint)); |
- } |
- |
- /// The source of this dependency's package. |
- Source get source { |
- var canonical = _canonicalRef(); |
- if (canonical == null) return null; |
- return canonical.source; |
- } |
- |
- /// The description of this dependency's package. |
- get description { |
- var canonical = _canonicalRef(); |
- if (canonical == null) return null; |
- return canonical.description; |
- } |
- |
- /// Return the PackageRef that has the canonical source and description for |
- /// this package. If any dependency is on the root package, that will be used; |
- /// otherwise, it will be the source and description that all dependencies |
- /// agree upon. |
- PackageRef _canonicalRef() { |
- if (_refs.isEmpty) return null; |
- var refs = _refs.values; |
- for (var ref in refs) { |
- if (ref.isRoot) return ref; |
- } |
- return refs.first; |
- } |
- |
- DependencyNode(this.name) |
- : _refs = <String, PackageRef>{}; |
- |
- DependencyNode._clone(DependencyNode other) |
- : name = other.name, |
- version = other.version, |
- _refs = new Map<String, PackageRef>.from(other._refs); |
- |
- /// Creates a copy of this dependency. |
- DependencyNode clone() => new DependencyNode._clone(this); |
- |
- /// Places [ref] as a constraint from [package] onto this. |
- void placeConstraint(String package, PackageRef ref) { |
- var requiredDepender = _requiredDepender(); |
- if (requiredDepender != null) { |
- var required = _refs[requiredDepender]; |
- if (required.source.name != ref.source.name) { |
- throw new SourceMismatchException(name, [ |
- new Dependency(requiredDepender, required), |
- new Dependency(package, ref)]); |
- } else if (!required.descriptionEquals(ref)) { |
- throw new DescriptionMismatchException(name, [ |
- new Dependency(requiredDepender, required), |
- new Dependency(package, ref)]); |
- } |
- } |
- |
- _refs[package] = ref; |
- } |
- |
- /// Returns the name of a package whose constraint source and description |
- /// all other constraints must match. Returns null if there are no |
- /// requirements on new constraints. |
- String _requiredDepender() { |
- if (_refs.isEmpty) return null; |
- |
- var dependers = _refs.keys.toList(); |
- if (dependers.length == 1) { |
- var depender = dependers[0]; |
- if (_refs[depender].isRoot) return null; |
- return depender; |
- } |
- |
- return dependers[1]; |
- } |
- |
- /// Removes the constraint from [package] onto this. |
- PackageRef removeConstraint(String package) => _refs.remove(package); |
- |
- /// Converts this to a list of [Dependency] objects like the error types |
- /// expect. |
- List<Dependency> toList() { |
- var result = <Dependency>[]; |
- _refs.forEach((name, ref) { |
- result.add(new Dependency(name, ref)); |
- }); |
- return result; |
- } |
-} |