diff --git a/tools/commands-packages.js b/tools/commands-packages.js index 37e16f412c..01c4c90e6b 100644 --- a/tools/commands-packages.js +++ b/tools/commands-packages.js @@ -711,3 +711,270 @@ main.registerCommand({ } process.stdout.write(formatList(items)); }); + + + +/////////////////////////////////////////////////////////////////////////////// +// update +/////////////////////////////////////////////////////////////////////////////// + +main.registerCommand({ + name: 'update', + options: { + patch: { type: Boolean, required: false }, + "packages-only": { 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. + requiresRelease: false, + minArgs: 0, + maxArgs: Infinity, +}, function (options) { + // XXX clean this up if we don't end up using it, but we probably should be + // using it on the refresh call + var couldNotContactServer = false; + + // Refresh the catalog, cacheing the remote package data on the server. + catalog.official.refresh(true); + + // If you are specifying packaging individually, you probably don't want to + // update the release. + if (options.args.length > 0) { + options["packages-only"] = true; + } + + if (!options["packages-only"]) { + + // refuse to update the release if we're in a git checkout. + if (! files.usesWarehouse()) { + process.stderr.write( + "update: can only be run from official releases, not from checkouts\n"); + return 1; + } + + // This is the release track we'll end up on --- either because it's + // the explicitly specified (with --release) track; or because we + // didn't specify a release and it's the app's current release (if we're + // in an app dir), since non-forced updates don't change the track. + // XXX better error checking on release.current.name + // XXX add a method to release.current + var releaseTrack = release.current.getReleaseTrack(); + + // Unless --release was passed (in which case we ought to already have + // springboarded to that release), go get the latest release and switch to + // it. (We already know what the latest release is because we refreshed the + // catalog above.) Note that after springboarding, we will hit this again + // (because springboarding to a specific release does NOT set release.forced), + // but it should be a no-op next time (unless there actually was a new latest + // release in the interim). + if (! release.forced) { + var latestRelease = release.latestDownloaded(releaseTrack); + // Are we on some track without ANY recommended releases at all, + // and the user ran 'meteor update' without specifying a release? We + // really can't do much here. + if (!latestRelease) { + // XXX is there a command to get to the latest METEOR-CORE@? Should we + // recommend it here? + process.stderr.write( + "There are no recommended releases on release track " + + releaseTrack + ".\n"); + return 1; + } + if (! release.current || release.current.name !== latestRelease) { + // The user asked for the latest release (well, they "asked for it" by not + // passing --release). We're not currently running the latest release on + // this track (we may have even just learned about it). #UpdateSpringboard + throw new main.SpringboardToLatestRelease(releaseTrack); + } + } + + // At this point we should have a release. (If we didn't to start + // with, #UpdateSpringboard fixed that.) And it can't be a checkout, + // because we checked for that at the very beginning. + if (! release.current || ! release.current.isProperRelease()) + throw new Error("don't have a proper release?"); + + // If we're not in an app, then we're done (other than maybe printing some + // stuff). + if (! options.appDir) { + if (release.forced || process.env.METEOR_SPRINGBOARD_RELEASE) { + // We get here if: + // 1) the user ran 'meteor update' and we found a new version + // 2) the user ran 'meteor update --release xyz' (regardless of + // whether we found a new release) + // + // In case (1), we downloaded and installed the update and then + // we springboarded (at #UpdateSpringboard above), causing + // $METEOR_SPRINGBOARD_RELEASE to be true. + // XXX probably should have a better interface than looking directly + // at the env var here + // + // In case (2), we downloaded, installed, and springboarded to + // the requested release in the initialization code, before the + // command even ran. They could equivalently have run 'meteor + // help --release xyz'. + console.log( + "Installed. Run 'meteor update' inside of a particular project\n" + + "directory to update that project to Meteor %s.", release.current.name); + } else { + // We get here if the user ran 'meteor update' and we didn't + // find a new version. + + if (couldNotContactServer) { + // We already printed an error message about our inability to + // ask the server if we're up to date. + } else { + console.log( + "The latest version of Meteor, %s, is already installed on this\n" + + "computer. Run 'meteor update' inside of a particular project\n" + + "directory to update that project to Meteor %s.", + release.current.name, release.current.name); + } + } + return; + } + + // Otherwise, we have to upgrade the app too, if the release changed. + var appRelease = project.getMeteorReleaseVersion(); + if (appRelease !== null && appRelease === release.current.name) { + var maybeTheLatestRelease = release.forced ? "" : ", the latest release"; + var maybeOnThisComputer = + couldNotContactServer ? "\ninstalled on this computer" : ""; + console.log( + "This project is already at Meteor %s%s%s.", + appRelease, maybeTheLatestRelease, maybeOnThisComputer); + return; + } + + // XXX: also while we are at it, we should consider disallowing both + // options.patch and release.forced. Otherwise, the behavior is... what I had + // to use to test this, actually ( update --patch --release + // ekate-meteor@5.0.13 updated me to ekate-meteor@5.0.13.1) but that's way too + // confusing to make sense. + + + // XXX did we have to change some package versions? we should probably + // mention that fact. + // XXX error handling. + var releaseVersionsToTry; + if (options.patch) { + // XXX: something something something current release + if (appRelease == null) { + console.log( + "Cannot patch update unless a release is set."); + process.exit(1); + } + var r = appRelease.split('@'); + var record = catalog.official.getReleaseVersion(r[0], r[1]); + var updateTo = record.patchReleaseVersion; + if (!updateTo) { + console.log( + "You are at the latest patch version."); + process.exit(1); + } + releaseVersionsToTry = [updateTo]; + } else if (release.forced) { + releaseVersionsToTry = [release.current.getReleaseVersion()]; + } else { + // XXX clean up all this splitty stuff + var appReleaseInfo = catalog.official.getReleaseVersion( + appRelease.split('@')[0], appRelease.split('@')[1]); + var appOrderKey = (appReleaseInfo && appReleaseInfo.orderKey) || null; + releaseVersionsToTry = catalog.official.getSortedRecommendedReleaseVersions( + releaseTrack, appOrderKey); + if (!releaseVersionsToTry.length) { + // XXX make error better, and make sure that the "already there" error + // above truly does cover every other case + var maybeOnThisComputer = + couldNotContactServer ? "\ninstalled on this computer" : ""; + console.log( + "This project is already at Meteor %s, which is newer than the latest release%s.", + appRelease, maybeOnThisComputer); + return; + } + } + + var solutionPackageVersions = null; + var directDependencies = project.getConstraints(); + var previousVersions = project.getVersions(); + var solutionReleaseVersion = _.find(releaseVersionsToTry, function (versionToTry) { + var releaseRecord = catalog.complete.getReleaseVersion(releaseTrack, versionToTry); + if (!releaseRecord) + throw Error("missing release record?"); + var constraints = project.calculateCombinedConstraints( + directDependencies, releaseRecord.packages); + try { + solutionPackageVersions = catalog.complete.resolveConstraints( + constraints, { previousSolution: previousVersions }); + } catch (e) { + // XXX we should make the error handling explicitly detectable, and not + // actually mention failures that are recoverable + process.stderr.write( + "XXX Update to release " + releaseTrack + + "@" + versionToTry + " impossible: " + e.message + "\n"); + return false; + } + return true; + }); + + if (!solutionReleaseVersion) { + // XXX put an error here when we stop doing an error on every failure above + return 1; + } + + var solutionReleaseName = releaseTrack + '@' + solutionReleaseVersion; + + // We could at this point springboard to solutionRelease (which is no newer + // than the release we are currently running), but there's no clear advantage + // to this yet. The main reason might be if we decide to delete some + // backward-compatibility code which knows how to deal with an older release, + // but if we actually do that, we can change this code to add the extra + // springboard at that time. + + var upgraders = require('./upgraders.js'); + var upgradersToRun = upgraders.upgradersToRun(); + + // XXX did we have to change some package versions? we should probably + // mention that fact. + + // Write the new versions to .meteor/packages and .meteor/versions. + project.setVersions(solutionPackageVersions); + + // Write the release to .meteor/release. + project.writeMeteorReleaseVersion(solutionReleaseName); + + console.log("%s: updated to Meteor %s.", + path.basename(options.appDir), solutionReleaseName); + + // Now run the upgraders. + // XXX should we also run upgraders on other random commands, in case there + // was a crash after changing .meteor/release but before running them? + _.each(upgradersToRun, function (upgrader) { + upgraders.runUpgrader(upgrader); + project.appendFinishedUpgrader(upgrader); + }); + } + + // Update the packages to the latest version. We don't do this for patch + // releases. + // + // XXX: Can we figure out if we got here with update --release foo, + // or just with update? + if (options['packages-only'] && !options['patch']) { + // We can't update packages when we are not in a release. + if (!options.appDir) return 0; + + // Let's update packages to the latest version. That's easy. + var versions = project.getVersions(); + var allPackages = project.getCurrentCombinedConstraints(); + + // XXX: Updating individual packages and the constraint solver + var newVersions = catalog.complete.resolveConstraints(allPackages, { + previousSolution: versions, + breaking: !options.minor, + upgrade: true + }); + project.setVersions(newVersions); + process.exit(0); + } +});