blob: e850bb50550f71811abe010884ab180b2b398233 [file] [log] [blame]
library pub.solver.backtracking_solver;
import 'dart:async';
import 'dart:collection' show Queue;
import '../barback.dart' as barback;
import '../exceptions.dart';
import '../lock_file.dart';
import '../log.dart' as log;
import '../package.dart';
import '../pubspec.dart';
import '../sdk.dart' as sdk;
import '../source_registry.dart';
import '../source/unknown.dart';
import '../utils.dart';
import '../version.dart';
import 'dependency_queue.dart';
import 'version_queue.dart';
import 'version_solver.dart';
class BacktrackingSolver {
final SolveType type;
final SourceRegistry sources;
final Package root;
final LockFile lockFile;
final PubspecCache cache;
final _forceLatest = new Set<String>();
final _overrides = new Map<String, PackageDep>();
final _selected = <VersionQueue>[];
int get attemptedSolutions => _attemptedSolutions;
var _attemptedSolutions = 1;
BacktrackingSolver(SolveType type, SourceRegistry sources, this.root,
this.lockFile, List<String> useLatest)
: type = type,
sources = sources,
cache = new PubspecCache(type, sources) {
for (var package in useLatest) {
_forceLatest.add(package);
}
for (var override in root.dependencyOverrides) {
_overrides[override.name] = override;
}
}
Future<SolveResult> solve() {
var stopwatch = new Stopwatch();
_logParameters();
var overrides = _overrides.values.toList();
overrides.sort((a, b) => a.name.compareTo(b.name));
return newFuture(() {
stopwatch.start();
cache.cache(new PackageId.root(root), root.pubspec);
_validateSdkConstraint(root.pubspec);
return _traverseSolution();
}).then((packages) {
var pubspecs = new Map.fromIterable(
packages,
key: (id) => id.name,
value: (id) => cache.getCachedPubspec(id));
return new SolveResult.success(
sources,
root,
lockFile,
packages,
overrides,
pubspecs,
_getAvailableVersions(packages),
attemptedSolutions);
}).catchError((error) {
if (error is! SolveFailure) throw error;
return new SolveResult.failure(
sources,
root,
lockFile,
overrides,
error,
attemptedSolutions);
}).whenComplete(() {
var buffer = new StringBuffer();
buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.');
buffer.writeln(cache.describeResults());
log.solver(buffer);
});
}
Map<String, List<Version>> _getAvailableVersions(List<PackageId> packages) {
var availableVersions = new Map<String, List<Version>>();
for (var package in packages) {
var cached = cache.getCachedVersions(package.toRef());
var versions;
if (cached != null) {
versions = cached.map((id) => id.version).toList();
} else {
versions = [package.version];
}
availableVersions[package.name] = versions;
}
return availableVersions;
}
PackageId select(VersionQueue versions) {
_selected.add(versions);
logSolve();
return versions.current;
}
PackageId getSelected(String name) {
if (root.name == name) return new PackageId.root(root);
for (var i = _selected.length - 1; i >= 0; i--) {
if (_selected[i].current.name == name) return _selected[i].current;
}
return null;
}
PackageId getLocked(String package) {
if (type == SolveType.GET) return lockFile.packages[package];
if (type == SolveType.DOWNGRADE) {
var locked = lockFile.packages[package];
if (locked != null && !sources[locked.source].hasMultipleVersions) {
return locked;
}
}
if (_forceLatest.isEmpty || _forceLatest.contains(package)) return null;
return lockFile.packages[package];
}
Future<List<PackageId>> _traverseSolution() => resetStack(() {
return new Traverser(this).traverse().catchError((error) {
if (error is! SolveFailure) throw error;
return _backtrack(error).then((canTry) {
if (canTry) {
_attemptedSolutions++;
return _traverseSolution();
}
throw error;
});
});
});
Future<bool> _backtrack(SolveFailure failure) {
if (_selected.isEmpty) return new Future.value(false);
var dependers = _getTransitiveDependers(failure.package);
for (var selected in _selected) {
if (dependers.contains(selected.current.name)) {
selected.fail();
}
}
advanceVersion() {
_backjump(failure);
var previous = _selected.last.current;
return _selected.last.advance().then((success) {
if (success) {
logSolve();
return true;
}
logSolve('$previous is last version, backtracking');
_selected.removeLast();
if (_selected.isEmpty) return false;
return advanceVersion();
});
}
return advanceVersion();
}
void _backjump(SolveFailure failure) {
for (var i = _selected.length - 1; i >= 0; i--) {
var selected = _selected[i].current;
if (failure is DisjointConstraintException &&
selected.name == failure.package) {
logSolve("skipping past disjoint selected ${selected.name}");
continue;
}
if (_selected[i].hasFailed) {
logSolve('backjump to ${selected.name}');
_selected.removeRange(i + 1, _selected.length);
return;
}
}
_selected.removeRange(1, _selected.length);
}
Set<String> _getTransitiveDependers(String dependency) {
var dependers = new Map<String, Set<String>>();
addDependencies(name, deps) {
dependers.putIfAbsent(name, () => new Set<String>());
for (var dep in deps) {
dependers.putIfAbsent(dep.name, () => new Set<String>()).add(name);
}
}
for (var i = 0; i < _selected.length; i++) {
var id = _selected[i].current;
var pubspec = cache.getCachedPubspec(id);
if (pubspec != null) addDependencies(id.name, pubspec.dependencies);
}
addDependencies(root.name, root.immediateDependencies);
var visited = new Set<String>();
walk(String package) {
if (visited.contains(package)) return;
visited.add(package);
var depender = dependers[package].forEach(walk);
}
walk(dependency);
return visited;
}
void _logParameters() {
var buffer = new StringBuffer();
buffer.writeln("Solving dependencies:");
for (var package in root.dependencies) {
buffer.write("- $package");
var locked = getLocked(package.name);
if (_forceLatest.contains(package.name)) {
buffer.write(" (use latest)");
} else if (locked != null) {
var version = locked.version;
buffer.write(" (locked to $version)");
}
buffer.writeln();
}
log.solver(buffer.toString().trim());
}
void logSolve([String message]) {
if (message == null) {
if (_selected.isEmpty) {
message = "* start at root";
} else {
message = "* select ${_selected.last.current}";
}
} else {
message = prefixLines(message);
}
var prefix = _selected.skip(1).map((_) => '| ').join();
log.solver(prefixLines(message, prefix: prefix));
}
}
class Traverser {
final BacktrackingSolver _solver;
final _packages = new Queue<PackageId>();
final _visited = new Set<PackageId>();
final _dependencies = <String, List<Dependency>>{};
Traverser(this._solver);
Future<List<PackageId>> traverse() {
_packages.add(new PackageId.root(_solver.root));
return _traversePackage();
}
Future<List<PackageId>> _traversePackage() {
if (_packages.isEmpty) {
return new Future<List<PackageId>>.value(_visited.toList());
}
var id = _packages.removeFirst();
if (_visited.contains(id)) {
return _traversePackage();
}
_visited.add(id);
return _solver.cache.getPubspec(id).then((pubspec) {
_validateSdkConstraint(pubspec);
var deps = pubspec.dependencies.toSet();
if (id.isRoot) {
deps.addAll(pubspec.devDependencies);
deps.addAll(_solver._overrides.values);
}
deps = deps.map((dep) {
var override = _solver._overrides[dep.name];
if (override != null) return override;
return dep;
}).toSet();
for (var dep in deps) {
if (!dep.isRoot && _solver.sources[dep.source] is UnknownSource) {
throw new UnknownSourceException(
id.name,
[new Dependency(id.name, id.version, dep)]);
}
}
return _traverseDeps(id, new DependencyQueue(_solver, deps));
}).catchError((error) {
if (error is! PackageNotFoundException) throw error;
throw new NoVersionException(id.name, null, id.version, []);
});
}
Future<List<PackageId>> _traverseDeps(PackageId depender,
DependencyQueue deps) {
if (deps.isEmpty) return _traversePackage();
return resetStack(() {
return deps.advance().then((dep) {
var dependency = new Dependency(depender.name, depender.version, dep);
return _registerDependency(dependency).then((_) {
if (dep.name == "barback") return _addImplicitDependencies();
});
}).then((_) => _traverseDeps(depender, deps));
});
}
Future _registerDependency(Dependency dependency) {
return new Future.sync(() {
_validateDependency(dependency);
var dep = dependency.dep;
var dependencies = _getDependencies(dep.name);
dependencies.add(dependency);
var constraint = _getConstraint(dep.name);
if (constraint.isEmpty) {
var constraints = dependencies.map(
(dep) => " ${dep.dep.constraint} from ${dep.depender}").join('\n');
_solver.logSolve('disjoint constraints on ${dep.name}:\n$constraints');
throw new DisjointConstraintException(dep.name, dependencies);
}
var selected = _validateSelected(dep, constraint);
if (selected != null) {
_packages.add(selected);
return null;
}
var locked = _getValidLocked(dep.name);
return VersionQueue.create(locked, () {
return _getAllowedVersions(dep);
}).then((versions) => _packages.add(_solver.select(versions)));
});
}
Future<Iterable<PackageId>> _getAllowedVersions(PackageDep dep) {
var constraint = _getConstraint(dep.name);
return _solver.cache.getVersions(dep.toRef()).then((versions) {
var allowed = versions.where((id) => constraint.allows(id.version));
if (allowed.isEmpty) {
_solver.logSolve('no versions for ${dep.name} match $constraint');
throw new NoVersionException(
dep.name,
null,
constraint,
_getDependencies(dep.name));
}
if (_solver._forceLatest.contains(dep.name)) allowed = [allowed.first];
var locked = _getValidLocked(dep.name);
if (locked != null) {
allowed = allowed.where((dep) => dep.version != locked.version);
}
return allowed;
}).catchError((error, stackTrace) {
if (error is PackageNotFoundException) {
throw new DependencyNotFoundException(
dep.name,
error,
_getDependencies(dep.name));
}
throw error;
});
}
void _validateDependency(Dependency dependency) {
var dep = dependency.dep;
var required = _getRequired(dep.name);
if (required == null) return;
if (required.dep.source != dep.source) {
_solver.logSolve(
'source mismatch on ${dep.name}: ${required.dep.source} ' '!= ${dep.source}');
throw new SourceMismatchException(dep.name, [required, dependency]);
}
var source = _solver.sources[dep.source];
if (!source.descriptionsEqual(dep.description, required.dep.description)) {
_solver.logSolve(
'description mismatch on ${dep.name}: '
'${required.dep.description} != ${dep.description}');
throw new DescriptionMismatchException(dep.name, [required, dependency]);
}
}
PackageId _validateSelected(PackageDep dep, VersionConstraint constraint) {
var selected = _solver.getSelected(dep.name);
if (selected == null) return null;
if (!dep.constraint.allows(selected.version)) {
_solver.logSolve('selection $selected does not match $constraint');
throw new NoVersionException(
dep.name,
selected.version,
constraint,
_getDependencies(dep.name));
}
return selected;
}
Future _addImplicitDependencies() {
if (_getDependencies("barback").length != 1) return new Future.value();
return Future.wait(barback.pubConstraints.keys.map((depName) {
var constraint = barback.pubConstraints[depName];
_solver.logSolve(
'add implicit $constraint pub dependency on ' '$depName');
var override = _solver._overrides[depName];
var pubDep = override == null ?
new PackageDep(depName, "hosted", constraint, depName) :
override.withConstraint(constraint);
return _registerDependency(
new Dependency("pub itself", Version.none, pubDep));
}));
}
List<Dependency> _getDependencies(String name) {
return _dependencies.putIfAbsent(name, () => <Dependency>[]);
}
Dependency _getRequired(String name) {
return _getDependencies(
name).firstWhere((dep) => !dep.dep.isRoot, orElse: () => null);
}
VersionConstraint _getConstraint(String name) {
var constraint = _getDependencies(
name).map(
(dep) =>
dep.dep.constraint).fold(VersionConstraint.any, (a, b) => a.intersect(b));
return constraint;
}
PackageId _getValidLocked(String name) {
var package = _solver.getLocked(name);
if (package == null) return null;
var constraint = _getConstraint(name);
if (!constraint.allows(package.version)) {
_solver.logSolve('$package is locked but does not match $constraint');
return null;
} else {
_solver.logSolve('$package is locked');
}
var required = _getRequired(name);
if (required != null) {
if (package.source != required.dep.source) return null;
var source = _solver.sources[package.source];
if (!source.descriptionsEqual(
package.description,
required.dep.description)) return null;
}
return package;
}
}
void _validateSdkConstraint(Pubspec pubspec) {
if (pubspec.environment.sdkVersion.allows(sdk.version)) return;
throw new BadSdkVersionException(
pubspec.name,
'Package ${pubspec.name} requires SDK version '
'${pubspec.environment.sdkVersion} but the current SDK is ' '${sdk.version}.');
}