Implement --breaking flag

--breaking:
Allow packages in your project to be upgraded or downgraded
to versions that are not semver-compatible with the current
versions, if required to resolve package version conflicts.

Fix behavior so that packages you're updating count too.
This commit is contained in:
David Greenspan
2015-02-09 17:12:24 -08:00
parent 64e9eaea5b
commit aa645f86a7
8 changed files with 196 additions and 35 deletions

View File

@@ -13,6 +13,7 @@ CS.Input = function (dependencies, constraints, catalogCache, options) {
self.upgrade = options.upgrade || [];
self.anticipatedPrereleases = options.anticipatedPrereleases || {};
self.previousSolution = options.previousSolution || null;
self.mayBreakRootDependencies = options.mayBreakRootDependencies || false;
check(self.dependencies, [String]);
check(self.constraints, [PackageConstraintType]);
@@ -89,7 +90,10 @@ CS.Input.prototype.toJSONable = function () {
}
if (self.previousSolution !== null) {
obj.previousSolution = self.previousSolution;
};
}
if (self.mayBreakRootDependencies) {
obj.mayBreakRootDependencies = true;
}
return obj;
};
@@ -101,7 +105,8 @@ CS.Input.fromJSONable = function (obj) {
anticipatedPrereleases: Match.Optional(
Match.ObjectWithValues(Match.ObjectWithValues(Boolean))),
previousSolution: Match.Optional(Match.OneOf(Object, null)),
upgrade: Match.Optional([String])
upgrade: Match.Optional([String]),
mayBreakRootDependencies: Match.Optional(Boolean)
});
return new CS.Input(
@@ -113,7 +118,8 @@ CS.Input.fromJSONable = function (obj) {
{
upgrade: obj.upgrade,
anticipatedPrereleases: obj.anticipatedPrereleases,
previousSolution: obj.previousSolution
previousSolution: obj.previousSolution,
mayBreakRootDependencies: obj.mayBreakRootDependencies
});
};

View File

@@ -27,6 +27,8 @@ CS.PackagesResolver = function (catalog, options) {
// included versions are the only pre-releases that are allowed to match
// constraints that don't specifically name them during the "try not to
// use unanticipated pre-releases" pass
// - mayBreakRootDependencies: allows choosing versions of
// root dependencies that are incompatible with the previous solution
// - missingPreviousVersionIsError - throw an error if a package version in
// previousSolution is not found in the catalog
CS.PackagesResolver.prototype.resolve = function (dependencies, constraints,
@@ -35,7 +37,8 @@ CS.PackagesResolver.prototype.resolve = function (dependencies, constraints,
options = options || {};
var input = new CS.Input(dependencies, constraints, self.catalogCache,
_.pick(options, 'upgrade', 'anticipatedPrereleases',
'previousSolution'));
'previousSolution',
'mayBreakRootDependencies'));
input.loadFromCatalog(self.catalogLoader);
if (options.previousSolution && options.missingPreviousVersionIsError) {

View File

@@ -35,6 +35,21 @@ var doTest = function (test, inputJSONable, outputJSONable) {
formatSolution(outputJSONable));
};
var doFailTest = function (test, inputJSONable, messageExpect) {
var input = CS.Input.fromJSONable(inputJSONable);
test.throws(function () {
try {
CS.PackagesResolver._resolveWithInput(input);
} catch (e) {
if (! e.constraintSolverError) {
test.fail(e.message);
}
throw e;
}
}, messageExpect);
};
Tinytest.add("constraint solver - input - upgrade indirect dependency", function (test) {
doTest(test, {
dependencies: ["foo"],
@@ -79,6 +94,42 @@ Tinytest.add("constraint solver - input - previous solution no patch", function
});
});
Tinytest.add("constraint solver - input - don't break root dep", function (test) {
doTest(test, {
dependencies: ["foo", "bar"],
constraints: [],
previousSolution: { bar: "2.0.0" },
upgrade: [],
catalogCache: {
data: {
"foo 1.0.0": ["bar@=2.0.1"],
"bar 2.0.0": [],
"bar 2.0.1": []
}
}
}, {
answer: {
foo: "1.0.0",
bar: "2.0.1"
}
});
doFailTest(test, {
dependencies: ["foo", "bar"],
constraints: [],
previousSolution: { bar: "2.0.1" },
upgrade: [],
catalogCache: {
data: {
"foo 1.0.0": ["bar@=2.0.0"],
"bar 2.0.0": [],
"bar 2.0.1": []
}
}
}, 'Breaking change required to top-level dependency: bar 2.0.0, was 2.0.1.\nConstraints:\n bar@=2.0.0 <- foo 1.0.0\nTo make breaking changes to top-level dependencies, you must pass --breaking.');
});
Tinytest.add("constraint solver - input - slow solve", function (test) {
var input = CS.Input.fromJSONable({
"dependencies": [

View File

@@ -175,6 +175,23 @@ CS.Solver.prototype.minimize = function (step, costTerms_, costWeights_) {
}
};
// Determine the non-zero contributions to the cost function in `step`
// based on the current solution, returning a map from term (usually
// the name of a package or package version) to positive integer cost.
CS.Solver.prototype.getStepContributions = function (step) {
var self = this;
var solution = self.solution;
var contributions = {};
var weights = step.weights;
_.each(step.terms, function (t, i) {
var w = (typeof weights === 'number' ? weights : weights[i]);
if (w && self.solution.evaluate(t)) {
contributions[t] = w;
}
});
return contributions;
};
CS.Solver.prototype.analyzeReachability = function () {
var self = this;
var input = self.input;
@@ -445,21 +462,48 @@ CS.Solver.prototype.getSolution = function () {
// cost function, so we can show a better error.
self.minimize('conflicts', _.pluck(analysis.constraints, 'conflictVar'));
// XXX This is where we will enforce that we don't make breaking changes
// to your root dependencies, unless you pass --breaking.
var previousRootSteps = self.getDistances(
'previous_root', analysis.previousRootDepVersions);
// the "previous_root_incompat" step
var previousRootIncompat = previousRootSteps[0];
// the "previous_root_major", "previous_root_minor", etc. steps
var previousRootVersionParts = previousRootSteps.slice(1);
var toUpdate = _.filter(input.upgrade, function (p) {
return analysis.reachablePackages[p] === true;
});
if (! input.mayBreakRootDependencies) {
// make sure packages that are being updated can still count as
// a previous_root for the purposes of previous_root_incompat
_.each(toUpdate, function (p) {
if (input.isRootDependency(p) && input.isInPreviousSolution(p)) {
var cats = self.pricer.categorizeVersions(
cache.getPackageVersions(p), input.previousSolution[p]);
_.each(cats.before.concat(cats.higherMajor), function (v) {
previousRootIncompat.addTerm(pvVar(p, v), 1);
});
}
});
}
if (! input.mayBreakRootDependencies) {
// Enforce that we don't make breaking changes to your root dependencies,
// unless you pass --breaking. It will actually be enforced farther down,
// but for now, we want to apply this constraint before handling updates.
self.minimize(previousRootIncompat);
}
self.minimize(self.getOldnesses('update', toUpdate));
var newRootDeps = _.filter(input.dependencies, function (p) {
return ! input.isInPreviousSolution(p);
});
self.minimize(self.getDistances(
'previous_root', analysis.previousRootDepVersions));
if (input.mayBreakRootDependencies) {
self.minimize(previousRootIncompat);
}
self.minimize(previousRootVersionParts);
var otherPrevious = _.filter(_.map(input.previousSolution, function (v, p) {
return new CS.PackageAndVersion(p, v);
@@ -493,10 +537,6 @@ CS.Solver.prototype.getSolution = function () {
}
});
//_.each(otherPackages, function (package) {
//self.minimize(self.getGravityPotential(
//'new_indirect(' + package + ')', [package]));
//});
self.minimize(self.getGravityPotential('new_indirect', otherPackages));
self.minimize('total_packages', _.keys(analysis.reachablePackages));
@@ -525,6 +565,23 @@ CS.Solver.prototype.getSolution = function () {
self.throwConflicts();
}
if ((! input.mayBreakRootDependencies) &&
self.stepsByName.previous_root_incompat.optimum > 0) {
_.each(_.keys(
self.getStepContributions(self.stepsByName.previous_root_incompat)),
function (pvStr) {
var pv = CS.PackageAndVersion.fromString(pvStr);
var prevVersion = input.previousSolution[pv.package];
self.errors.push(
'Breaking change required to top-level dependency: ' +
pvStr + ', was ' + prevVersion + '.\n' +
self.listConstraintsOnPackage(pv.package));
});
self.errors.push('To allow breaking changes to top-level dependencies, you ' +
'must pass --breaking to meteor [run], update, add, or remove.');
self.throwAnyErrors();
}
var versionMap = self.currentVersionMap();
return {
@@ -589,6 +646,30 @@ var _getConstraintFormula = function (toPackage, vConstraint) {
}
};
CS.Solver.prototype.listConstraintsOnPackage = function (package) {
var self = this;
var constraints = self.analysis.constraints;
var result = 'Constraints:';
_.each(constraints, function (c) {
if (c.toPackage === package) {
var paths;
if (c.fromVar) {
paths = self.getPathsToPackageVersion(
CS.PackageAndVersion.fromString(c.fromVar));
} else {
paths = [['top level']];
}
_.each(paths, function (path) {
result += '\n* ' + (new PV.PackageConstraint(
package, c.vConstraint.raw)) + ' <- ' + path.join(' <- ');
});
}
});
return result;
};
CS.Solver.prototype.throwConflicts = function () {
var self = this;
@@ -615,23 +696,7 @@ CS.Solver.prototype.throwConflicts = function () {
c.toPackage, c.vConstraint)) +
' is not satisfied by ' + c.toPackage + ' ' + chosenVersion + '.');
error += '\nConstraints:';
_.each(constraints, function (c2) {
if (c2.toPackage === c.toPackage) {
var paths;
if (c2.fromVar) {
paths = self.getPathsToPackageVersion(
CS.PackageAndVersion.fromString(c2.fromVar));
} else {
paths = [['top level']];
}
_.each(paths, function (path) {
error += '\n ' + (new PV.PackageConstraint(
c.toPackage, c2.vConstraint)) + ' <- ' + path.join(' <- ');
});
}
});
error += '\n' + self.listConstraintsOnPackage(c.toPackage);
self.errors.push(error);
}

View File

@@ -1470,7 +1470,8 @@ main.registerCommand({
name: 'update',
options: {
patch: { type: Boolean, required: false },
"packages-only": { type: Boolean, required: false }
"packages-only": { type: Boolean, required: false },
breaking: { type: Boolean, required: false }
},
// We have to be able to work without a release, since 'meteor
// update' is how you fix apps that don't have a release.
@@ -1514,7 +1515,8 @@ main.registerCommand({
// we're done even if we're not on the matching release!)
var projectContext = new projectContextModule.ProjectContext({
projectDir: options.appDir,
alwaysWritePackageMap: true
alwaysWritePackageMap: true,
mayBreakRootDependencies: options.breaking
});
main.captureAndExit("=> Errors while initializing project:", function () {
projectContext.readProjectMetadata();
@@ -1617,13 +1619,17 @@ main.registerCommand({
main.registerCommand({
name: 'add',
options: {
breaking: { type: Boolean, required: false }
},
minArgs: 1,
maxArgs: Infinity,
requiresApp: true,
catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: true })
}, function (options) {
var projectContext = new projectContextModule.ProjectContext({
projectDir: options.appDir
projectDir: options.appDir,
mayBreakRootDependencies: options.breaking
});
main.captureAndExit("=> Errors while initializing project:", function () {
// We're just reading metadata here --- we're not going to resolve
@@ -1797,13 +1803,17 @@ main.registerCommand({
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'remove',
options: {
breaking: { type: Boolean, required: false }
},
minArgs: 1,
maxArgs: Infinity,
requiresApp: true,
catalogRefresh: new catalog.Refresh.Never()
}, function (options) {
var projectContext = new projectContextModule.ProjectContext({
projectDir: options.appDir
projectDir: options.appDir,
mayBreakRootDependencies: options.breaking
});
main.captureAndExit("=> Errors while initializing project:", function () {
// We're just reading metadata here --- we're not going to resolve

View File

@@ -173,7 +173,10 @@ var runCommandOptions = {
// With --clean, meteor cleans the application directory and uses the
// bundled assets only. Encapsulates the behavior of once (does not rerun)
// and does not monitor for file changes. Not for end-user use.
clean: { type: Boolean}
clean: { type: Boolean},
// Allow the version solver to make breaking changes to the versions
// of top-level dependencies.
breaking: { type: Boolean }
},
catalogRefresh: new catalog.Refresh.Never()
};
@@ -220,7 +223,8 @@ function doRunCommand (options) {
options.httpProxyPort = options['http-proxy-port'];
var projectContext = new projectContextModule.ProjectContext({
projectDir: options.appDir
projectDir: options.appDir,
mayBreakRootDependencies: options.breaking
});
main.captureAndExit("=> Errors while initializing project:", function () {

View File

@@ -57,6 +57,9 @@ Options:
--settings Set optional data for Meteor.settings on the server.
--release Specify the release of Meteor to use.
--verbose Print all output from builds logs.
--breaking Allow packages in your project to be upgraded or downgraded
to versions that are not semver-compatible with the current
versions, if required to resolve package version conflicts.
--test [Experimental] Run Velocity tests using phantomjs and exit.
>>> debug
@@ -143,6 +146,9 @@ Options:
--packages-only Update the package versions only. Do not update the release.
--patch Update the release to a patch release only.
--release Update to a specific release of meteor.
--breaking Allow packages in your project to be upgraded or downgraded
to versions that are not semver-compatible with the current
versions, if required to resolve package version conflicts.
>>> admin run-upgrader
@@ -161,6 +167,11 @@ Adds packages to your Meteor project. You can add multiple
packages with one command. To query for available packages, use
the meteor search command.
Options:
--breaking Allow packages in your project to be upgraded or downgraded
to versions that are not semver-compatible with the current
versions, if required to resolve package version conflicts.
>>> remove
Remove a package from this project.
@@ -170,6 +181,11 @@ Removes a package previously added to your Meteor project. For a
list of the packages that your application is currently using, see
'meteor list'.
Options:
--breaking Allow packages in your project to be upgraded or downgraded
to versions that are not semver-compatible with the current
versions, if required to resolve package version conflicts.
>>> list
List the packages explicitly used by your project.

View File

@@ -139,6 +139,11 @@ _.extend(ProjectContext.prototype, {
self._cachedVersionsBeforeReset = null;
}
// The --breaking command-line switch, which allows the version solver
// to choose versions of root dependencies that are incompatible with
// the previous solution.
self._mayBreakRootDependencies = options.mayBreakRootDependencies;
// Initialized by readProjectMetadata.
self.releaseFile = null;
self.projectConstraintsFile = null;
@@ -402,6 +407,7 @@ _.extend(ProjectContext.prototype, {
var resolveOptions = {
previousSolution: cachedVersions,
anticipatedPrereleases: anticipatedPrereleases,
mayBreakRootDependencies: self._mayBreakRootDependencies,
// Not finding an exact match for a previous version in the catalog
// is considered an error if we haven't refreshed yet, and will
// trigger a refresh and another attempt. That way, if a previous