Chromium Code Reviews| Index: utils/pub/version_solver.dart | 
| diff --git a/utils/pub/version_solver.dart b/utils/pub/version_solver.dart | 
| index 4550dde8f22e73cdb76a210c47e1a9dd00e44d74..28cb6454780ade0e7cc9ea1dcd48a668dde92f2c 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.withVersion(_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; | 
| 
 
Bob Nystrom
2012/06/29 17:24:40
Can we replace these three fields with just a Pack
 
nweiz
2012/06/29 18:45:08
It's invalid for a PackageId to have a null versio
 
 | 
| + | 
| + /** | 
| + * 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 of [id]. | 
| 
 
Bob Nystrom
2012/06/29 17:24:40
Comment doesn't line up with parameters.
 
nweiz
2012/06/29 18:45:08
Done.
 
 | 
| */ | 
| 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)}'"; | 
| +} |