diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index 18be887a85..8fe72c5a8b 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -130,8 +130,7 @@ Autoupdate._retrySubscription = function () { attachStylesheetLink(newLink); }); } - else if (doc._id === 'version' && autoupdateVersion !== 'unknown' && - doc.version !== autoupdateVersion) { + else if (doc._id === 'version' && doc.version !== autoupdateVersion) { handle && handle.stop(); Package.reload.Reload._reload(); } diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 7ffab49700..60358afaa2 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -45,11 +45,12 @@ ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions", // runtime config before using the client hash as our default auto // update version id. +// Note: Tests allow people to override Autoupdate.autoupdateVersion before +// startup. Autoupdate.autoupdateVersion = null; Autoupdate.autoupdateVersionRefreshable = null; var syncQueue = new Meteor._SynchronousQueue(); -var startupVersion = null; // updateVersions can only be called after the server has fully loaded. var updateVersions = function (shouldReloadClientProgram) { @@ -60,13 +61,18 @@ var updateVersions = function (shouldReloadClientProgram) { WebAppInternals.reloadClientProgram(); } - if (startupVersion === null) { + // If we just re-read the client program, or if we don't have an autoupdate + // version, calculate it. + if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) { Autoupdate.autoupdateVersion = - __meteor_runtime_config__.autoupdateVersion = - process.env.AUTOUPDATE_VERSION || - process.env.SERVER_ID || // XXX COMPAT 0.6.6 - WebApp.calculateClientHashNonRefreshable(); + process.env.AUTOUPDATE_VERSION || + process.env.SERVER_ID || // XXX COMPAT 0.6.6 + WebApp.calculateClientHashNonRefreshable(); } + // If we just recalculated it OR if it was set by (eg) test-in-browser, + // ensure it ends up in __meteor_runtime_config__. + __meteor_runtime_config__.autoupdateVersion = + Autoupdate.autoupdateVersion; Autoupdate.autoupdateVersionRefreshable = __meteor_runtime_config__.autoupdateVersionRefreshable = @@ -115,9 +121,6 @@ var updateVersions = function (shouldReloadClientProgram) { }; Meteor.startup(function () { - // Allow people to override Autoupdate.autoupdateVersion before startup. - // Tests do this. - startupVersion = Autoupdate.autoupdateVersion; updateVersions(false); }); diff --git a/packages/constraint-solver/constraint-solver-tests.js b/packages/constraint-solver/constraint-solver-tests.js index a73ea11dcf..446ebb56df 100644 --- a/packages/constraint-solver/constraint-solver-tests.js +++ b/packages/constraint-solver/constraint-solver-tests.js @@ -515,8 +515,10 @@ function getCatalogStub (gems) { }); var ecv = function (version) { - // hard-code to "x.0.0" - return parseInt(version) + ".0.0"; + // hard-coded, because lots of the constraints are > or >= which we + // don't support anymore. But constant ECV means that "compatible-with" + // is interpreted as >=. + return "0.0.0"; }; var packageVersion = { @@ -565,12 +567,10 @@ function convertConstraints (inp) { // '>=1.2.3' => '1.2.3' .map(function (s) { if (s.indexOf(">= 0") === 0) - return "none"; + return ""; var x = s.split(' '); - if (x[0] === '~>') + if (x[0] === '~>' || x[0] === '>' || x[0] === '>=') x[0] = ''; - else if (x[0] === '>' || x[0] === '>=') - x[0] = '>='; else if (x[0] === '=') x[0] = '='; else diff --git a/packages/constraint-solver/constraint-solver.js b/packages/constraint-solver/constraint-solver.js index 19f9f97010..3ed7d372bd 100644 --- a/packages/constraint-solver/constraint-solver.js +++ b/packages/constraint-solver/constraint-solver.js @@ -141,8 +141,10 @@ ConstraintSolver.PackagesResolver.prototype._loadPackageInfo = function ( // dependencies - an array of string names of packages (not slices) // constraints - an array of objects: +// (almost, but not quite, what PackageVersion.parseConstraint returns) // - packageName - string name -// - version - string constraint (ex.: "1.2.3", ">=2.3.4", "=3.3.3") +// - version - string constraint +// - type - constraint type // options: // - upgrade - list of dependencies for which upgrade is prioritized higher // than keeping the old version @@ -161,7 +163,9 @@ ConstraintSolver.PackagesResolver.prototype.resolve = function ( check(dependencies, [String]); check(constraints, [{ - packageName: String, version: String, type: String, + packageName: String, + version: Match.OneOf(String, null), + type: String, constraintString: Match.Optional(Match.OneOf(String, null)) }]); @@ -281,15 +285,7 @@ ConstraintSolver.PackagesResolver.prototype._splitDepsToConstraints = }); _.each(inputConstraints, function (constraint) { - if (!semver.valid(constraint.version)) - throw Error("Bad semver: " + constraint.version); - var operator = ""; - if (constraint.type === "exactly") - operator = "="; - if (constraint.type === "at-least") - operator = ">="; - var constraintStr = operator + constraint.version; - + var constraintStr = PackageVersion.constraintToVersionString(constraint); _.each(self._unibuildsForPackage(constraint.packageName), function (unibuildName) { constraints.push(self.resolver.getConstraint(unibuildName, constraintStr)); }); @@ -414,7 +410,6 @@ ConstraintSolver.PackagesResolver.prototype._getResolverOptions = resolverOptions.estimateCostFunction = function (state, options) { options = options || {}; - var constraints = state.constraints; var cost = [0, 0, 0, 0]; state.eachDependency(function (dep, alternatives) { @@ -426,7 +421,7 @@ ConstraintSolver.PackagesResolver.prototype._getResolverOptions = if (_.has(prevSolMapping, dep)) { var prev = prevSolMapping[dep]; - var prevVersionMatches = constraints.isSatisfied(prev, self.resolver); + var prevVersionMatches = state.isSatisfied(prev); // 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 b927e1df0f..c19f19fad9 100644 --- a/packages/constraint-solver/constraints-list.js +++ b/packages/constraint-solver/constraints-list.js @@ -63,10 +63,12 @@ ConstraintSolver.ConstraintsList.prototype.push = function (c) { // Note that this is one of the only pieces of the constraint solver that // actually does logic on constraints (and thus relies on the restricted set // of constraints that we support). - var minimal = mori.get(newList.minimalVersion, c.name); - if (!minimal || semver.lt(c.version, minimal)) { - newList.minimalVersion = mori.assoc( - newList.minimalVersion, c.name, c.version); + if (c.type !== 'any-reasonable') { + var minimal = mori.get(newList.minimalVersion, c.name); + if (!minimal || semver.lt(c.version, minimal)) { + newList.minimalVersion = mori.assoc( + newList.minimalVersion, c.name, c.version); + } } return newList; }; @@ -102,13 +104,13 @@ ConstraintSolver.ConstraintsList.prototype.each = function (iter) { // Checks if the passed unit version satisfies all of the constraints. ConstraintSolver.ConstraintsList.prototype.isSatisfied = function ( - uv, resolver) { + uv, resolver, resolveContext) { var self = this; var satisfied = true; self.forPackage(uv.name, function (c) { - if (! c.isSatisfied(uv, resolver)) { + if (! c.isSatisfied(uv, resolver, resolveContext)) { satisfied = false; return BREAK; } diff --git a/packages/constraint-solver/resolver-state.js b/packages/constraint-solver/resolver-state.js index 6c4ebb4b11..875a48b8ce 100644 --- a/packages/constraint-solver/resolver-state.js +++ b/packages/constraint-solver/resolver-state.js @@ -1,6 +1,7 @@ -ResolverState = function (resolver) { +ResolverState = function (resolver, resolveContext) { var self = this; self._resolver = resolver; + self._resolveContext = resolveContext; // The versions we've already chosen. // unitName -> UnitVersion self.choices = mori.hash_map(); @@ -23,7 +24,8 @@ _.extend(ResolverState.prototype, { self.constraints = self.constraints.push(constraint); var chosen = mori.get(self.choices, constraint.name); - if (chosen && !constraint.isSatisfied(chosen, self._resolver)) { + if (chosen && + !constraint.isSatisfied(chosen, self._resolver, self._resolveContext)) { // This constraint conflicts with a choice we've already made! self.error = "conflict: " + constraint.toString({removeUnibuild: true}) + " vs " + chosen.version; @@ -34,7 +36,8 @@ _.extend(ResolverState.prototype, { if (alternatives) { // Note: filter preserves order, which is important. var newAlternatives = filter(alternatives, function (unitVersion) { - return constraint.isSatisfied(unitVersion, self._resolver); + return constraint.isSatisfied( + unitVersion, self._resolver, self._resolveContext); }); if (mori.is_empty(newAlternatives)) { // XXX we should mention other constraints that are active @@ -68,7 +71,7 @@ _.extend(ResolverState.prototype, { // Note: relying on sortedness of unitsVersions so that alternatives is // sorted too (the estimation function uses this). var alternatives = filter(self._resolver.unitsVersions[unitName], function (uv) { - return self.constraints.isSatisfied(uv, self._resolver); + return self.isSatisfied(uv); // XXX hang on to list of violated constraints and use it in error // message }); @@ -99,7 +102,7 @@ _.extend(ResolverState.prototype, { self = self._clone(); // Does adding this choice break some constraints we already have? - if (!self.constraints.isSatisfied(uv, self._resolver)) { + if (!self.isSatisfied(uv)) { // XXX improve error self.error = "conflict: " + uv.toString({removeUnibuild: true}) + " can't be chosen"; @@ -132,9 +135,13 @@ _.extend(ResolverState.prototype, { mori.last(nameAndAlternatives)); }, self._dependencies); }, + isSatisfied: function (uv) { + var self = this; + return self.constraints.isSatisfied(uv, self._resolver, self._resolveContext); + }, _clone: function () { var self = this; - var clone = new ResolverState(self._resolver); + var clone = new ResolverState(self._resolver, self._resolveContext); _.each(['choices', '_dependencies', 'constraints', 'error'], function (field) { clone[field] = self[field]; }); diff --git a/packages/constraint-solver/resolver-tests.js b/packages/constraint-solver/resolver-tests.js index 51e4661d4c..ce0bc12b77 100644 --- a/packages/constraint-solver/resolver-tests.js +++ b/packages/constraint-solver/resolver-tests.js @@ -153,22 +153,29 @@ Tinytest.add("constraint solver - resolver, don't pick rcs", function (test) { resolver.addUnitVersion(A100rc1); resolver.addUnitVersion(A100); - var initialConstraint = resolver.getConstraint("A", ">=0.0.0"); + var basicConstraint = resolver.getConstraint("A", ""); + var rcConstraint = resolver.getConstraint("A", "1.0.0-rc1"); - var solution = resolver.resolve(["A"], [initialConstraint], { - costFunction: function (state) { - return mori.reduce(mori.sum, 0, mori.map(function (nameAndUv) { - var name = mori.first(nameAndUv); - var uv = mori.last(nameAndUv); - // Make the non-rc one more costly. But we still shouldn't choose it! - if (uv.version === "1.0.0") - return 100; - return 0; - }, state.choices)); - } - }); + // Make the non-rc one more costly. But we still shouldn't choose it unless it + // was specified in an initial constraint! + var proRcCostFunction = function (state) { + return mori.reduce(mori.sum, 0, mori.map(function (nameAndUv) { + var name = mori.first(nameAndUv); + var uv = mori.last(nameAndUv); + // Make the non-rc one more costly. But we still shouldn't choose it! + if (uv.version === "1.0.0") + return 100; + return 0; + }, state.choices)); + }; + var solution = resolver.resolve( + ["A"], [basicConstraint], {costFunction: proRcCostFunction }); resultEquals(test, solution, [A100]); + + solution = resolver.resolve( + ["A"], [rcConstraint], {costFunction: proRcCostFunction }); + resultEquals(test, solution, [A100rc1]); }); function semver2number (semverStr) { diff --git a/packages/constraint-solver/resolver.js b/packages/constraint-solver/resolver.js index 1adef00487..897ebe676c 100644 --- a/packages/constraint-solver/resolver.js +++ b/packages/constraint-solver/resolver.js @@ -140,17 +140,41 @@ ConstraintSolver.Resolver.prototype.resolve = function ( } }, options); + var resolveContext = new ResolveContext; + // 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 // package has a higher weight to the solution (like a direct dependency) or // is more likely to break our solution in the future than others, it would be // great to try out and evaluate all versions early in the decision tree. + // XXX this could go on ResolveContext var resolutionPriority = {}; - var startState = new ResolverState(self); + var startState = new ResolverState(self, resolveContext); _.each(constraints, function (constraint) { startState = startState.addConstraint(constraint); + + // Keep track of any top-level constraints that mention a pre-release. + // These will be the only pre-release versions that count as "reasonable" + // for "any-reasonable" (ie, unconstrained) constraints. + // + // Why only top-level mentions, and not mentions we find while walking the + // graph? The constraint solver assumes that adding a constraint to the + // resolver state can't make previously impossible choices now possible. If + // pre-releases mentioned anywhere worked, then applying the constraints + // "any reasonable" followed by "1.2.3-rc1" would result in "1.2.3-rc1" + // ruled first impossible and then possible again. That's no good, so we + // have to fix the meaning based on something at the start. (We could try + // to apply our prerelease-avoidance tactics solely in the cost functions, + // but then it becomes a much less strict rule.) + if (constraint.version && /-/.test(constraint.version)) { + if (!_.has(resolveContext.topLevelPrereleases, constraint.name)) { + resolveContext.topLevelPrereleases[constraint.name] = {}; + } + resolveContext.topLevelPrereleases[constraint.name][constraint.version] + = true; + } }); _.each(dependencies, function (unitName) { startState = startState.addDependency(unitName); @@ -329,58 +353,75 @@ _.extend(ConstraintSolver.UnitVersion.prototype, { ConstraintSolver.Constraint = function (name, versionString) { var self = this; - if (versionString) { + if (versionString !== undefined) { _.extend(self, - PackageVersion.parseVersionConstraint( - versionString, {allowAtLeast: true})); + PackageVersion.parseVersionConstraint(versionString)); self.name = name; } else { // borrows the structure from the parseVersionConstraint format: - // - type - String [compatibl-with|exactly|at-least] + // - type - String [compatible-with|exactly|any-reasonable] // - version - String - semver string - _.extend(self, PackageVersion.parseConstraint(name, {allowAtLeast: true})); + _.extend(self, PackageVersion.parseConstraint(name)); } // See comment in UnitVersion constructor. - self.version = self.version.replace(/\+.*$/, ''); + if (self.version) + self.version = self.version.replace(/\+.*$/, ''); }; ConstraintSolver.Constraint.prototype.toString = function (options) { var self = this; options = options || {}; - var operator = ""; - if (self.type === "exactly") - operator = "="; - if (self.type === "at-least") - operator = ">="; var name = options.removeUnibuild ? removeUnibuild(self.name) : self.name; - return name + "@" + operator + self.version; + return name + "@" + PackageVersion.constraintToVersionString(self); }; -ConstraintSolver.Constraint.prototype.isSatisfied = function (candidateUV, - resolver) { +ConstraintSolver.Constraint.prototype.isSatisfied = function ( + candidateUV, resolver, resolveContext) { var self = this; check(candidateUV, ConstraintSolver.UnitVersion); + if (self.name !== candidateUV.name) { + throw Error("asking constraint on " + self.name + " about " + + candidateUV.name); + } + + if (self.type === "any-reasonable") { + // Non-prerelease versions are always reasonable. + if (!/-/.test(candidateUV.version)) + return true; + + // Is it a pre-release version that was explicitly mentioned at the top + // level? + if (_.has(resolveContext.topLevelPrereleases, self.name) && + _.has(resolveContext.topLevelPrereleases[self.name], + candidateUV.version)) { + return true; + } + + // Otherwise, not this pre-release! + return false; + } + + if (self.type === "exactly") { + return self.version === candidateUV.version; + } + + if (self.type !== "compatible-with") { + throw Error("Unknown constraint type: " + self.type); + } + // Pre-releases only match precisely; @1.2.3-rc1 doesn't necessarily match // 1.2.4, and @1.2.3 doesn't necessarily match 1.2.4-rc1. if (/-/.test(candidateUV.version) || /-/.test(self.version)) { return self.version === candidateUV.version; } - if (self.type === "exactly") - return self.version === candidateUV.version; - // If the candidate version is less than the version named in the constraint, // we are not satisfied. if (semver.lt(candidateUV.version, self.version)) return false; - // If we only care about "at-least" and not backwards-incompatible changes in - // the middle, then candidateUV is good enough. - if (self.type === "at-least") - return true; - var myECV = resolver.getEarliestCompatibleVersion(self.name, self.version); // If the constraint is "@1.2.3" and 1.2.3 doesn't exist, then nothing can // match. This is because we don't know the ECV (compatibility class) of @@ -395,17 +436,11 @@ ConstraintSolver.Constraint.prototype.isSatisfied = function (candidateUV, return myECV === candidateUV.earliestCompatibleVersion; }; -// Returns any unit version satisfying the constraint in the resolver -ConstraintSolver.Constraint.prototype.getSatisfyingUnitVersion = function ( - resolver) { +// An object that records the general context of a resolve call. It can be +// different for different resolve calls on the same Resolver, but is the same +// for every ResolverState in a given call. +var ResolveContext = function () { var self = this; - - if (self.type === "exactly") { - return resolver.getUnitVersion(self.name, self.version); - } - - // XXX this chooses a random UV, not the earliest or latest. Is that OK? - return _.find(resolver.unitsVersions[self.name], function (uv) { - return self.isSatisfied(uv, resolver); - }); + // unitName -> version string -> true + self.topLevelPrereleases = {}; }; diff --git a/packages/package-version-parser/package-version-parser-tests.js b/packages/package-version-parser/package-version-parser-tests.js index 30c0fbc1f3..e855069185 100644 --- a/packages/package-version-parser/package-version-parser-tests.js +++ b/packages/package-version-parser/package-version-parser-tests.js @@ -17,8 +17,8 @@ var FAIL = function (versionString) { Tinytest.add("Smart Package version string parsing - old format", function (test) { currentTest = test; - t("foo", { name: "foo", version: null, type: "compatible-with" }); - t("foo-1234", { name: "foo-1234", version: null, type: "compatible-with" }); + t("foo", { name: "foo", version: null, type: "any-reasonable" }); + t("foo-1234", { name: "foo-1234", version: null, type: "any-reasonable" }); FAIL("my_awesome_InconsitentPackage123"); }); @@ -37,6 +37,8 @@ Tinytest.add("Smart Package version string parsing - compatible version, compati FAIL("foo@x.y.z"); FAIL("foo@<1.2"); FAIL("foo<1.2"); + + t("foo", { name: "foo", version: null, type: "any-reasonable" }); }); Tinytest.add("Smart Package version string parsing - compatible version, exactly", function (test) { @@ -54,32 +56,10 @@ Tinytest.add("Smart Package version string parsing - compatible version, exactly FAIL("foo@=<1.2"); FAIL("foo@<=1.2"); FAIL("foo<=1.2"); -}); -Tinytest.add("Smart Package version string parsing - compatible version, at-least", function (test) { - var t = function (versionString, expected, descr) { - test.equal( - _.omit(PackageVersion.parseConstraint(versionString, - {allowAtLeast: true}), - 'constraintString'), - expected, - descr); - test.throws(function () { - PackageVersion.parseConstraint(versionString); - }); - }; - - var FAIL = function (versionString) { - test.throws(function () { - PackageVersion.parseConstraint(versionString, {allowAtLeast: true}); - }); - test.throws(function () { - PackageVersion.parseConstraint(versionString); - }); - }; - - t("foo@>=1.2.3", { name: "foo", version: "1.2.3", type: "at-least" }); - t("foo-bar@>=3.2.1", { name: "foo-bar", version: "3.2.1", type: "at-least" }); + // We no longer support @>=. + FAIL("foo@>=1.2.3"); + FAIL("foo-bar@>=3.2.1"); FAIL("42@>=0.2.0"); FAIL("foo@>=1.2.3.4"); FAIL("foo@>=1.4"); diff --git a/tools/catalog-base.js b/tools/catalog-base.js index 3cc8045376..e479f6ee2f 100644 --- a/tools/catalog-base.js +++ b/tools/catalog-base.js @@ -197,15 +197,20 @@ _.extend(baseCatalog.BaseCatalog.prototype, { // As getVersion, but returns info on the latest version of the // package, or null if the package doesn't exist or has no versions. - getLatestVersion: function (name) { + // It does not include prereleases (with dashes in the version); + getLatestMainlineVersion: function (name) { var self = this; self._requireInitialized(); buildmessage.assertInCapture(); var versions = self.getSortedVersions(name); - if (versions.length === 0) + versions.reverse(); + var latest = _.find(versions, function (version) { + return !/-/.test(version); + }); + if (!latest) return null; - return self.getVersion(name, versions[versions.length - 1]); + return self.getVersion(name, latest); }, // If this package has any builds at this version, return an array of builds diff --git a/tools/catalog.js b/tools/catalog.js index ea2469d427..4429545a03 100644 --- a/tools/catalog.js +++ b/tools/catalog.js @@ -453,7 +453,8 @@ _.extend(CompleteCatalog.prototype, { // Constraints for uniload should just be packages with no version // constraint and one local version (since they should all be in core). - if (!_.has(constraint, 'packageName') || _.size(constraint) !== 1) { + if (!_.has(constraint, 'packageName') || + constraint.type !== 'any-reasonable') { throw Error("Surprising constraint: " + JSON.stringify(constraint)); } if (!_.has(self.versions, constraint.packageName)) { @@ -499,9 +500,7 @@ _.extend(CompleteCatalog.prototype, { deps.push(constraint.packageName); } delete constraint.weak; - if (constraint.version) { - constr.push(constraint); - } + constr.push(constraint); }); // If we are called with 'ignore projectDeps', then we don't even look to @@ -519,6 +518,13 @@ _.extend(CompleteCatalog.prototype, { }); } + // Local packages can only be loaded from the version we have the source + // for: that's a weak exact constraint. + _.each(self.packageSources, function (packageSource, name) { + constr.push({packageName: name, version: packageSource.version, + type: 'exactly'}); + }); + var patience = new utils.Patience({ messageAfterMs: 1000, message: "Figuring out the best package versions to use. This may take a moment." @@ -1004,8 +1010,9 @@ _.extend(CompleteCatalog.prototype, { } }); } - // And put a build record for it in the catalog - var versionId = self.getLatestVersion(name); + // And put a build record for it in the catalog. There is only one version + // for this package! + var versionId = _.values(self.versions[name])._id; // XXX why isn't this build just happening through the package cache // directly? diff --git a/tools/commands-packages.js b/tools/commands-packages.js index 7bcf83b492..d573896b38 100644 --- a/tools/commands-packages.js +++ b/tools/commands-packages.js @@ -966,6 +966,7 @@ main.registerCommand({ return _.extend({ buildArchitectures: myStringBuilds }, versionRecord); }; + // XXX should this skip pre-releases? var versions = catalog.official.getSortedVersions(name); if (full.length > 1) { versions = [full[1]]; @@ -1081,7 +1082,7 @@ main.registerCommand({ var vr; doOrDie(function () { - vr = catalog.official.getLatestVersion(name); + vr = catalog.official.getLatestMainlineVersion(name); }); return vr && !vr.unmigrated; }; @@ -1118,7 +1119,7 @@ main.registerCommand({ _.each(allPackages, function (pack) { if (selector(pack, false)) { var vr = doOrDie(function () { - return catalog.official.getLatestVersion(pack); + return catalog.official.getLatestMainlineVersion(pack); }); if (vr) { matchingPackages.push( @@ -1200,8 +1201,12 @@ main.registerCommand({ } var versionAddendum = "" ; - var latest = catalog.complete.getLatestVersion(name, version); + var latest = catalog.complete.getLatestMainlineVersion(name, version); + var semver = require('semver'); if (version !== latest.version && + // If we're currently running a prerelease, "latest" may be older than + // what we're at, so don't tell us we're outdated! + semver.lt(version, latest.version) && !catalog.complete.isLocalPackage(name)) { versionAddendum = "*"; newVersionsAvailable = true; @@ -1738,6 +1743,24 @@ main.registerCommand({ } else { process.stdout.write("The version constraint will be removed.\n"); } + // Now remove the old constraint from what we're going to calculate + // with. + // This matches code in calculateCombinedConstraints. + var oldConstraint = _.extend( + {packageName: constraint.name}, + utils.parseVersionConstraint(packages[constraint.name])); + var removed = false; + for (var i = 0; i < allPackages.length; ++i) { + if (_.isEqual(oldConstraint, allPackages[i])) { + removed = true; + allPackages.splice(i, 1); + break; + } + } + if (!removed) { + throw Error("Couldn't find constraint to remove: " + + JSON.stringify(oldConstraint)); + } } } @@ -1761,33 +1784,44 @@ main.registerCommand({ } var downloaded, versions, newVersions; - var messages = buildmessage.capture(function () { - // Get the contents of our versions file. We need to pass them to the - // constraint solver, because our contract with the user says that we will - // never downgrade a dependency. - versions = project.getVersions(); - // Call the constraint solver. - newVersions = catalog.complete.resolveConstraints( - allPackages, - { previousSolution: versions }, - { ignoreProjectDeps: true }); - if ( ! newVersions) { - // XXX: Better error handling. - process.stderr.write("Cannot resolve package dependencies.\n"); - return; - } + try { + var messages = buildmessage.capture(function () { + // Get the contents of our versions file. We need to pass them to the + // constraint solver, because our contract with the user says that we will + // never downgrade a dependency. + versions = project.getVersions(); - // Don't tell the user what all the operations were until we finish -- we - // don't want to give a false sense of completeness until everything is - // written to disk. - var messageLog = []; + // Call the constraint solver. + newVersions = catalog.complete.resolveConstraints( + allPackages, + { previousSolution: versions }, + { ignoreProjectDeps: true }); + if ( ! newVersions) { + // XXX: Better error handling. + process.stderr.write("Cannot resolve package dependencies.\n"); + return; + } - // Install the new versions. If all new versions were installed - // successfully, then change the .meteor/packages and .meteor/versions to - // match expected reality. - downloaded = project.addPackages(constraints, newVersions); - }); + // Don't tell the user what all the operations were until we finish -- we + // don't want to give a false sense of completeness until everything is + // written to disk. + var messageLog = []; + + // Install the new versions. If all new versions were installed + // successfully, then change the .meteor/packages and .meteor/versions to + // match expected reality. + downloaded = project.addPackages(constraints, newVersions); + }); + } catch (e) { + if (!e.constraintSolverError) + throw e; + // XXX this is too many forms of error handling! + process.stderr.write( + "Could not satisfy all the specified constraints:\n" + + e + "\n"); + return 1; + } if (messages.hasMessages()) { process.stderr.write(messages.formatMessages()); return 1; diff --git a/tools/commands.js b/tools/commands.js index d060bff043..587d6bccf9 100644 --- a/tools/commands.js +++ b/tools/commands.js @@ -54,22 +54,6 @@ var hostedWithGalaxy = function (site) { return !! require('./deploy-galaxy.js').discoverGalaxy(site); }; -// Get all local packages available. Returns a map from the package name to the -// version record for that package. -var getLocalPackages = function () { - var ret = {}; - buildmessage.assertInCapture(); - - var names = catalog.complete.getAllPackageNames(); - _.each(names, function (name) { - if (catalog.complete.isLocalPackage(name)) { - ret[name] = catalog.complete.getLatestVersion(name); - } - }); - - return ret; -}; - /////////////////////////////////////////////////////////////////////////////// // options that act like commands @@ -950,18 +934,8 @@ main.registerCommand({ }, function (options) { var testPackages; if (options.args.length === 0) { - // Only test local packages if no package is specified. - // XXX should this use the new getLocalPackageNames? - var packageList = commandsPackages.doOrDie(function () { - return getLocalPackages(); - }); - if (! packageList) { - // Couldn't load the package list, probably because some package - // has a parse error. Bail out -- this kind of sucks; we would - // like to find a way to get reloading. - return 1; - } - testPackages = _.keys(packageList); + // Test all local packages if no package is specified. + testPackages = catalog.complete.getLocalPackageNames(); } else { var messages = buildmessage.capture(function () { testPackages = _.map(options.args, function (p) { @@ -978,16 +952,15 @@ main.registerCommand({ } // Check to see if this is a real package, and if it is a real // package, if it has tests. - var versionRec = catalog.complete.getLatestVersion(p); - if (!versionRec) { - buildmessage.error( - "Unknown package: " + p ); - } if (!catalog.complete.isLocalPackage(p)) { buildmessage.error( - "Not a local package, cannot test: " + p ); + "Not a known local package, cannot test: " + p ); return p; } + var versionNames = catalog.complete.getSortedVersions(p); + if (versionNames.length !== 1) + throw Error("local package should have one version?"); + var versionRec = catalog.complete.getVersion(p, versionNames[0]); if (versionRec && !versionRec.testName) { buildmessage.error( "There are no tests for package: " + p ); @@ -1058,7 +1031,10 @@ main.registerCommand({ var tests = []; var messages = buildmessage.capture(function () { _.each(testPackages, function(name) { - var versionRecord = catalog.complete.getLatestVersion(name); + var versionNames = catalog.complete.getSortedVersions(name); + if (versionNames.length !== 1) + throw Error("local package should have one version?"); + var versionRecord = catalog.complete.getVersion(name, versionNames[0]); if (versionRecord && versionRecord.testName) { tests.push(versionRecord.testName); } diff --git a/tools/compiler.js b/tools/compiler.js index 3f1145fb1c..fb5f7bd95e 100644 --- a/tools/compiler.js +++ b/tools/compiler.js @@ -179,10 +179,7 @@ var determineBuildTimeDependencies = function (packageSource, var constraints_array = []; _.each(dependencyMetadata, function (info, packageName) { constraints[packageName] = info.constraint; - var version = null; - if (info.constraint) { - version = utils.parseVersionConstraint(info.constraint) ; - } + var version = utils.parseVersionConstraint(info.constraint || '') ; constraints_array.push(_.extend({ packageName: packageName }, version)); }); @@ -230,12 +227,9 @@ var determineBuildTimeDependencies = function (packageSource, _.each(info.use, function (spec) { var parsedSpec = utils.splitConstraint(spec); constraints[parsedSpec.package] = parsedSpec.constraint || null; - var version = null; - if (parsedSpec.constraint) { - version = utils.parseVersionConstraint(info.constraint) ; - } - constraints_array.push({packageName: parsedSpec.package, - version: version }); + var version = utils.parseVersionConstraint(info.constraint || ''); + constraints_array.push(_.extend({packageName: parsedSpec.package}, + version)); }); var pluginVersion = pluginVersions[info.name] || {}; diff --git a/tools/package-version-parser.js b/tools/package-version-parser.js index 8e7827e81a..d7f9238333 100644 --- a/tools/package-version-parser.js +++ b/tools/package-version-parser.js @@ -23,32 +23,28 @@ var __ = inTool ? require('underscore') : _; // 2. "exactly" - A@=x.y.z - constraints package A only to version x.y.z and // nothing else. // "pick A exactly at x.y.z" -// 3. "at-least" - A@>=x.y.z - constraints package A to version x.y.z or higher. -// "pick A at least at x.y.z" -// This one is only used internally by the constraint solver --- end users -// shouldn't be allowed to specify it, and you need to specially request it -// with the "allowAtLeast" option. +// 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). PV.parseVersionConstraint = function (versionString, options) { options = options || {}; - var versionDesc = { version: null, type: "compatible-with", + var versionDesc = { version: null, type: "any-reasonable", constraintString: versionString }; - if (versionString === "none" || versionString === null) { - versionDesc.type = "at-least"; - versionDesc.version = "0.0.0"; + if (!versionString) { return versionDesc; } if (versionString.charAt(0) === '=') { versionDesc.type = "exactly"; versionString = versionString.substr(1); - } else if (options.allowAtLeast && versionString.substr(0, 2) === '>=') { - versionDesc.type = "at-least"; - versionString = versionString.substr(2); + } else { + versionDesc.type = "compatible-with"; } - // XXX check for a dash in the version in case of foo@1.2.3-rc0 - if (! semver.valid(versionString)) { throwVersionParserError( "Version string must look like semver (eg '1.2.3'), not '" @@ -68,7 +64,7 @@ PV.parseConstraint = function (constraintString, options) { var splitted = constraintString.split('@'); var constraint = { name: "", version: null, - type: "compatible-with", constraintString: null }; + type: "any-reasonable", constraintString: null }; var name = splitted[0]; var versionString = splitted[1]; @@ -124,3 +120,20 @@ var throwVersionParserError = function (message) { e.versionParserError = true; throw e; }; + +// XXX if we were better about consistently only using functions in this file, +// we could just do this using the constraintString field +PV.constraintToVersionString = function (parsedConstraint) { + if (parsedConstraint.type === "any-reasonable") + return ""; + if (parsedConstraint.type === "compatible-with") + return parsedConstraint.version; + if (parsedConstraint.type === "exactly") + return "=" + parsedConstraint.version; + throw Error("Unknown constraint type: " + parsedConstraint.type); +}; + +PV.constraintToFullString = function (parsedConstraint) { + return parsedConstraint.name + "@" + PV.constraintToVersionString( + parsedConstraint); +}; diff --git a/tools/project.js b/tools/project.js index 3fdeac7bc0..dd323c81d4 100644 --- a/tools/project.js +++ b/tools/project.js @@ -234,6 +234,7 @@ _.extend(Project.prototype, { var allDeps = []; // First, we process the contents of the .meteor/packages file. The // self.constraints variable is always up to date. + // Note that two parts of the "add" command run code that matches this. _.each(self.constraints, function (constraint, packageName) { allDeps.push(_.extend({packageName: packageName}, utils.parseVersionConstraint(constraint))); @@ -281,7 +282,7 @@ _.extend(Project.prototype, { // someday, this will make sense. (The conditional here allows us to work // in tests with releases that have no packages.) if (catalog.complete.getPackage("ctl")) { - allDeps.push({packageName: "ctl", version: null }); + allDeps.push({packageName: "ctl", version: null, type: 'any-reasonable'}); } return allDeps; diff --git a/tools/selftest.js b/tools/selftest.js index 6f4116d12d..5603ae6de0 100644 --- a/tools/selftest.js +++ b/tools/selftest.js @@ -713,7 +713,7 @@ _.extend(Sandbox.prototype, { ['autopublish', 'standard-app-packages', 'insecure'], function (name) { var versionRec = doOrThrow(function () { - return catalog.official.getLatestVersion(name); + return catalog.official.getLatestMainlineVersion(name); }); if (!versionRec) { catalog.official.offline = false; @@ -722,7 +722,7 @@ _.extend(Sandbox.prototype, { }); catalog.official.offline = true; versionRec = doOrThrow(function () { - return catalog.official.getLatestVersion(name); + return catalog.official.getLatestMainlineVersion(name); }); if (!versionRec) { throw new Error(" hack fails for " + name); diff --git a/tools/tests/packages/package-of-two-versions/packagerc.js b/tools/tests/packages/package-of-two-versions/packagerc.js new file mode 100644 index 0000000000..1a8172641b --- /dev/null +++ b/tools/tests/packages/package-of-two-versions/packagerc.js @@ -0,0 +1,4 @@ +Package.describe({ + summary: "Test package.", + version: "1.0.4-rc3" +}); diff --git a/tools/tests/publish.js b/tools/tests/publish.js index 63c43192cc..18104a6f07 100644 --- a/tools/tests/publish.js +++ b/tools/tests/publish.js @@ -172,7 +172,7 @@ selftest.define("list-with-a-new-version", run = s.run("list"); run.waitSecs(10); run.match(fullPackageName); - run.match("1.0.0"); + run.match("1.0.0 "); run.forbidAll("New versions"); run.expectExit(0); }); @@ -194,6 +194,74 @@ selftest.define("list-with-a-new-version", run.match("New versions"); run.match("meteor update"); run.expectExit(0); + + // Switch to the other version. + run = s.run("add", fullPackageName + "@1.0.1"); + run.waitSecs(100); + run.expectExit(0); + run = s.run("list"); + run.waitSecs(10); + run.match(fullPackageName); + run.match("1.0.1 "); + run.forbidAll("New versions"); + run.expectExit(0); + + // Switch back to the first version. + run = s.run("add", fullPackageName + "@=1.0.0"); + run.waitSecs(100); + run.expectExit(0); + run = s.run("list"); + run.waitSecs(10); + run.match(fullPackageName); + run.match("1.0.0*"); + run.match("New versions"); + run.match("meteor update"); + run.expectExit(0); + + // ... and back to the second version + run = s.run("add", fullPackageName + "@=1.0.1"); + run.waitSecs(100); + run.expectExit(0); + run = s.run("list"); + run.waitSecs(10); + run.match(fullPackageName); + run.match("1.0.1 "); + run.forbidAll("New versions"); + run.expectExit(0); }); + // Now publish an 1.0.4-rc4. + s.cp(fullPackageName+'/packagerc.js', fullPackageName+'/package.js'); + s.cd(fullPackageName, function () { + run = s.run("publish"); + run.waitSecs(15); + run.expectExit(0); + run.match("Done"); + }); + + s.cd('mapp', function () { + // // + // run = s.run("search", "asdf"); + // run.waitSecs(100); + // run.expectExit(0); + + // Because it's an RC, we shouldn't see an update message. + run = s.run("list"); + run.waitSecs(10); + run.match(fullPackageName); + run.match("1.0.1 "); + run.forbidAll("New versions"); + run.expectExit(0); + + // It works if ask for it, though. + run = s.run("add", fullPackageName + "@1.0.4-rc3"); + run.waitSecs(100); + run.expectExit(0); + run = s.run("list"); + run.waitSecs(10); + run.match(fullPackageName); + run.match("1.0.4-rc3 "); + run.forbidAll("New versions"); + run.expectExit(0); + }); }); diff --git a/tools/unipackage.js b/tools/unipackage.js index a6f09dc1d6..2891fe5508 100644 --- a/tools/unipackage.js +++ b/tools/unipackage.js @@ -307,9 +307,19 @@ _.extend(Unipackage.prototype, { // An sorted array of all the architectures included in this package. architectures: function () { var self = this; - var arches = _.uniq( - _.pluck(self.unibuilds, 'arch').concat(self._toolArchitectures()) - ).sort(); + var archSet = {}; + _.each(self.unibuilds, function (unibuild) { + archSet[unibuild.arch] = true; + }); + _.each(self._toolArchitectures(), function (arch) { + archSet[arch] = true; + }); + _.each(self.plugins, function (plugin, name) { + _.each(plugin, function (plug, arch) { + archSet[arch] = true; + }); + }); + var arches = _.keys(archSet).sort(); // Ensure that our buildArchitectures string does not look like // web+os+os.osx.x86_64 // This would happen if there is an 'os' unibuild but a platform-specific