| Index: utils/pub/version_solver.dart
|
| diff --git a/utils/pub/version_solver.dart b/utils/pub/version_solver.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..793b1f9fd017ac19687c1230a853e9b593063a95
|
| --- /dev/null
|
| +++ b/utils/pub/version_solver.dart
|
| @@ -0,0 +1,425 @@
|
| +// 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_solver');
|
| +
|
| +#import('package.dart');
|
| +#import('pubspec.dart');
|
| +#import('source.dart');
|
| +#import('source_registry.dart');
|
| +#import('utils.dart');
|
| +#import('version.dart');
|
| +
|
| +/**
|
| + * Attempts to select the best concrete versions for all of the transitive
|
| + * dependencies of [root] taking into account all of the [VersionConstraint]s
|
| + * that those dependencies place on each other. If successful, completes to a
|
| + * [Map] that maps package names to the selected version for that package. If
|
| + * it fails, the future will complete with a [NoVersionException],
|
| + * [DisjointConstraintException], or [CouldNotSolveException].
|
| + */
|
| +Future<Map<String, Version>> resolveVersions(
|
| + SourceRegistry sources, Package root) {
|
| + return new VersionSolver(sources).solve(root);
|
| +}
|
| +
|
| +class VersionSolver {
|
| + final SourceRegistry _sources;
|
| + final PubspecCache _pubspecs;
|
| + final Map<String, Dependency> _packages;
|
| + final Queue<WorkItem> _work;
|
| + int _numIterations = 0;
|
| +
|
| + VersionSolver(SourceRegistry sources)
|
| + : _sources = sources,
|
| + _pubspecs = new PubspecCache(sources),
|
| + _packages = <Dependency>{},
|
| + _work = new Queue<WorkItem>();
|
| +
|
| + Future<Map<String, Version>> solve(Package root) {
|
| + // Kick off the work by adding the root package at its concrete version to
|
| + // the dependency graph.
|
| + _pubspecs.cache(root);
|
| + enqueue(new ChangeConstraint('(entrypoint)', root.name, root.version));
|
| +
|
| + Future processNextWorkItem(_) {
|
| + while (true) {
|
| + // Stop if we are done.
|
| + if (_work.isEmpty()) return new Future.immediate(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.chain(processNextWorkItem);
|
| + }
|
| + }
|
| + }
|
| +
|
| + return processNextWorkItem(null);
|
| + }
|
| +
|
| + void enqueue(WorkItem work) {
|
| + _work.add(work);
|
| + }
|
| +
|
| + Dependency getDependency(String package) {
|
| + // There can be unused dependencies in the graph, so just create an empty
|
| + // one if needed.
|
| + _packages.putIfAbsent(package, () => new Dependency());
|
| + return _packages[package];
|
| + }
|
| +
|
| + /**
|
| + * Sets the best selected version of [package] to [version].
|
| + */
|
| + void setVersion(String package, Version version) {
|
| + _packages[package].version = version;
|
| + }
|
| +
|
| + Map<String, Version> buildResults() {
|
| + var results = <Version>{};
|
| + _packages.forEach((name, dependency) {
|
| + if (dependency.isDependedOn) {
|
| + results[name] = dependency.version;
|
| + }
|
| + });
|
| +
|
| + return results;
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * 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.
|
| +*/
|
| +interface 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(VersionSolver solver);
|
| +}
|
| +
|
| +/**
|
| + * The best selected version for [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 package whose version is changing.
|
| + */
|
| + final String package;
|
| +
|
| + /**
|
| + * The new selected version.
|
| + */
|
| + final Version version;
|
| +
|
| + ChangeVersion(this.package, this.version);
|
| +
|
| + Future process(VersionSolver solver) {
|
| + var oldVersion = solver.getDependency(package).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 Futures.wait([
|
| + getDependencyRefs(solver, oldVersion),
|
| + getDependencyRefs(solver, version)]).transform((list) {
|
| + var oldDependencies = list[0];
|
| + var newDependencies = list[1];
|
| +
|
| + for (var dependency in oldDependencies.getValues()) {
|
| + var constraint;
|
| + if (newDependencies.containsKey(dependency.name)) {
|
| + // The dependency is in both versions of this package, but its
|
| + // constraint may have changed.
|
| + constraint = newDependencies.remove(dependency.name).constraint;
|
| + } else {
|
| + // The dependency is not in the new version of the package, so just
|
| + // remove its constraint.
|
| + constraint = null;
|
| + }
|
| +
|
| + solver.enqueue(new ChangeConstraint(
|
| + package, dependency.name, constraint));
|
| + }
|
| +
|
| + // Everything that's left is a depdendency that's only in the new
|
| + // version of the package.
|
| + for (var dependency in newDependencies.getValues()) {
|
| + solver.enqueue(new ChangeConstraint(
|
| + package, dependency.name, dependency.constraint));
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Get the dependencies that [package] has at [version].
|
| + */
|
| + 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>>.immediate(<PackageRef>{});
|
| + }
|
| +
|
| + return solver._pubspecs.load(package, version).transform((pubspec) {
|
| + var dependencies = <PackageRef>{};
|
| + for (var dependency in pubspec.dependencies) {
|
| + dependencies[dependency.name] = dependency;
|
| + }
|
| + return dependencies;
|
| + });
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * The [VersionConstraint] that [depender] places on [dependent] has changed.
|
| + */
|
| +class ChangeConstraint implements WorkItem {
|
| + /**
|
| + * The package that has the dependency.
|
| + */
|
| + final String depender;
|
| +
|
| + /**
|
| + * The package being depended on.
|
| + */
|
| + final String dependent;
|
| +
|
| + /**
|
| + * The constraint that [depender] places on [dependent]'s version.
|
| + */
|
| + final VersionConstraint constraint;
|
| +
|
| + ChangeConstraint(this.depender, this.dependent, this.constraint);
|
| +
|
| + Future process(VersionSolver solver) {
|
| + var dependency = solver.getDependency(dependent);
|
| + var oldConstraint = dependency.constraint;
|
| + dependency.placeConstraint(depender, constraint);
|
| + var newConstraint = dependency.constraint;
|
| +
|
| + // If the package is over-constrained, i.e. the packages depending have
|
| + // disjoint constraints, then stop.
|
| + if (newConstraint != null && newConstraint.isEmpty) {
|
| + throw new DisjointConstraintException(dependent);
|
| + }
|
| +
|
| + // 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 (!dependency.isDependedOn) {
|
| + solver.enqueue(new ChangeVersion(dependent, null));
|
| + return null;
|
| + }
|
| +
|
| + // The constraint has changed, so see what the best version of the package
|
| + // that meets the new constraint is.
|
| + // TODO(rnystrom): Should this always be the default source?
|
| + var source = solver._sources.defaultSource;
|
| + return source.getVersions(dependent).transform((versions) {
|
| + var best = null;
|
| + for (var version in versions) {
|
| + if (newConstraint.allows(version)) {
|
| + if (best == null || version > best) best = version;
|
| + }
|
| + }
|
| +
|
| + // TODO(rnystrom): Better exception.
|
| + if (best == null) throw new NoVersionException(dependent, newConstraint);
|
| +
|
| + if (dependency.version != best) {
|
| + solver.enqueue(new ChangeVersion(dependent, best));
|
| + }
|
| + });
|
| + }
|
| +}
|
| +
|
| +// TODO(rnystrom): Instead of always pulling from the source (which will mean
|
| +// hitting a server), we should consider caching pubspecs of uninstalled
|
| +// packages in the system cache.
|
| +/**
|
| + * Maintains a cache of previously-loaded pubspecs. Used to avoid requesting
|
| + * the same pubspec from the server repeatedly.
|
| + */
|
| +class PubspecCache {
|
| + final SourceRegistry _sources;
|
| + final Map<String, Map<Version, Pubspec>> _pubspecs;
|
| +
|
| + PubspecCache(this._sources)
|
| + : _pubspecs = <Map<Version, Pubspec>>{};
|
| +
|
| + /**
|
| + * Adds the already loaded [package] to the cache.
|
| + */
|
| + void cache(Package package) {
|
| + _pubspecs.putIfAbsent(package.name, () => new Map<Version, Pubspec>());
|
| + _pubspecs[package.name][package.version] = package.pubspec;
|
| + }
|
| +
|
| + /**
|
| + * Loads the pubspec for [package] at [version].
|
| + */
|
| + Future<Pubspec> load(String package, Version version) {
|
| + // Complete immediately if it's already cached.
|
| + if (_pubspecs.containsKey(package) &&
|
| + _pubspecs[package].containsKey(version)) {
|
| + return new Future<Pubspec>.immediate(_pubspecs[package][version]);
|
| + }
|
| +
|
| + // TODO(rnystrom): Should this always be the default source?
|
| + var source = _sources.defaultSource;
|
| + return source.describe(package, version).transform((pubspec) {
|
| + // Cache it.
|
| + _pubspecs.putIfAbsent(package, () => new Map<Version, Pubspec>());
|
| + _pubspecs[package][version] = pubspec;
|
| +
|
| + return pubspec;
|
| + });
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Describes one [Package] in the [DependencyGraph] and keeps track of which
|
| + * packages depend on it and what [VersionConstraint]s they place on it.
|
| + */
|
| +class Dependency {
|
| + /**
|
| + * The currently selected best version for this dependency.
|
| + */
|
| + Version version;
|
| +
|
| + /**
|
| + * The constraints that depending packages have placed on this one.
|
| + */
|
| + final Map<String, VersionConstraint> _constraints;
|
| +
|
| + /**
|
| + * 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() => !_constraints.isEmpty();
|
| +
|
| + /**
|
| + * 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 (_constraints.isEmpty()) return null;
|
| + return new VersionConstraint.intersect(_constraints.getValues());
|
| + }
|
| +
|
| + Dependency()
|
| + : _constraints = <VersionConstraint>{};
|
| +
|
| + /**
|
| + * Places [constraint] from [package] onto this.
|
| + */
|
| + void placeConstraint(String package, VersionConstraint constraint) {
|
| + if (constraint == null) {
|
| + _constraints.remove(package);
|
| + } else {
|
| + _constraints[package] = constraint;
|
| + }
|
| + }
|
| +}
|
| +
|
| +// TODO(rnystrom): Report the last of depending packages and their constraints.
|
| +/**
|
| + * Exception thrown when the [VersionConstraint] used to match a package is
|
| + * valid (i.e. non-empty), but there are no released versions of the package
|
| + * that fit that constraint.
|
| + */
|
| +class NoVersionException implements Exception {
|
| + final String package;
|
| + final VersionConstraint constraint;
|
| +
|
| + NoVersionException(this.package, this.constraint);
|
| +
|
| + String toString() =>
|
| + "Package '$package' has no versions that match $constraint.";
|
| +}
|
| +
|
| +// TODO(rnystrom): Report the last of depending packages and their constraints.
|
| +/**
|
| + * Exception thrown when the [VersionConstraint] used to match a package is
|
| + * the empty set: in other words, multiple packages depend on it and have
|
| + * conflicting constraints that have no overlap.
|
| + */
|
| +class DisjointConstraintException implements Exception {
|
| + final String package;
|
| +
|
| + DisjointConstraintException(this.package);
|
| +
|
| + String toString() =>
|
| + "Package '$package' has disjoint constraints.";
|
| +}
|
| +
|
| +/**
|
| + * Exception thrown when the [VersionSolver] fails to find a solution after a
|
| + * certain number of iterations.
|
| + */
|
| +class CouldNotSolveException implements Exception {
|
| + CouldNotSolveException();
|
| +
|
| + String toString() =>
|
| + "Could not find a solution that met all version constraints.";
|
| +}
|
|
|