mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
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:
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user