diff --git a/packages/constraint-solver/constraint-solver-tests.js b/packages/constraint-solver/constraint-solver-tests.js index 865ed5f537..389b3fa21a 100644 --- a/packages/constraint-solver/constraint-solver-tests.js +++ b/packages/constraint-solver/constraint-solver-tests.js @@ -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; diff --git a/packages/constraint-solver/constraint-solver.js b/packages/constraint-solver/constraint-solver.js index f82f18d7f6..9f5525ccbd 100644 --- a/packages/constraint-solver/constraint-solver.js +++ b/packages/constraint-solver/constraint-solver.js @@ -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 diff --git a/packages/constraint-solver/constraints-list.js b/packages/constraint-solver/constraints-list.js index 1e52a8949a..b2ba5c0d23 100644 --- a/packages/constraint-solver/constraints-list.js +++ b/packages/constraint-solver/constraints-list.js @@ -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 diff --git a/packages/constraint-solver/dependencies-list.js b/packages/constraint-solver/dependencies-list.js deleted file mode 100644 index cf9d24b645..0000000000 --- a/packages/constraint-solver/dependencies-list.js +++ /dev/null @@ -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 : ""; -}; - -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; -}; - diff --git a/packages/constraint-solver/package.js b/packages/constraint-solver/package.js index 6d35520841..17b14d971e 100644 --- a/packages/constraint-solver/package.js +++ b/packages/constraint-solver/package.js @@ -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) { diff --git a/packages/constraint-solver/resolver-state.js b/packages/constraint-solver/resolver-state.js new file mode 100644 index 0000000000..1fc6c8e2a0 --- /dev/null +++ b/packages/constraint-solver/resolver-state.js @@ -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; + } +}); diff --git a/packages/constraint-solver/resolver.js b/packages/constraint-solver/resolver.js index e7070a10c2..3c61f94b9f 100644 --- a/packages/constraint-solver/resolver.js +++ b/packages/constraint-solver/resolver.js @@ -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;