9b23dff467
This reverts commit r17907. TBR. Review URL: https://codereview.chromium.org//12090093 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@17918 260f80e4-7a28-3924-810f-c04153c831b5
707 lines
25 KiB
Dart
707 lines
25 KiB
Dart
// 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 'dart:async';
|
|
import 'dart:collection' show Queue;
|
|
import 'dart:json' as json;
|
|
import 'dart:math';
|
|
import 'lock_file.dart';
|
|
import 'log.dart' as log;
|
|
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 and the requirements imposed by
|
|
/// [lockFile]. 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<List<PackageId>> resolveVersions(SourceRegistry sources, Package root,
|
|
LockFile lockFile) {
|
|
log.message('Resolving dependencies...');
|
|
return new VersionSolver(sources, root, lockFile).solve();
|
|
}
|
|
|
|
class VersionSolver {
|
|
final SourceRegistry _sources;
|
|
final Package _root;
|
|
final LockFile lockFile;
|
|
final PubspecCache _pubspecs;
|
|
final Map<String, Dependency> _packages;
|
|
final Queue<WorkItem> _work;
|
|
int _numIterations = 0;
|
|
|
|
VersionSolver(SourceRegistry sources, this._root, this.lockFile)
|
|
: _sources = sources,
|
|
_pubspecs = new PubspecCache(sources),
|
|
_packages = <String, Dependency>{},
|
|
_work = new Queue<WorkItem>();
|
|
|
|
/// Tell the version solver to use the most recent version of [package] that
|
|
/// exists in whatever source it's installed from. If that version violates
|
|
/// constraints imposed by other dependencies, an error will be raised when
|
|
/// solving the versions, even if an earlier compatible version exists.
|
|
void useLatestVersion(String package) {
|
|
// TODO(nweiz): How do we want to detect and handle unknown dependencies
|
|
// here?
|
|
getDependency(package).useLatestVersion = true;
|
|
lockFile.packages.remove(package);
|
|
}
|
|
|
|
Future<List<PackageId>> solve() {
|
|
// Kick off the work by adding the root package at its concrete version to
|
|
// the dependency graph.
|
|
var ref = new PackageRef.root(_root);
|
|
enqueue(new AddConstraint('(entrypoint)', ref));
|
|
_pubspecs.cache(ref.atVersion(_root.version), _root.pubspec);
|
|
|
|
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 > 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);
|
|
}
|
|
|
|
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(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(Dependency dependency) {
|
|
return dependency.getVersions().then((versions) {
|
|
var best = null;
|
|
for (var version in versions) {
|
|
if (dependency.useLatestVersion ||
|
|
dependency.constraint.allows(version)) {
|
|
if (best == null || version > best) best = version;
|
|
}
|
|
}
|
|
|
|
// TODO(rnystrom): Better exception.
|
|
if (best == null) {
|
|
if (tryUnlockDepender(dependency)) return null;
|
|
throw new NoVersionException(dependency.name, dependency.constraint,
|
|
dependency._refs);
|
|
} 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(Dependency 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) {
|
|
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((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);
|
|
})
|
|
.toList();
|
|
}
|
|
}
|
|
|
|
/// 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(VersionSolver 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(VersionSolver 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>>.immediate(<String, PackageRef>{});
|
|
}
|
|
|
|
var id = new PackageId(package, source, version, description);
|
|
return solver._pubspecs.load(id).then((pubspec) {
|
|
var dependencies = <String, PackageRef>{};
|
|
for (var dependency in pubspec.dependencies) {
|
|
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(VersionSolver solver);
|
|
|
|
void undo(VersionSolver solver);
|
|
|
|
Future _processChange(VersionSolver solver, Dependency oldDependency,
|
|
Dependency 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._refs);
|
|
}
|
|
|
|
// 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) {
|
|
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(VersionSolver 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(VersionSolver 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(VersionSolver 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(VersionSolver 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.
|
|
Dependency package;
|
|
|
|
UnlockPackage(this.package);
|
|
|
|
Future process(VersionSolver 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));
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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<PackageId, Pubspec> _pubspecs;
|
|
|
|
PubspecCache(this._sources)
|
|
: _pubspecs = new Map<PackageId, Pubspec>();
|
|
|
|
/// Caches [pubspec] as the [Pubspec] for the package identified by [id].
|
|
void cache(PackageId id, Pubspec pubspec) {
|
|
_pubspecs[id] = pubspec;
|
|
}
|
|
|
|
/// Loads the pubspec for the package identified by [id].
|
|
Future<Pubspec> load(PackageId id) {
|
|
// Complete immediately if it's already cached.
|
|
if (_pubspecs.containsKey(id)) {
|
|
return new Future<Pubspec>.immediate(_pubspecs[id]);
|
|
}
|
|
|
|
return id.describe().then((pubspec) {
|
|
// Cache it.
|
|
_pubspecs[id] = pubspec;
|
|
return pubspec;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Describes one [Package] in the [DependencyGraph] and keeps track of which
|
|
/// packages depend on it and what constraints they place on it.
|
|
class Dependency {
|
|
/// 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;
|
|
}
|
|
|
|
Dependency(this.name)
|
|
: _refs = <String, PackageRef>{};
|
|
|
|
Dependency._clone(Dependency other)
|
|
: name = other.name,
|
|
version = other.version,
|
|
_refs = new Map<String, PackageRef>.from(other._refs);
|
|
|
|
/// Creates a copy of this dependency.
|
|
Dependency clone() => new Dependency._clone(this);
|
|
|
|
/// Return a list of available versions for this dependency.
|
|
Future<List<Version>> getVersions() => source.getVersions(name, description);
|
|
|
|
/// 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,
|
|
requiredDepender, required.source, package, ref.source);
|
|
} else if (!required.source.descriptionsEqual(
|
|
required.description, ref.description)) {
|
|
throw new DescriptionMismatchException(name,
|
|
requiredDepender, required.description, package, ref.description);
|
|
}
|
|
}
|
|
|
|
_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);
|
|
}
|
|
|
|
/// 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;
|
|
final Map<String, PackageRef> _dependencies;
|
|
|
|
NoVersionException(this.package, this.constraint, this._dependencies);
|
|
|
|
String toString() {
|
|
var buffer = new StringBuffer();
|
|
buffer.add("Package '$package' has no versions that match $constraint "
|
|
"derived from:\n");
|
|
|
|
var keys = new List.from(_dependencies.keys);
|
|
keys.sort();
|
|
|
|
for (var key in keys) {
|
|
buffer.add("- '$key' depends on version "
|
|
"${_dependencies[key].constraint}\n");
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
// TODO(rnystrom): Report the list of depending packages and their constraints.
|
|
/// Exception thrown when the most recent version of [package] must be selected,
|
|
/// but doesn't match the [VersionConstraint] imposed on the package.
|
|
class CouldNotUpdateException implements Exception {
|
|
final String package;
|
|
final VersionConstraint constraint;
|
|
final Version best;
|
|
|
|
CouldNotUpdateException(this.package, this.constraint, this.best);
|
|
|
|
String toString() =>
|
|
"The latest version of '$package', $best, does not match $constraint.";
|
|
}
|
|
|
|
/// 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;
|
|
final Map<String, PackageRef> _dependencies;
|
|
|
|
DisjointConstraintException(this.package, this._dependencies);
|
|
|
|
String toString() {
|
|
var buffer = new StringBuffer();
|
|
buffer.add("Incompatible version constraints on '$package':\n");
|
|
|
|
var keys = new List.from(_dependencies.keys);
|
|
keys.sort();
|
|
|
|
for (var key in keys) {
|
|
buffer.add("- '$key' depends on version "
|
|
"${_dependencies[key].constraint}\n");
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
/// 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.";
|
|
}
|
|
|
|
/// Exception thrown when two packages with the same name but different sources
|
|
/// are depended upon.
|
|
class SourceMismatchException implements Exception {
|
|
final String package;
|
|
final String depender1;
|
|
final Source source1;
|
|
final String depender2;
|
|
final Source source2;
|
|
|
|
SourceMismatchException(this.package, this.depender1, this.source1,
|
|
this.depender2, this.source2);
|
|
|
|
String toString() {
|
|
return "Incompatible dependencies on '$package':\n"
|
|
"- '$depender1' depends on it from source '$source1'\n"
|
|
"- '$depender2' depends on it from source '$source2'";
|
|
}
|
|
}
|
|
|
|
/// 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 String depender1;
|
|
final description1;
|
|
final String depender2;
|
|
final description2;
|
|
|
|
DescriptionMismatchException(this.package, this.depender1, this.description1,
|
|
this.depender2, this.description2);
|
|
|
|
String toString() {
|
|
// TODO(nweiz): Dump descriptions to YAML when that's supported.
|
|
return "Incompatible dependencies on '$package':\n"
|
|
"- '$depender1' depends on it with description "
|
|
"${json.stringify(description1)}\n"
|
|
"- '$depender2' depends on it with description "
|
|
"${json.stringify(description2)}";
|
|
}
|
|
}
|