mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Refactor ConstraintSolver.Resolver
Factor out the "state" into its own class, ResolverState. The big difference from the previous state object: it actually explicitly tracks the set of potential UnitVersions for every active dependency. This essentially replaces the DependencyList class. Because we always know exactly how many options there are for a given dependency, we can both generalize and simplify the "propagate transitive exact deps" optimization. That optimization only worked on "foo@=1.2.3" dependencies, which meant it didn't apply in any other situation where there was only one possible package to choose. But there are a whole lot of other situations like that: local packages, packages that just don't have many versions, packages that already have a lot of constraints applied to them, etc. By tracking the set of potential alternatives, we can just make sure to always expand 1-alternative units first. We also maintain the aspect of the optimization where we don't need to call the cost function until we've actually gotten to a state with multiple neighbors. This keeps #2410 fixed as well. I've removed the constraintAncestor support as part of this refactoring, so some error messages may be worse than they were before. But this should set me up pretty well to improve error messages tomorrow.
This commit is contained in:
@@ -94,14 +94,6 @@ var t = function (deps, expected, options) {
|
||||
currentTest.equal(resolvedDeps, expected);
|
||||
};
|
||||
|
||||
var t_progagateExact = function (deps, expected) {
|
||||
var dependencies = splitArgs(deps).dependencies;
|
||||
var constraints = splitArgs(deps).constraints;
|
||||
|
||||
var resolvedDeps = resolver.propagateExactDeps(dependencies, constraints);
|
||||
currentTest.equal(resolvedDeps, expected);
|
||||
};
|
||||
|
||||
var FAIL = function (deps, regexp) {
|
||||
currentTest.throws(function () {
|
||||
var dependencies = splitArgs(deps).dependencies;
|
||||
@@ -111,21 +103,6 @@ var FAIL = function (deps, regexp) {
|
||||
}, regexp);
|
||||
};
|
||||
|
||||
Tinytest.add("constraint solver - exact dependencies", function (test) {
|
||||
currentTest = test;
|
||||
t_progagateExact({ "sparky-forms": "=1.1.2" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
|
||||
t_progagateExact({ "sparky-forms": "=1.1.2", "forms": "=1.0.1" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
|
||||
t_progagateExact({ "sparky-forms": "=1.1.2", "sparkle": "=2.1.1" }, { "sparky-forms": "1.1.2", "forms": "1.0.1", "sparkle": "2.1.1" });
|
||||
t_progagateExact({ "awesome-dropdown": "=1.5.0" }, { "awesome-dropdown": "1.5.0", "dropdown": "1.2.2" });
|
||||
t_progagateExact({ foobar1: "=1.0.0" }, {foobar1: "1.0.0", foobar2: "1.0.0"});
|
||||
|
||||
FAIL({ "sparky-forms": "=1.1.2", "sparkle": "=1.0.0" }, /(.*sparkle.*sparky-forms.*)|(.*sparky-forms.*sparkle.*).*sparkle/);
|
||||
// something that isn't available for your architecture
|
||||
FAIL({ "sparky-forms": "=1.1.2", "sparkle": "=2.0.0" });
|
||||
FAIL({ "sparky-forms": "=0.0.1" });
|
||||
FAIL({ "sparky-forms-nonexistent": "0.0.1" }, /Cannot find anything about.*sparky-forms-nonexistent/);
|
||||
});
|
||||
|
||||
Tinytest.add("constraint solver - simple exact + regular deps", function (test) {
|
||||
currentTest = test;
|
||||
|
||||
|
||||
@@ -65,13 +65,6 @@ ConstraintSolver.PackagesResolver.prototype._loadPackageInfo = function (
|
||||
// actually have different archs used.
|
||||
var allArchs = ["os", "web.browser", "web.cordova"];
|
||||
|
||||
if (!self.catalog.getPackage(packageName, { noRefresh: true })) {
|
||||
_.each(allArchs, function (arch) {
|
||||
var unitName = packageName + "#" + arch;
|
||||
self.resolver.noUnitVersionsExist(unitName);
|
||||
});
|
||||
}
|
||||
|
||||
// XXX is sortedness actually relevant?
|
||||
var sortedVersions = self.catalog.getSortedVersions(packageName);
|
||||
_.each(sortedVersions, function (version) {
|
||||
@@ -282,32 +275,6 @@ var resolverResultToPackageMap = function (choices) {
|
||||
};
|
||||
|
||||
|
||||
// This method, along with the stopAfterFirstPropagation, are designed for
|
||||
// tests; they allow us to test Resolver._propagateExactTransDeps but with an
|
||||
// interface that's a little more like PackagesResolver.resolver.
|
||||
ConstraintSolver.PackagesResolver.prototype.propagateExactDeps = function (
|
||||
dependencies, constraints) {
|
||||
var self = this;
|
||||
|
||||
check(dependencies, [String]);
|
||||
check(constraints, [{ packageName: String, version: String, type: String }]);
|
||||
|
||||
_.each(dependencies, function (packageName) {
|
||||
self._ensurePackageInfoLoaded(packageName);
|
||||
});
|
||||
_.each(constraints, function (constraint) {
|
||||
self._ensurePackageInfoLoaded(constraint.packageName);
|
||||
});
|
||||
|
||||
var dc = self._splitDepsToConstraints(dependencies, constraints);
|
||||
|
||||
// XXX resolver.resolve can throw an error, should have error handling with
|
||||
// proper error translation.
|
||||
var res = self.resolver.resolve(dc.dependencies, dc.constraints,
|
||||
{ stopAfterFirstPropagation: true });
|
||||
return resolverResultToPackageMap(res);
|
||||
};
|
||||
|
||||
// takes dependencies and constraints and rewrites the names from "foo" to
|
||||
// "foo#os" and "foo#web.browser" and "foo#web.cordova"
|
||||
// XXX right now creates a dependency for every unibuild it can find
|
||||
@@ -396,21 +363,19 @@ ConstraintSolver.PackagesResolver.prototype._getResolverOptions =
|
||||
|
||||
resolverOptions.costFunction = function (state, options) {
|
||||
options = options || {};
|
||||
var choices = state.choices;
|
||||
var constraints = state.constraints;
|
||||
// very major, major, medium, minor costs
|
||||
// XXX maybe these can be calculated lazily?
|
||||
var cost = [0, 0, 0, 0];
|
||||
|
||||
var minimalConstraint = {};
|
||||
constraints.each(function (c) {
|
||||
state.constraints.each(function (c) {
|
||||
if (! _.has(minimalConstraint, c.name))
|
||||
minimalConstraint[c.name] = c.version;
|
||||
else if (semver.lt(c.version, minimalConstraint[c.name]))
|
||||
minimalConstraint[c.name] = c.version;
|
||||
});
|
||||
|
||||
mori.each(choices, function (nameAndUv) {
|
||||
mori.each(state.choices, function (nameAndUv) {
|
||||
var uv = mori.last(nameAndUv);
|
||||
if (_.has(prevSolMapping, uv.name)) {
|
||||
// The package was present in the previous solution
|
||||
@@ -468,12 +433,11 @@ ConstraintSolver.PackagesResolver.prototype._getResolverOptions =
|
||||
|
||||
resolverOptions.estimateCostFunction = function (state, options) {
|
||||
options = options || {};
|
||||
var dependencies = state.dependencies;
|
||||
var constraints = state.constraints;
|
||||
|
||||
var constraints = state.constraints;
|
||||
var cost = [0, 0, 0, 0];
|
||||
|
||||
dependencies.each(function (dep) {
|
||||
state.eachDependency(function (dep, alternatives) {
|
||||
// XXX don't try to estimate transitive dependencies
|
||||
if (! isRootDep[dep]) {
|
||||
cost[MINOR] += 10000000;
|
||||
@@ -482,8 +446,7 @@ ConstraintSolver.PackagesResolver.prototype._getResolverOptions =
|
||||
|
||||
if (_.has(prevSolMapping, dep)) {
|
||||
var prev = prevSolMapping[dep];
|
||||
var prevVersionMatches =
|
||||
_.isEmpty(constraints.violatedConstraints(prev, self.resolver));
|
||||
var prevVersionMatches = constraints.isSatisfied(prev, self.resolver);
|
||||
|
||||
// if it matches, assume we would pick it and the cost doesn't
|
||||
// increase
|
||||
|
||||
@@ -80,12 +80,18 @@ ConstraintSolver.ConstraintsList.prototype.forPackage = function (name, iter) {
|
||||
var exact = mori.get(forPackage, "exact");
|
||||
var inexact = mori.get(forPackage, "inexact");
|
||||
|
||||
var breaked = false;
|
||||
var niter = function (pair) {
|
||||
iter(mori.last(pair));
|
||||
if (iter(mori.last(pair)) === BREAK) {
|
||||
breaked = true;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
mori.each(exact, niter);
|
||||
mori.each(inexact, niter);
|
||||
mori.some(niter, exact);
|
||||
if (breaked)
|
||||
return;
|
||||
mori.some(niter, inexact);
|
||||
};
|
||||
|
||||
// doesn't break on the false return value
|
||||
@@ -129,23 +135,21 @@ ConstraintSolver.ConstraintsList.prototype.union = function (anotherList) {
|
||||
return newList;
|
||||
};
|
||||
|
||||
// Checks if the passed unit version violates any of the constraints.
|
||||
// Returns a list of constraints that are violated (empty if the unit
|
||||
// version does not violate any constraints).
|
||||
// XXX Returns a regular array, not a ConstraintsList.
|
||||
ConstraintSolver.ConstraintsList.prototype.violatedConstraints = function (
|
||||
// Checks if the passed unit version satisfies all of the constraints.
|
||||
ConstraintSolver.ConstraintsList.prototype.isSatisfied = function (
|
||||
uv, resolver) {
|
||||
var self = this;
|
||||
|
||||
var violated = [];
|
||||
var satisfied = true;
|
||||
|
||||
self.forPackage(uv.name, function (c) {
|
||||
if (! c.isSatisfied(uv, resolver)) {
|
||||
violated.push(c);
|
||||
satisfied = false;
|
||||
return BREAK;
|
||||
}
|
||||
});
|
||||
|
||||
return violated;
|
||||
return satisfied;
|
||||
};
|
||||
|
||||
// XXX Returns a regular array, not a ConstraintsList.
|
||||
@@ -162,21 +166,6 @@ ConstraintSolver.ConstraintsList.prototype.constraintsForPackage = function (p)
|
||||
};
|
||||
|
||||
|
||||
// a weird method that returns a list of exact constraints those correspond to
|
||||
// the dependencies in the passed list
|
||||
ConstraintSolver.ConstraintsList.prototype.exactDependenciesIntersection =
|
||||
function (deps) {
|
||||
var self = this;
|
||||
var newList = new ConstraintSolver.ConstraintsList();
|
||||
|
||||
self.eachExact(function (c) {
|
||||
if (deps.contains(c.name))
|
||||
newList = newList.push(c);
|
||||
});
|
||||
|
||||
return newList;
|
||||
};
|
||||
|
||||
// Finds the earliest and latest versions of package `dep` in `resolver` that
|
||||
// matches this list of constraints.
|
||||
// The feature is: it runs in linear time of all constraints for given package
|
||||
@@ -197,8 +186,8 @@ ConstraintSolver.ConstraintsList.prototype.edgeMatchingVersionsFor = function (
|
||||
return;
|
||||
|
||||
// If there's an exact constraint, then remember that, and we'll ignore all
|
||||
// the other constraints. (We'll later use self.violatedConstraints to
|
||||
// ensure that there aren't any constraints that conflict with this choice.)
|
||||
// the other constraints. (We'll later use self.isSatisfied to ensure that
|
||||
// there aren't any constraints that conflict with this choice.)
|
||||
if (c.type === "exactly") {
|
||||
exactConstraint = c;
|
||||
return;
|
||||
@@ -243,10 +232,11 @@ ConstraintSolver.ConstraintsList.prototype.edgeMatchingVersionsFor = function (
|
||||
// there is some exact constraint, the choice is obvious... if it works.
|
||||
if (exactConstraint) {
|
||||
var uv = exactConstraint.getSatisfyingUnitVersion(resolver);
|
||||
if (uv && _.isEmpty(self.violatedConstraints(uv, resolver)))
|
||||
if (uv && self.isSatisfied(uv, resolver)) {
|
||||
return { earliest: uv, latest: uv };
|
||||
else
|
||||
} else {
|
||||
return { earliest: null, latest: null };
|
||||
}
|
||||
}
|
||||
|
||||
// OK, maybe we have a lower bound and/or an earliestCompatibleVersion
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// DependenciesList
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// A persistent data-structure that wrapps persistent dictionary
|
||||
|
||||
ConstraintSolver.DependenciesList = function (prev) {
|
||||
var self = this;
|
||||
|
||||
if (prev) {
|
||||
self._mapping = prev._mapping;
|
||||
} else {
|
||||
self._mapping = mori.hash_map();
|
||||
}
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.contains = function (d) {
|
||||
var self = this;
|
||||
return mori.has_key(self._mapping, d);
|
||||
};
|
||||
|
||||
// returns a new version containing passed dependency
|
||||
ConstraintSolver.DependenciesList.prototype.push = function (d) {
|
||||
var self = this;
|
||||
|
||||
if (self.contains(d)) {
|
||||
return self;
|
||||
}
|
||||
|
||||
var newList = new ConstraintSolver.DependenciesList(self);
|
||||
newList._mapping = mori.assoc(self._mapping, d, d);
|
||||
return newList;
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.remove = function (d) {
|
||||
var self = this;
|
||||
var newList = new ConstraintSolver.DependenciesList(self);
|
||||
newList._mapping = mori.dissoc(self._mapping, d);
|
||||
|
||||
return newList;
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.peek = function () {
|
||||
var self = this;
|
||||
return mori.last(mori.first(self._mapping));
|
||||
};
|
||||
|
||||
// a weird method that returns a list of exact constraints those correspond to
|
||||
// the dependencies in this list
|
||||
ConstraintSolver.DependenciesList.prototype.exactConstraintsIntersection =
|
||||
function (constraintsList) {
|
||||
var self = this;
|
||||
var exactConstraints = new ConstraintSolver.ConstraintsList();
|
||||
|
||||
self.each(function (d) {
|
||||
var c = mori.last(
|
||||
// pick an exact constraint for this dependency if such exists
|
||||
mori.last(mori.get(mori.get(constraintsList.byName, d), "exact")));
|
||||
|
||||
if (c)
|
||||
exactConstraints = exactConstraints.push(c);
|
||||
});
|
||||
|
||||
return exactConstraints;
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.union = function (anotherList) {
|
||||
var self = this;
|
||||
var newList = new ConstraintSolver.DependenciesList(self);
|
||||
newList._mapping = mori.union(newList._mapping, anotherList._mapping);
|
||||
|
||||
return newList;
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.isEmpty = function () {
|
||||
var self = this;
|
||||
return mori.is_empty(self._mapping);
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.each = function (iter) {
|
||||
var self = this;
|
||||
mori.some(function (d) {
|
||||
return iter(mori.last(d)) === BREAK;
|
||||
}, self._mapping);
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.toString = function (simple) {
|
||||
var self = this;
|
||||
var str = "";
|
||||
|
||||
var strs = [];
|
||||
self.each(function (d) {
|
||||
strs.push(d);
|
||||
});
|
||||
|
||||
strs.sort();
|
||||
_.each(strs, function (d) {
|
||||
if (str !== "") {
|
||||
str += simple ? " " : ", ";
|
||||
}
|
||||
str += d;
|
||||
});
|
||||
|
||||
return simple ? str : "<dependencies list: " + str + ">";
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.prototype.toArray = function () {
|
||||
var self = this;
|
||||
var arr = [];
|
||||
self.each(function (d) {
|
||||
arr.push(d);
|
||||
});
|
||||
|
||||
return arr;
|
||||
};
|
||||
|
||||
ConstraintSolver.DependenciesList.fromArray = function (arr) {
|
||||
var list = new ConstraintSolver.DependenciesList();
|
||||
var args = [];
|
||||
_.each(arr, function (d) {
|
||||
args.push(d);
|
||||
args.push(d);
|
||||
});
|
||||
|
||||
list._mapping = mori.hash_map.apply(mori, args);
|
||||
return list;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ Package.on_use(function (api) {
|
||||
api.use(['underscore', 'ejson', 'check', 'package-version-parser',
|
||||
'binary-heap', 'random'], 'server');
|
||||
api.add_files(['constraint-solver.js', 'resolver.js', 'constraints-list.js',
|
||||
'dependencies-list.js', 'priority-queue.js'], ['server']);
|
||||
'resolver-state.js', 'priority-queue.js'], ['server']);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
136
packages/constraint-solver/resolver-state.js
Normal file
136
packages/constraint-solver/resolver-state.js
Normal file
@@ -0,0 +1,136 @@
|
||||
ResolverState = function (resolver) {
|
||||
var self = this;
|
||||
self._resolver = resolver;
|
||||
// The versions we've already chosen.
|
||||
// unitName -> UnitVersion
|
||||
self.choices = mori.hash_map();
|
||||
// Units we need, but haven't chosen yet.
|
||||
// unitName -> set(UnitVersions)
|
||||
self._dependencies = mori.hash_map();
|
||||
// Constraints that apply.
|
||||
self.constraints = new ConstraintSolver.ConstraintsList;
|
||||
// If we've already hit a contradiction.
|
||||
self.error = null;
|
||||
};
|
||||
|
||||
_.extend(ResolverState.prototype, {
|
||||
addConstraint: function (constraint) {
|
||||
var self = this;
|
||||
if (self.error)
|
||||
return self;
|
||||
self = self._clone();
|
||||
|
||||
self.constraints = self.constraints.push(constraint);
|
||||
|
||||
var chosen = mori.get(self.choices, constraint.name);
|
||||
if (chosen && !constraint.isSatisfied(chosen, self._resolver)) {
|
||||
// This constraint conflicts with a choice we've already made!
|
||||
self.error = "conflict: " + constraint.toString() + " vs " +
|
||||
chosen.version;
|
||||
return self;
|
||||
}
|
||||
|
||||
var alternatives = mori.get(self._dependencies, constraint.name);
|
||||
if (alternatives) {
|
||||
var newAlternatives = mori.set(mori.filter(function (unitVersion) {
|
||||
return constraint.isSatisfied(unitVersion, self._resolver);
|
||||
}, alternatives));
|
||||
if (mori.is_empty(newAlternatives)) {
|
||||
// XXX we should mention other constraints that are active
|
||||
self.error = "conflict: " + constraint.toString() +
|
||||
" cannot be satisfied";
|
||||
} else if (mori.count(newAlternatives) !== mori.count(alternatives)) {
|
||||
self._dependencies = mori.assoc(
|
||||
self._dependencies, constraint.name, newAlternatives);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
},
|
||||
addDependency: function (unitName) {
|
||||
var self = this;
|
||||
|
||||
if (self.error || mori.has_key(self.choices, unitName)
|
||||
|| mori.has_key(self._dependencies, unitName)) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self = self._clone();
|
||||
|
||||
if (!_.has(self._resolver.unitsVersions, unitName)) {
|
||||
self.error = "unknown package: " + unitName;
|
||||
return self;
|
||||
}
|
||||
|
||||
var alternatives = mori.set();
|
||||
_.each(self._resolver.unitsVersions[unitName], function (uv) {
|
||||
if (self.constraints.isSatisfied(uv, self._resolver)) {
|
||||
// XXX hang on to list of violated constraints and use it in error
|
||||
// message
|
||||
alternatives = mori.conj(alternatives, uv);
|
||||
}
|
||||
});
|
||||
|
||||
if (mori.is_empty(alternatives)) {
|
||||
// XXX mention constraints or something
|
||||
self.error = "conflict: " + unitName + " can't be satisfied";
|
||||
return self;
|
||||
}
|
||||
|
||||
self._dependencies = mori.assoc(self._dependencies, unitName, alternatives);
|
||||
|
||||
return self;
|
||||
},
|
||||
addChoice: function (uv) {
|
||||
var self = this;
|
||||
|
||||
if (self.error)
|
||||
return self;
|
||||
if (mori.has_key(self.choices, uv.name))
|
||||
throw Error("Already chose " + uv.name);
|
||||
if (!mori.has_key(self._dependencies, uv.name))
|
||||
throw Error("No need to choose " + uv.name);
|
||||
|
||||
self = self._clone();
|
||||
|
||||
// Does adding this choice break some constraints we already have?
|
||||
if (!self.constraints.isSatisfied(uv, self._resolver)) {
|
||||
// XXX improve error
|
||||
self.error = "conflict: " + uv.toString() + " can't be chosen";
|
||||
return self;
|
||||
}
|
||||
|
||||
// Great, move it from dependencies to choices.
|
||||
self.choices = mori.assoc(self.choices, uv.name, uv);
|
||||
self._dependencies = mori.dissoc(self._dependencies, uv.name);
|
||||
|
||||
// Since we're committing to this version, we're committing to all it
|
||||
// implies.
|
||||
_.each(uv.dependencies, function (unitName) {
|
||||
self = self.addDependency(unitName);
|
||||
});
|
||||
uv.constraints.each(function (constraint) {
|
||||
self = self.addConstraint(constraint);
|
||||
});
|
||||
|
||||
return self;
|
||||
},
|
||||
success: function () {
|
||||
var self = this;
|
||||
return !self.error && mori.is_empty(self._dependencies);
|
||||
},
|
||||
eachDependency: function (iter) {
|
||||
var self = this;
|
||||
mori.some(function (nameAndAlternatives) {
|
||||
return BREAK == iter(mori.first(nameAndAlternatives),
|
||||
mori.last(nameAndAlternatives));
|
||||
}, self._dependencies);
|
||||
},
|
||||
_clone: function () {
|
||||
var self = this;
|
||||
var clone = new ResolverState(self._resolver);
|
||||
_.each(['choices', '_dependencies', 'constraints', 'error'], function (field) {
|
||||
clone[field] = self[field];
|
||||
});
|
||||
return clone;
|
||||
}
|
||||
});
|
||||
@@ -26,11 +26,6 @@ ConstraintSolver.Resolver = function (options) {
|
||||
// Maps name@version string to a unit version
|
||||
self._unitsVersionsMap = {};
|
||||
|
||||
// A set of unit names which have no unit versions (ie, the package does not
|
||||
// exist). Note that we only set this for nonexistent packages, not for
|
||||
// version-less packages, though maybe that's wrong.
|
||||
self._noUnitVersionsExist = {};
|
||||
|
||||
// Maps unit name string to the greatest version string we have
|
||||
self._latestVersion = {};
|
||||
|
||||
@@ -53,9 +48,6 @@ ConstraintSolver.Resolver.prototype.addUnitVersion = function (unitVersion) {
|
||||
|
||||
check(unitVersion, ConstraintSolver.UnitVersion);
|
||||
|
||||
if (_.has(self._noUnitVersionsExist, unitVersion.name))
|
||||
throw Error("but no unit versions exist for " + unitVersion.name + "!");
|
||||
|
||||
if (! _.has(self.unitsVersions, unitVersion.name)) {
|
||||
self.unitsVersions[unitVersion.name] = [];
|
||||
self._latestVersion[unitVersion.name] = unitVersion.version;
|
||||
@@ -70,14 +62,6 @@ ConstraintSolver.Resolver.prototype.addUnitVersion = function (unitVersion) {
|
||||
self._latestVersion[unitVersion.name] = unitVersion.version;
|
||||
};
|
||||
|
||||
ConstraintSolver.Resolver.prototype.noUnitVersionsExist = function (unitName) {
|
||||
var self = this;
|
||||
|
||||
if (_.has(self.unitsVersions, unitName))
|
||||
throw Error("already have unit versions for " + unitName + "!");
|
||||
self._noUnitVersionsExist[unitName] = true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
ConstraintSolver.Resolver.prototype.getUnitVersion = function (unitName, version) {
|
||||
@@ -155,46 +139,6 @@ ConstraintSolver.Resolver.prototype.resolve = function (
|
||||
}
|
||||
}, options);
|
||||
|
||||
// required for error reporting later.
|
||||
// maps Constraint [object identity! thus getConstraint] to list of unit name
|
||||
var constraintAncestor = mori.hash_map();
|
||||
_.each(constraints, function (c) {
|
||||
constraintAncestor = mori.assoc(constraintAncestor, c, mori.list(c.name));
|
||||
});
|
||||
|
||||
dependencies = ConstraintSolver.DependenciesList.fromArray(dependencies);
|
||||
constraints = ConstraintSolver.ConstraintsList.fromArray(constraints);
|
||||
|
||||
// create a fake unit version to represent the app or the build target
|
||||
var fakeUnitName = "###TARGET###";
|
||||
var appUV = new ConstraintSolver.UnitVersion(fakeUnitName, "1.0.0", "0.0.0");
|
||||
appUV.dependencies = dependencies;
|
||||
appUV.constraints = constraints;
|
||||
|
||||
// state is an object:
|
||||
// - dependencies: DependenciesList
|
||||
// - constraints: ConstraintsList
|
||||
// - choices: mori.has_map of unitName to UnitVersion
|
||||
// - constraintAncestor: mapping Constraint -> mori.list(unitName)
|
||||
var startState = self._propagateExactTransDeps(
|
||||
appUV, dependencies, constraints, choices, constraintAncestor);
|
||||
// The fake unit version is not a real choice --- remove it!
|
||||
startState.choices = mori.dissoc(startState.choices, fakeUnitName);
|
||||
|
||||
if (options.stopAfterFirstPropagation)
|
||||
return startState.choices;
|
||||
|
||||
var pq = new PriorityQueue();
|
||||
var costFunction = options.costFunction;
|
||||
var estimateCostFunction = options.estimateCostFunction;
|
||||
var combineCostFunction = options.combineCostFunction;
|
||||
|
||||
var estimatedStartingCost =
|
||||
combineCostFunction(costFunction(startState),
|
||||
estimateCostFunction(startState));
|
||||
|
||||
pq.push(startState, [estimatedStartingCost, 0]);
|
||||
|
||||
// Mapping that assigns every package an integer priority. We compute this
|
||||
// dynamically and in the process of resolution we try to resolve packages
|
||||
// with higher priority first. This helps the resolver a lot because if some
|
||||
@@ -203,13 +147,38 @@ ConstraintSolver.Resolver.prototype.resolve = function (
|
||||
// great to try out and evaluate all versions early in the decision tree.
|
||||
var resolutionPriority = {};
|
||||
|
||||
// put direct dependencies on higher priority
|
||||
dependencies.each(function (dep) {
|
||||
resolutionPriority[dep] = 100;
|
||||
var startState = new ResolverState(self);
|
||||
_.each(constraints, function (constraint) {
|
||||
startState = startState.addConstraint(constraint);
|
||||
});
|
||||
_.each(dependencies, function (unitName) {
|
||||
startState = startState.addDependency(unitName);
|
||||
// Direct dependencies start on higher priority
|
||||
resolutionPriority[unitName] = 100;
|
||||
});
|
||||
|
||||
if (startState.success()) {
|
||||
return startState.choices;
|
||||
}
|
||||
|
||||
if (startState.error) {
|
||||
throwConstraintSolverError(startState.error);
|
||||
}
|
||||
|
||||
var pq = new PriorityQueue();
|
||||
var overallCostFunction = function (state) {
|
||||
return [
|
||||
options.combineCostFunction(
|
||||
options.costFunction(state),
|
||||
options.estimateCostFunction(state)),
|
||||
-mori.count(state.choices)
|
||||
];
|
||||
};
|
||||
|
||||
pq.push(startState, overallCostFunction(startState));
|
||||
|
||||
var someError = null;
|
||||
var solution = null;
|
||||
var anySucceeded = false;
|
||||
while (! pq.empty()) {
|
||||
// Since we're in a CPU-bound loop, allow yielding or printing a message or
|
||||
// something.
|
||||
@@ -217,326 +186,102 @@ ConstraintSolver.Resolver.prototype.resolve = function (
|
||||
|
||||
var currentState = pq.pop();
|
||||
|
||||
if (currentState.dependencies.isEmpty()) {
|
||||
solution = currentState.choices;
|
||||
break;
|
||||
if (currentState.success()) {
|
||||
return currentState.choices;
|
||||
}
|
||||
|
||||
var neighborsObj = self._stateNeighbors(currentState, resolutionPriority);
|
||||
|
||||
// Minor (but noticeable in the benchmark) optimization: as long as our
|
||||
// state is just yielding a single non-final state, keep processing it. This
|
||||
// lets us skip some intermediate invocations of the cost function.
|
||||
while (neighborsObj.success && neighborsObj.neighbors.length === 1
|
||||
&& !neighborsObj.neighbors[0].success()) {
|
||||
neighborsObj = self._stateNeighbors(
|
||||
neighborsObj.neighbors[0], resolutionPriority);
|
||||
}
|
||||
|
||||
if (! neighborsObj.success) {
|
||||
someError = someError || neighborsObj.failureMsg;
|
||||
resolutionPriority[neighborsObj.conflictingUnit] = (resolutionPriority[neighborsObj.conflictingUnit] || 0) + 1;
|
||||
resolutionPriority[neighborsObj.conflictingUnit] =
|
||||
(resolutionPriority[neighborsObj.conflictingUnit] || 0) + 1;
|
||||
} else {
|
||||
_.each(neighborsObj.neighbors, function (state) {
|
||||
var tentativeCost =
|
||||
combineCostFunction(costFunction(state),
|
||||
estimateCostFunction(state));
|
||||
|
||||
pq.push(state, [tentativeCost, -mori.count(state.choices)]);
|
||||
// We don't just return the first successful one we find, in case there
|
||||
// are multiple successful states (we want to sort by cost function in
|
||||
// that case).
|
||||
pq.push(state, overallCostFunction(state));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (solution)
|
||||
return solution;
|
||||
|
||||
// XXX should be much much better
|
||||
if (someError) {
|
||||
var e = new Error(someError);
|
||||
e.constraintSolverError = true;
|
||||
throw e;
|
||||
throwConstraintSolverError(someError);
|
||||
}
|
||||
|
||||
throw new Error("Couldn't resolve, I am sorry");
|
||||
throw new Error("ran out of states without error?");
|
||||
};
|
||||
|
||||
var throwConstraintSolverError = function (message) {
|
||||
var e = new Error(message);
|
||||
e.constraintSolverError = true;
|
||||
throw e;
|
||||
};
|
||||
|
||||
// state is an object:
|
||||
// - dependencies: DependenciesList - remaining dependencies
|
||||
// - constraints: ConstraintsList - constraints to satisfy
|
||||
// - choices: mori.hash_map unitName _> UnitVersion - current set of choices
|
||||
// - constraintAncestor: Constraint (string representation) ->
|
||||
// mori.list(Dependency name). Used for error reporting to indicate which
|
||||
// direct dependencies have caused a failure. For every constraint, this is
|
||||
// the list of direct dependencies which led to this constraint being present.
|
||||
//
|
||||
// returns {
|
||||
// success: Boolean,
|
||||
// failureMsg: String,
|
||||
// neighbors: [state]
|
||||
// }
|
||||
//
|
||||
// NOTE: assumes that exact dependencies are already propagated
|
||||
ConstraintSolver.Resolver.prototype._stateNeighbors = function (
|
||||
state, resolutionPriority) {
|
||||
var self = this;
|
||||
|
||||
var dependencies = state.dependencies;
|
||||
var constraints = state.constraints;
|
||||
var choices = state.choices;
|
||||
var constraintAncestor = state.constraintAncestor;
|
||||
var candidateName = null;
|
||||
var candidateVersions = null;
|
||||
var currentNaughtiness = -1;
|
||||
|
||||
var candidateName = dependencies.peek();
|
||||
var currentNaughtiness = resolutionPriority[candidateName] || 0;
|
||||
|
||||
dependencies.each(function (d) {
|
||||
state.eachDependency(function (unitName, versions) {
|
||||
// Prefer to resolve things where there is no choice first (at the very
|
||||
// least this includes local packages).
|
||||
// XXX should we do this in _propagateExactTransDeps too?
|
||||
if (_.size(self.unitsVersions[d]) === 1) {
|
||||
candidateName = d;
|
||||
// least this includes local packages and unknown packages).
|
||||
if (mori.count(versions) === 1) {
|
||||
candidateName = unitName;
|
||||
candidateVersions = versions;
|
||||
return BREAK;
|
||||
}
|
||||
var r = resolutionPriority[d] || 0;
|
||||
var r = resolutionPriority[unitName] || 0;
|
||||
if (r > currentNaughtiness) {
|
||||
currentNaughtiness = r;
|
||||
candidateName = d;
|
||||
candidateName = unitName;
|
||||
candidateVersions = versions;
|
||||
}
|
||||
});
|
||||
|
||||
var edgeVersions = constraints.edgeMatchingVersionsFor(candidateName, self);
|
||||
|
||||
edgeVersions.earliest = edgeVersions.earliest || { version: "1000.1000.1000" };
|
||||
edgeVersions.latest = edgeVersions.latest || { version: "0.0.0" };
|
||||
|
||||
if (_.has(self._noUnitVersionsExist, candidateName)) {
|
||||
return {
|
||||
success: false,
|
||||
failureMsg: "No such package: " + candidateName,
|
||||
conflictingUnit: candidateName
|
||||
};
|
||||
}
|
||||
|
||||
var candidateVersions =
|
||||
_.filter(self.unitsVersions[candidateName], function (uv) {
|
||||
// reject immideately if not in acceptable range
|
||||
return semver.lte(edgeVersions.earliest.version, uv.version) && semver.lte(uv.version, edgeVersions.latest.version);
|
||||
});
|
||||
|
||||
var generateError = function (name, constraints) {
|
||||
var violatedConstraints = constraints.constraintsForPackage(name);
|
||||
|
||||
var directDepsString = "";
|
||||
|
||||
_.each(violatedConstraints, function (c) {
|
||||
if (directDepsString !== "")
|
||||
directDepsString += ", ";
|
||||
var cAsString = c.toString();
|
||||
|
||||
directDepsString += mori.into_array(mori.get(constraintAncestor, c))
|
||||
.reverse().join("=>");
|
||||
directDepsString += "(" + c.toString() + ")";
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
// XXX We really want to say "directDep1 depends on X@1.0 and
|
||||
// directDep2 depends on X@2.0"
|
||||
// XXX Imrove message
|
||||
failureMsg: "Direct dependencies of " + directDepsString + " conflict on " + name,
|
||||
conflictingUnit: candidateName
|
||||
};
|
||||
};
|
||||
|
||||
if (_.isEmpty(candidateVersions)) {
|
||||
// var uv = self.unitsVersions[candidateName] &&
|
||||
// self.unitsVersions[candidateName][0];
|
||||
|
||||
// if (! uv) {
|
||||
// var violatedConstraints = constraints.constraintsForPackage(candidateName);
|
||||
// return { success: false, failureMsg: "Cannot find anything about package -- " + candidateName, conflictingUnit: candidateName };
|
||||
// }
|
||||
|
||||
return generateError(candidateName, constraints);
|
||||
}
|
||||
if (mori.is_empty(candidateVersions))
|
||||
throw Error("empty candidate set? should have detected earlier");
|
||||
|
||||
var neighbors = [];
|
||||
var firstError = null;
|
||||
|
||||
var neighbors = _.chain(candidateVersions).map(function (uv) {
|
||||
return self._propagateExactTransDeps(
|
||||
uv, dependencies, constraints, choices, constraintAncestor);
|
||||
}).filter(function (state) {
|
||||
var vcfc =
|
||||
violatedConstraintsForSomeChoice(state.choices, state.constraints, self);
|
||||
|
||||
if (! vcfc)
|
||||
return true;
|
||||
|
||||
if (! firstError) {
|
||||
firstError = generateError(vcfc.choice.name, constraints);
|
||||
mori.each(candidateVersions, function (unitVersion) {
|
||||
var neighborState = state.addChoice(unitVersion);
|
||||
if (!neighborState.error) {
|
||||
neighbors.push(neighborState);
|
||||
} else if (!firstError) {
|
||||
firstError = neighborState.error;
|
||||
}
|
||||
return false;
|
||||
}).value();
|
||||
|
||||
if (firstError && ! neighbors.length)
|
||||
return firstError;
|
||||
|
||||
// Should never be true as !!firstError === !neighbors.length but still check
|
||||
// just in case.
|
||||
if (! neighbors.length)
|
||||
return { success: false,
|
||||
failureMsg: "None of the versions unit produces a sensible result -- "
|
||||
+ candidateName,
|
||||
conflictingUnit: candidateName };
|
||||
|
||||
return { success: true, neighbors: neighbors };
|
||||
};
|
||||
|
||||
// Propagates exact dependencies (which have exact constraints) from
|
||||
// the given unit version taking into account the existing set of dependencies
|
||||
// and constraints.
|
||||
// Assumes that the unit versions graph without passed unit version is already
|
||||
// propagated (i.e. doesn't try to propagate anything not related to the passed
|
||||
// unit version).
|
||||
ConstraintSolver.Resolver.prototype._propagateExactTransDeps = function (
|
||||
uv, dependencies, constraints, choices, constraintAncestor) {
|
||||
var self = this;
|
||||
|
||||
// XXX representing a queue as an array with push/shift operations is not
|
||||
// efficient as Array.shift is O(N). Replace if it becomes a problem.
|
||||
var queue = [];
|
||||
// Boolean map to avoid adding the same stuff to queue over and over again.
|
||||
// Keeps the time complexity the same but can save some memory.
|
||||
var hasBeenEnqueued = {};
|
||||
// // For keeping track of new choices in this iteration
|
||||
// var oldChoice = {};
|
||||
// _.each(choices, function (uv) { oldChoice[uv.name] = uv; });
|
||||
|
||||
// Keeps track of the exact constraint that led to a choice
|
||||
var exactConstrForChoice = {};
|
||||
|
||||
// 'dependencies' tracks units that we still need to choose a version for,
|
||||
// so there is no need to keep anything that we've already chosen.
|
||||
mori.each(choices, function (nameAndUv) {
|
||||
dependencies = dependencies.remove(mori.first(nameAndUv));
|
||||
});
|
||||
|
||||
queue.push(uv);
|
||||
hasBeenEnqueued[uv.name] = true;
|
||||
while (queue.length > 0) {
|
||||
// Since we're in a CPU-bound loop, allow yielding or printing a message or
|
||||
// something.
|
||||
self._nudge && self._nudge();
|
||||
|
||||
uv = queue[0];
|
||||
queue.shift();
|
||||
|
||||
// Choose uv itself.
|
||||
choices = mori.assoc(choices, uv.name, uv);
|
||||
|
||||
// It's not longer a dependency we need to satisfy.
|
||||
dependencies = dependencies.remove(uv.name);
|
||||
|
||||
// Add the constraints from uv to the list of constraints we are tracking.
|
||||
constraints = constraints.union(uv.constraints);
|
||||
|
||||
// Add any dependencies from uv that haven't already been chosen to the list
|
||||
// of remaining dependencies.
|
||||
uv.dependencies.each(function (dep) {
|
||||
if (!mori.has_key(choices, dep)) {
|
||||
dependencies = dependencies.push(dep);
|
||||
}
|
||||
});
|
||||
|
||||
// Which nodes to process now? You'd think it would be
|
||||
// immediatelyImpliedVersions, sure. But that's too limited: that only
|
||||
// includes things where 'uv' both contains the exact constraint and the
|
||||
// dependency. If we already had a dependency but 'uv' adds an exact
|
||||
// constraint, or vice versa, we should go there too!
|
||||
// XXX Strangely, changing some of these calls between
|
||||
// exactDependenciesIntersection and exactConstraintsIntersection
|
||||
// (eg, changing the last one to exactConstraintsIntersection) has
|
||||
// a big impact on correctness or performance... eg, it totally
|
||||
// breaks the benchmark tests.
|
||||
var newExactConstraintsList = uv.dependencies
|
||||
.exactConstraintsIntersection(constraints)
|
||||
.union(uv.constraints.exactDependenciesIntersection(dependencies));
|
||||
|
||||
newExactConstraintsList.each(function (c) {
|
||||
var dep = c.getSatisfyingUnitVersion(self);
|
||||
if (! dep)
|
||||
throw new Error("No unit version was found for the constraint -- " + c.toString());
|
||||
|
||||
// Enqueue all new exact dependencies.
|
||||
if (_.has(hasBeenEnqueued, dep.name))
|
||||
return;
|
||||
queue.push(dep);
|
||||
hasBeenEnqueued[dep.name] = true;
|
||||
exactConstrForChoice[dep.name] = c;
|
||||
});
|
||||
|
||||
var constr = exactConstrForChoice[uv.name];
|
||||
if (! constr) {
|
||||
// likely the uv passed to this propagation in a first place
|
||||
constraints.forPackage(uv.name, function (c) { constr = c; });
|
||||
}
|
||||
// for error reporting
|
||||
uv.constraints.each(function (c) {
|
||||
if (! mori.has_key(constraintAncestor, c)) {
|
||||
constraintAncestor = mori.assoc(
|
||||
constraintAncestor,
|
||||
c,
|
||||
mori.cons(uv.name,
|
||||
(constr ? mori.get(constraintAncestor, constr) : null)));
|
||||
}
|
||||
});
|
||||
if (neighbors.length) {
|
||||
return { success: true, neighbors: neighbors };
|
||||
}
|
||||
|
||||
// // Update the constraintAncestor table
|
||||
// _.each(choices, function (uv) {
|
||||
// if (oldChoice[uv.name])
|
||||
// return;
|
||||
|
||||
// var relevantConstraint = null;
|
||||
// constraints.forPackage(uv.name, function (c) { relevantConstraint = c; });
|
||||
|
||||
// var rootAnc = null;
|
||||
// if (relevantConstraint) {
|
||||
// rootAnc = constraintAncestor[relevantConstraint.toString()];
|
||||
// } else {
|
||||
// // XXX this probably only works correctly when uv was a root dependency
|
||||
// // w/o a constraint or dependency of one of the root deps.
|
||||
// _.each(choices, function (choice) {
|
||||
// if (rootAnc)
|
||||
// return;
|
||||
|
||||
// if (choice.dependencies.contains(uv.name))
|
||||
// rootAnc = choice.name;
|
||||
// });
|
||||
|
||||
// if (! rootAnc)
|
||||
// rootAnc = uv.name;
|
||||
// }
|
||||
|
||||
// uv.constraints.each(function (c) {
|
||||
// if (! constraintAncestor[c.toString()])
|
||||
// constraintAncestor[c.toString()] = rootAnc;
|
||||
// });
|
||||
// });
|
||||
|
||||
return {
|
||||
dependencies: dependencies,
|
||||
constraints: constraints,
|
||||
choices: choices,
|
||||
constraintAncestor: constraintAncestor
|
||||
success: false,
|
||||
failureMsg: firstError,
|
||||
conflictingUnit: candidateName
|
||||
};
|
||||
};
|
||||
|
||||
var violatedConstraintsForSomeChoice = function (choices, constraints, resolver) {
|
||||
var ret = null;
|
||||
mori.each(choices, function (nameAndUv) {
|
||||
if (ret)
|
||||
return;
|
||||
var choice = mori.last(nameAndUv);
|
||||
|
||||
var violatedConstraints = constraints.violatedConstraints(choice, resolver);
|
||||
if (! _.isEmpty(violatedConstraints))
|
||||
ret = { constraints: violatedConstraints, choice: choice };
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// UnitVersion
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -553,7 +298,7 @@ ConstraintSolver.UnitVersion = function (name, unitVersion, ecv) {
|
||||
// Things with different build IDs should represent the same code, so ignore
|
||||
// them. (Notably: depending on @=1.3.1 should allow 1.3.1+local!)
|
||||
self.version = unitVersion.replace(/\+.*$/, '');
|
||||
self.dependencies = new ConstraintSolver.DependenciesList();
|
||||
self.dependencies = [];
|
||||
self.constraints = new ConstraintSolver.ConstraintsList();
|
||||
// a string in a form of "1.2.0"
|
||||
self.earliestCompatibleVersion = ecv;
|
||||
@@ -564,12 +309,10 @@ _.extend(ConstraintSolver.UnitVersion.prototype, {
|
||||
var self = this;
|
||||
|
||||
check(name, String);
|
||||
if (self.dependencies.contains(name)) {
|
||||
if (_.contains(self.dependencies, name)) {
|
||||
return;
|
||||
// XXX may also throw if it is unexpected
|
||||
throw new Error("Dependency already exists -- " + name);
|
||||
}
|
||||
self.dependencies = self.dependencies.push(name);
|
||||
self.dependencies.push(name);
|
||||
},
|
||||
addConstraint: function (constraint) {
|
||||
var self = this;
|
||||
|
||||
Reference in New Issue
Block a user