Permalink
Browse files

Allow package-name@x.y.z! override syntax in .meteor/packages.

With this commit, if a top-level package version constraint in
.meteor/packages ends with a '!' character, any other (non-!) constraints
on that package elsewhere in the application will be weakened to accept
any version of the package that is not less than the constraint,
regardless of whether the major/minor versions actually match.

This functionality is extremely useful in cases where an unmaintained
package was last published with api.versionsFrom(<some ancient version>),
thus constraining the major version of any Meteor core package it depended
on, but you really want to upgrade that core package anyway. Just put a
'!' after the core package's version constraint in your .meteor/packages
file, and you will almost certainly get your way. The fact that minimum
versions are still enforced is good/fine because the constraints you want
to override are typically ancient, so they easily match any recent version
of the package.

Your only recourse before this @x.y.z! syntax was to find a replacement
for the unmaintained package, or fork and modify it locally, or somehow
persuade the package author to publish a new version with a more
reasonable api.versionsFrom. None of these options were easy.

Many thanks to @GeoffreyBooth, long-time maintainer of the `coffeescript`
package, for originally suggesting a ! syntax similar to this one:
meteor/meteor-feature-requests#208 (comment)

The limitation of this syntax to .meteor/packages is deliberate, since
overriding package version constraints is a power-tool that should be used
sparingly by application developers, and never abused by package authors.
Also, limiting the scope of this syntax reduces the risk of an arms race
between overrides, a la the infamous CSS !important modifier.
  • Loading branch information...
benjamn committed Aug 14, 2018
1 parent 8dd3ce5 commit 4a70b12eddef00b6700f129e90018a6076cb1681
@@ -182,10 +182,26 @@ CS.isConstraintSatisfied = function (pkg, vConstraint, version) {

if (type === "any-reasonable") {
return true;
} else if (type === "exactly") {
}

// If any top-level constraints use the @x.y.z! override syntax, all
// other constraints on the same package will be marked with the
// weakMinimum property, which means they constrain nothing other than
// the minimum version of the package. Look for weakMinimum in the
// CS.Solver#analyze method for related logic.
if (vConstraint.weakMinimum) {
return ! PV.lessThan(
PV.parse(version),
PV.parse(simpleConstraint.versionString)
);
}

if (type === "exactly") {
var cVersion = simpleConstraint.versionString;
return (cVersion === version);
} else if (type === 'compatible-with') {
}

if (type === 'compatible-with') {
if (typeof simpleConstraint.test === "function") {
return simpleConstraint.test(version);
}
@@ -206,9 +222,9 @@ CS.isConstraintSatisfied = function (pkg, vConstraint, version) {
}

return true;
} else {
throw Error("Unknown constraint type: " + type);
}

throw Error("Unknown constraint type: " + type);
});
};

@@ -1,6 +1,6 @@
Package.describe({
summary: "Given the set of the constraints, picks a satisfying configuration",
version: "1.1.1"
version: "1.2.0"
});

Package.onUse(function (api) {
@@ -207,11 +207,54 @@ CS.Solver.prototype.analyze = function () {
analysis.topLevelEqualityConstrainedPackages = {};

Profile.time("analyze constraints", function () {
// Find package names with @x.y.z! overrides. We consider only
// top-level constraints here, which includes (1) .meteor/packages,
// (2) local package versions, and (3) Meteor release constraints.
// Since (2) and (3) are generated programmatically without any
// override syntax (in tools/project-context.js), the .meteor/packages
// file is effectively the only place where override syntax has any
// impact. This limitation is deliberate, since overriding package
// version constraints is a power-tool that should be used sparingly
// by application developers, and never abused by package authors.
var overrides = new Set;
_.each(input.constraints, function (c) {
if (c.constraintString &&
c.versionConstraint.override) {
overrides.add(c.package);
}
});

// Return c.versionConstraint unless it is overridden, in which case
// make a copy of it and set vConstraint.weakMinimum = true.
function getVersionConstraint(c) {
var vConstraint = c.versionConstraint;
if (vConstraint.override) {
return vConstraint;
}

if (overrides.has(c.package)) {
// Make a defensive shallow copy of vConstraint with the same
// prototype (that is, PV.VersionConstraint.prototype).
vConstraint = Object.create(
Object.getPrototypeOf(vConstraint),
Object.getOwnPropertyDescriptors(vConstraint)
);

// This weakens the constraint so that it matches any version not
// less than the constraint, regardless of whether the major or
// minor versions are the same. See CS.isConstraintSatisfied in
// constraint-solver.js for the implementation of this behavior.
vConstraint.weakMinimum = true;
}

return vConstraint;
}

// top-level constraints
_.each(input.constraints, function (c) {
if (c.constraintString) {
analysis.constraints.push(new CS.Solver.Constraint(
null, c.package, c.versionConstraint,
null, c.package, getVersionConstraint(c),
"constraint#" + analysis.constraints.length));

if (c.versionConstraint.alternatives.length === 1 &&
@@ -231,7 +274,7 @@ CS.Solver.prototype.analyze = function () {
if (input.isKnownPackage(p2) &&
dep.packageConstraint.constraintString) {
analysis.constraints.push(new CS.Solver.Constraint(
pv, p2, dep.packageConstraint.versionConstraint,
pv, p2, getVersionConstraint(dep.packageConstraint),
"constraint#" + analysis.constraints.length));
}
});
@@ -225,21 +225,30 @@ PV.compare = function (versionOne, versionTwo) {
}
};

// Conceptually we have three types of constraints:
// 1. "compatible-with" - A@x.y.z - constraints package A to version x.y.z or
// higher, as long as the version is backwards compatible with x.y.z.
// "pick A compatible with x.y.z"
// It is the default type.
// 2. "exactly" - A@=x.y.z - constraints package A only to version x.y.z and
// Conceptually we have four types of simple constraints:
//
// 1. "any-reasonable" - "A" - any version of A is allowed (other than
// prerelease versions that contain dashes, unless a prerelease version
// has been explicitly selected elsewhere).
//
// 2. "compatible-with" (major) - "A@x.y.z" - constrains package A to
// version x.y.z or higher, and requires the major version of package A
// to match x. This is the most common kind of version constraint.
//
// 3. "compatible-with" (minor) - "A@~x.y.z" - constrains package A to
// version x.y.z or higher, and requires the major and minor versions
// of package A to match x and y, respectively. This style is allowed
// anywhere, but is used most often to constrain the minor versions of
// Meteor core packages, according to the current Meteor release.
//
// 4. "exactly" - A@=x.y.z - constrains package A to version x.y.z and
// nothing else.
// "pick A exactly at x.y.z"
// 3. "any-reasonable" - "A"
// Basically, this means any version of A ... other than ones that have
// dashes in the version (ie, are prerelease) ... unless the prerelease
// version has been explicitly selected (which at this stage in the game
// means they are mentioned in a top-level constraint in the top-level
// call to the resolver).
var parseSimpleConstraint = function (constraintString) {
//
// If a top-level constraint (e.g. in .meteor/packages) ends with a '!'
// character, any other constraints on that package will be weakened to
// accept any version of the package that is not less than the constraint,
// regardless of whether the major/minor versions match.
function parseSimpleConstraint(constraintString) {
if (! constraintString) {
throw new Error("Non-empty string required");
}
@@ -279,8 +288,7 @@ var parseSimpleConstraint = function (constraintString) {
}

return result;
};

}

// Check to see if the versionString that we pass in is a valid meteor version.
//
@@ -301,6 +309,18 @@ PV.VersionConstraint = function (vConstraintString) {
[ { type: "any-reasonable", versionString: null } ];
vConstraintString = "";
} else {
if (vConstraintString.endsWith("!")) {
// If a top-level constraint (e.g. from .meteor/packages) ends with
// a '!' character, any other constraints on that package will be
// weakened to accept any version of the package that is not less
// than the constraint, regardless of whether the major/minor
// versions actually match. See packages/constraint-solver/solver.js
// for implementation details.
this.override = true;
vConstraintString =
vConstraintString.slice(0, vConstraintString.length - 1);
}

// Parse out the versionString.
var parts = vConstraintString.split(/ *\|\| */);
alternatives = parts.map(function (alt) {
@@ -1,6 +1,6 @@
Package.describe({
summary: "Parses Meteor Smart Package version strings",
version: "3.0.10"
version: "3.2.0"
});

Npm.depends({
@@ -728,20 +728,20 @@ _.extend(ProjectContext.prototype, {
}),

_getRootDepsAndConstraints: function () {
var self = this;
const depsAndConstraints = {
deps: [],
constraints: [],
};

var depsAndConstraints = {deps: [], constraints: []};
this._addAppConstraints(depsAndConstraints);
this._addLocalPackageConstraints(depsAndConstraints);
this._addReleaseConstraints(depsAndConstraints);

self._addAppConstraints(depsAndConstraints);
self._addLocalPackageConstraints(depsAndConstraints);
self._addReleaseConstraints(depsAndConstraints);
return depsAndConstraints;
},

_addAppConstraints: function (depsAndConstraints) {
var self = this;

self.projectConstraintsFile.eachConstraint(function (constraint) {
this.projectConstraintsFile.eachConstraint(function (constraint) {
// Add a dependency ("this package must be used") and a constraint
// ("... at this version (maybe 'any reasonable')").
depsAndConstraints.deps.push(constraint.package);

6 comments on commit 4a70b12

@benjamn

This comment has been minimized.

Member

benjamn replied Aug 15, 2018

This feature will obviously need some tests!

@GeoffreyBooth

This comment has been minimized.

Contributor

GeoffreyBooth replied Aug 15, 2018

Thank you! This looks like just the type of UX I was looking for!

One question that I assume you’ve considered: does the version number need to be specified? Like can a .meteor/packages file contain just

coffeescript!

or does it need to be something like

coffeescript@2.2!

? I’m assuming that both should work (and in either case, the exact version would be specified in .meteor/versions).

@benjamn

This comment has been minimized.

Member

benjamn replied Aug 15, 2018

There has to be a @x.y.z version of some kind before the !. These changes don't clearly enforce that rule, but it's enforced elsewhere, and I think it's what we want.

@GeoffreyBooth

This comment has been minimized.

Contributor

GeoffreyBooth replied Aug 15, 2018

If it needs to be a full version, i.e. coffeescript@2.2.1_1!, then it would be nice if there was some way to upgrade this easily. Like if meteor update coffeescript bumps this to whatever the current latest version is, like coffeescript@2.3.0_1! or whatever.

@benjamn

This comment has been minimized.

Member

benjamn replied Aug 15, 2018

I had to check, but it looks like meteor update coffeescript does not touch your .meteor/packages file. There's some behavior for adding version constraints to core packages when you update Meteor itself, but I think non-core packages are left alone.

The good news is that a constraint of coffeescript@2.2.1_1! is fully compatible with coffeescript@2.3.0_1, since it's a later version, and the major versions match. That's an important difference between coffeescript@2.2.1_1! and coffeescript@=2.2.1_1: the ! version means "take this version constraint seriously" whereas the = version means "use exactly this version."

@GeoffreyBooth

This comment has been minimized.

Contributor

GeoffreyBooth replied Aug 15, 2018

Ah, so coffeescript@2.2.1_1 is like package.json’s "^2.2.1_1" it seems like. I didn’t know that, that’s good to know.

Yeah, then this UX works as far as I can tell. Looking forward to it getting merged in!

Please sign in to comment.