| Index: utils/pub/version_solver.dart
|
| diff --git a/utils/pub/version_solver.dart b/utils/pub/version_solver.dart
|
| index 4550dde8f22e73cdb76a210c47e1a9dd00e44d74..3bee2056ce51cfdadc2e711f233337292c166335 100644
|
| --- a/utils/pub/version_solver.dart
|
| +++ b/utils/pub/version_solver.dart
|
| @@ -37,8 +37,10 @@
|
| */
|
| #library('version_solver');
|
|
|
| +#import('dart:json');
|
| #import('package.dart');
|
| #import('pubspec.dart');
|
| +#import('root_source.dart');
|
| #import('source.dart');
|
| #import('source_registry.dart');
|
| #import('utils.dart');
|
| @@ -72,11 +74,12 @@ class VersionSolver {
|
| _packages = <Dependency>{},
|
| _work = new Queue<WorkItem>();
|
|
|
| - Future<Map<String, Version>> solve() {
|
| + Future<List<PackageId>> solve() {
|
| // 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));
|
| + var ref = new PackageRef(new RootSource(_root), _root.version, _root.name);
|
| + enqueue(new AddConstraint('(entrypoint)', ref));
|
| + _pubspecs.cache(ref.atVersion(_root.version), _root.pubspec);
|
|
|
| Future processNextWorkItem(_) {
|
| while (true) {
|
| @@ -115,7 +118,7 @@ class VersionSolver {
|
| 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());
|
| + _packages.putIfAbsent(package, () => new Dependency(package));
|
| return _packages[package];
|
| }
|
|
|
| @@ -154,25 +157,38 @@ interface WorkItem {
|
| }
|
|
|
| /**
|
| - * 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.
|
| + * 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 package whose version is changing.
|
| + * The source of the package whose version is changing.
|
| */
|
| - final String package;
|
| + 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.version);
|
| + /**
|
| + * The name of the package whose version is changing.
|
| + */
|
| + String get package() => source.packageName(description);
|
| +
|
| + ChangeVersion(this.source, this.description, this.version) {
|
| + if (source == null) throw "null source";
|
| + }
|
|
|
| Future process(VersionSolver solver) {
|
| - var oldVersion = solver.getDependency(package).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
|
| @@ -180,36 +196,32 @@ class ChangeVersion implements WorkItem {
|
| return Futures.wait([
|
| getDependencyRefs(solver, oldVersion),
|
| getDependencyRefs(solver, version)]).transform((list) {
|
| - var oldDependencies = list[0];
|
| - var newDependencies = list[1];
|
| + var oldDependencyRefs = list[0];
|
| + var newDependencyRefs = list[1];
|
|
|
| - for (var dependency in oldDependencies.getValues()) {
|
| - var constraint;
|
| - if (newDependencies.containsKey(dependency.name)) {
|
| + for (var oldRef in oldDependencyRefs.getValues()) {
|
| + if (newDependencyRefs.containsKey(oldRef.name)) {
|
| // The dependency is in both versions of this package, but its
|
| // constraint may have changed.
|
| - constraint = newDependencies.remove(dependency.name).constraint;
|
| + 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.
|
| - constraint = null;
|
| + solver.enqueue(new RemoveConstraint(package, oldRef.name));
|
| }
|
| -
|
| - 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));
|
| + for (var newRef in newDependencyRefs.getValues()) {
|
| + solver.enqueue(new AddConstraint(package, newRef));
|
| }
|
| });
|
| }
|
|
|
| /**
|
| - * Get the dependencies that [package] has at [version].
|
| + * Get the dependencies at [version] of the package being changed.
|
| */
|
| Future<Map<String, PackageRef>> getDependencyRefs(VersionSolver solver,
|
| Version version) {
|
| @@ -218,7 +230,8 @@ class ChangeVersion implements WorkItem {
|
| return new Future<Map<String, PackageRef>>.immediate(<PackageRef>{});
|
| }
|
|
|
| - return solver._pubspecs.load(package, version).transform((pubspec) {
|
| + var id = new PackageId(source, version, description);
|
| + return solver._pubspecs.load(id).transform((pubspec) {
|
| var dependencies = <PackageRef>{};
|
| for (var dependency in pubspec.dependencies) {
|
| dependencies[dependency.name] = dependency;
|
| @@ -229,36 +242,25 @@ class ChangeVersion implements WorkItem {
|
| }
|
|
|
| /**
|
| - * The [VersionConstraint] that [depender] places on [dependent] has changed.
|
| + * 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.
|
| */
|
| 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;
|
| + abstract Future process(VersionSolver solver);
|
|
|
| - 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;
|
| + Future _processChange(VersionSolver solver, Source source, description,
|
| + Dependency dependency, VersionConstraint oldConstraint,
|
| + VersionConstraint newConstraint) {
|
| + var name = dependency.name;
|
|
|
| // 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);
|
| + throw new DisjointConstraintException(name);
|
| }
|
|
|
| // If this constraint change didn't cause the overall constraint on the
|
| @@ -267,22 +269,21 @@ class ChangeConstraint implements WorkItem {
|
|
|
| // If the dependency has been cut free from the graph, just remove it.
|
| if (!dependency.isDependedOn) {
|
| - solver.enqueue(new ChangeVersion(dependent, null));
|
| + solver.enqueue(new ChangeVersion(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 (dependent == solver._root.name) {
|
| - solver.enqueue(new ChangeVersion(dependent, solver._root.version));
|
| + if (name == solver._root.name) {
|
| + solver.enqueue(new ChangeVersion(
|
| + source, description, solver._root.version));
|
| 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) {
|
| + return source.getVersions(description).transform((versions) {
|
| var best = null;
|
| for (var version in versions) {
|
| if (newConstraint.allows(version)) {
|
| @@ -291,15 +292,71 @@ class ChangeConstraint implements WorkItem {
|
| }
|
|
|
| // TODO(rnystrom): Better exception.
|
| - if (best == null) throw new NoVersionException(dependent, newConstraint);
|
| + if (best == null) throw new NoVersionException(name, newConstraint);
|
|
|
| if (dependency.version != best) {
|
| - solver.enqueue(new ChangeVersion(dependent, best));
|
| + solver.enqueue(new ChangeVersion(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(VersionSolver solver) {
|
| + var dependency = solver.getDependency(ref.name);
|
| + var oldConstraint = dependency.constraint;
|
| + dependency.placeConstraint(depender, ref);
|
| + var newConstraint = dependency.constraint;
|
| + return _processChange(solver, ref.source, ref.description, dependency,
|
| + oldConstraint, newConstraint);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * [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;
|
| +
|
| + RemoveConstraint(this.depender, this.dependent);
|
| +
|
| + Future process(VersionSolver solver) {
|
| + var dependency = solver.getDependency(dependent);
|
| + var oldConstraint = dependency.constraint;
|
| + var source = dependency.source;
|
| + var description = dependency.description;
|
| + dependency.removeConstraint(depender);
|
| + var newConstraint = dependency.constraint;
|
| + return _processChange(solver, source, description, dependency,
|
| + oldConstraint, newConstraint);
|
| + }
|
| +}
|
| +
|
| // 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.
|
| @@ -309,36 +366,30 @@ class ChangeConstraint implements WorkItem {
|
| */
|
| class PubspecCache {
|
| final SourceRegistry _sources;
|
| - final Map<String, Map<Version, Pubspec>> _pubspecs;
|
| + final Map<PackageId, Pubspec> _pubspecs;
|
|
|
| PubspecCache(this._sources)
|
| - : _pubspecs = <Map<Version, Pubspec>>{};
|
| + : _pubspecs = new Map<PackageId, Pubspec>();
|
|
|
| /**
|
| - * Adds the already loaded [package] to the cache.
|
| + * Caches [pubspec] as the [Pubspec] for the package identified by [id].
|
| */
|
| - void cache(Package package) {
|
| - _pubspecs.putIfAbsent(package.name, () => new Map<Version, Pubspec>());
|
| - _pubspecs[package.name][package.version] = package.pubspec;
|
| + void cache(PackageId id, Pubspec pubspec) {
|
| + _pubspecs[id] = pubspec;
|
| }
|
|
|
| /**
|
| - * Loads the pubspec for [package] at [version].
|
| + * Loads the pubspec for the package identified by [id].
|
| */
|
| - Future<Pubspec> load(String package, Version version) {
|
| + Future<Pubspec> load(PackageId id) {
|
| // Complete immediately if it's already cached.
|
| - if (_pubspecs.containsKey(package) &&
|
| - _pubspecs[package].containsKey(version)) {
|
| - return new Future<Pubspec>.immediate(_pubspecs[package][version]);
|
| + if (_pubspecs.containsKey(id)) {
|
| + return new Future<Pubspec>.immediate(_pubspecs[id]);
|
| }
|
|
|
| - // TODO(rnystrom): Should this always be the default source?
|
| - var source = _sources.defaultSource;
|
| - return source.describe(package, version).transform((pubspec) {
|
| + return id.describe().transform((pubspec) {
|
| // Cache it.
|
| - _pubspecs.putIfAbsent(package, () => new Map<Version, Pubspec>());
|
| - _pubspecs[package][version] = pubspec;
|
| -
|
| + _pubspecs[id] = pubspec;
|
| return pubspec;
|
| });
|
| }
|
| @@ -346,25 +397,46 @@ class PubspecCache {
|
|
|
| /**
|
| * Describes one [Package] in the [DependencyGraph] and keeps track of which
|
| - * packages depend on it and what [VersionConstraint]s they place on it.
|
| + * packages depend on it and what constraints they place on it.
|
| */
|
| class Dependency {
|
| /**
|
| - * The currently selected best version for this dependency.
|
| + * The name of the this dependency's package.
|
| */
|
| - Version version;
|
| + final String name;
|
| +
|
| + /**
|
| + * The [PackageRefs] that represent constraints that depending packages have
|
| + * placed on this one.
|
| + */
|
| + final Map<String, PackageRef> _refs;
|
| +
|
| + /**
|
| + * The source of this dependency's package.
|
| + *
|
| + * All constraints in [_refs] must have this as their source.
|
| + */
|
| + Source source;
|
| +
|
| + /**
|
| + * The description of this dependency's package.
|
| + *
|
| + * All constraints in [_refs] must have a description equivalent to this one
|
| + * according to [source].
|
| + */
|
| + var description;
|
|
|
| /**
|
| - * The constraints that depending packages have placed on this one.
|
| + * The currently-selected best version for this dependency.
|
| */
|
| - final Map<String, VersionConstraint> _constraints;
|
| + Version version;
|
|
|
| /**
|
| * 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();
|
| + bool get isDependedOn() => !_refs.isEmpty();
|
|
|
| /**
|
| * Gets the overall constraint that all packages are placing on this one.
|
| @@ -372,21 +444,42 @@ class Dependency {
|
| * 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());
|
| + if (_refs.isEmpty()) return null;
|
| + return new VersionConstraint.intersect(
|
| + _refs.getValues().map((ref) => ref.constraint));
|
| }
|
|
|
| - Dependency()
|
| - : _constraints = <VersionConstraint>{};
|
| + Dependency(this.name)
|
| + : _refs = <PackageRef>{};
|
| +
|
| + /**
|
| + * Places [ref] as a constraint from [package] onto this.
|
| + */
|
| + void placeConstraint(String package, PackageRef ref) {
|
| + // If this isn't the first constraint placed on this package, make sure it
|
| + // matches the source and description of past constraints.
|
| + if (_refs.isEmpty()) {
|
| + source = ref.source;
|
| + description = ref.description;
|
| + } else if (source.name != ref.source.name) {
|
| + throw new SourceMismatchException(name, source, ref.source);
|
| + } else if (!source.descriptionsEqual(description, ref.description)) {
|
| + throw new DescriptionMismatchException(
|
| + name, description, ref.description);
|
| + }
|
| +
|
| + _refs[package] = ref;
|
| + }
|
|
|
| /**
|
| - * Places [constraint] from [package] onto this.
|
| + * Removes the constraint from [package] onto this.
|
| */
|
| - void placeConstraint(String package, VersionConstraint constraint) {
|
| - if (constraint == null) {
|
| - _constraints.remove(package);
|
| - } else {
|
| - _constraints[package] = constraint;
|
| + void removeConstraint(String package) {
|
| + _refs.remove(package);
|
| +
|
| + if (_refs.isEmpty()) {
|
| + source = null;
|
| + description = null;
|
| }
|
| }
|
| }
|
| @@ -432,3 +525,37 @@ class CouldNotSolveException implements Exception {
|
| String toString() =>
|
| "Could not find a solution that met all version constraints.";
|
| }
|
| +
|
| +/**
|
| + * Exception thrown when two packages with the same name but different sources
|
| + * are depended upon.
|
| + */
|
| +class SourceMismatchException implements Exception {
|
| + final String package;
|
| + final Source source1;
|
| + final Source source2;
|
| +
|
| + SourceMismatchException(this.package, this.source1, this.source2);
|
| +
|
| + String toString() {
|
| + return "Package '$package' is depended on from both sources "
|
| + "'${source1.name}' and '${source2.name}'.";
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Exception thrown when two packages with the same name and source but
|
| + * different descriptions are depended upon.
|
| + */
|
| +class DescriptionMismatchException implements Exception {
|
| + final String package;
|
| + final description1;
|
| + final description2;
|
| +
|
| + DescriptionMismatchException(this.package, this.description1,
|
| + this.description2);
|
| +
|
| + // TODO(nweiz): Dump to YAML when that's supported
|
| + String toString() => "Package '$package' has conflicting descriptions "
|
| + "'${JSON.stringify(description1)}' and '${JSON.stringify(description2)}'";
|
| +}
|
|
|