Files
meteor/tools/main.js
David Glasser 7743a55bba Use official catalog for springboarding
Read it into memory at initialize time
2014-08-13 18:11:46 -07:00

1255 lines
47 KiB
JavaScript

var showRequireProfile = ('METEOR_PROFILE_REQUIRE' in process.env);
if (showRequireProfile)
require('./profile-require.js').start();
var _ = require('underscore');
var Fiber = require('fibers');
var files = require('./files.js');
var path = require('path');
var warehouse = require('./warehouse.js');
var tropohouse = require('./tropohouse.js');
var release = require('./release.js');
var project = require('./project.js');
var fs = require('fs');
var catalog = require('./catalog.js');
var buildmessage = require('./buildmessage.js');
var main = exports;
// node (v8) defaults to only recording 10 lines of stack trace. This
// in especially insufficient when using fibers, because you get
// proper call stacks instead of only seeing the stack up to the most
// recent callback invocation. Increase the limit (for the `meteor` tool
// itself, not for apps).
//
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
Error.stackTraceLimit = Infinity;
///////////////////////////////////////////////////////////////////////////////
// Command registration
///////////////////////////////////////////////////////////////////////////////
var Command = function (options) {
options = _.extend({
minArgs: 0,
options: {},
requiresApp: false,
requiresRelease: true,
hidden: false
}, options);
if (! _.has(options, 'maxArgs'))
options.maxArgs = options.minArgs;
_.each(["name", "func"], function (key) {
if (! _.has(options, key))
throw new Error("command missing '" + key + "'?");
});
_.extend(this, options);
_.each(this.options, function (value, key) {
if (key === "args" || key === "appDir")
throw new Error(options.name + ": bad option name " + key);
if (! _.has(value, 'type'))
value.type = String;
if (_.has(value, 'default') && _.has(value, 'required'))
throw new Error(options.name + ": " + key + " can't be both optional " +
"and required");
if (_.has(value, 'short') && value.short.length !== 1)
throw new Error(options.name + ": " + key + " has a bad short option");
});
};
// map from command name to a Command, or to a subcommand map (a map
// of subcommand names to either Commands or further submaps).
//
// Options that function as commands (eg, "meteor --arch") are treated
// as subcommands of "--".
var commands = {};
// map from full command name ('deploy' or 'admin grant') to
// - description: one-line help message, for use in command list
// - usage: full usage help. ends with a newline but no blank lines
var messages = {};
// Exception to throw from a command to bail out and show command
// usage information.
main.ShowUsage = function () {};
// Exception to throw from a helper function inside a command which is identical
// to returning the given exit code from the command. ONLY USE THIS IN HELPERS
// THAT ARE ONLY CALLED DIRECTLY FROM COMMANDS! DON'T BE LAZY AND PUT THROW OF
// THIS IN RANDOM LIBRARY CODE!
main.ExitWithCode = function (code) {
this.code = code;
};
// Exception to throw to skip the process.exit call.
main.WaitForExit = function () {};
// Exception to throw from a command to exit, restart, and reinvoke
// the command with the latest available (downloaded) Meteor release.
// If track is specified, it uses the latest available in the given
// track instead of the default track.
main.SpringboardToLatestRelease = function (track) {
this.track = track;
};
// Register a command-line command.
//
// options:
// - name
// - can be a basic command, like "deploy"
// - can be a subcommand, like "admin grant"
// (distinguished by presence of ' ')
// - can be an option that functions as a command, like "--arch"
// (distinguished by starting with '--')
// - minArgs: minimum non-option arguments that can be present (default 0)
// - maxArgs: maximum non-option arguments that can be present (defaults to
// whatever value you passed for minArgs; use Infinity for unlimited)
// - options: map from long option name to:
// - type: String, Number, or Boolean. default is String. a future
// version could support [String] and [Number] to allow the option to
// be passed more than once, but we don't do that yet.
// - short: single character short alias (eg, 'p' for 'port', to do -p 3000)
// - default: value to use if none supplied
// - required: true if required (incompatible with 'default')
// - requiresApp: does this command work with an app? possible values
// (defaults to false):
// - true if an app is required, and command must be run inside an
// app. The command will be run using the app's Meteor release
// (unless overridden by --release or a checkout). An 'appDir'
// option will be passed with the absolute path to the app's
// top-level directory, and an error will be printed if the
// command isn't run from inside an app.
// - false if an app is not required. But if the command does happen
// to have been run from an app, 'appDir' will be
// provided. Moreover, in that case, we will still use the version
// of this program that goes with the Meteor release of the
// app. This is not ideal but is necessary for 'meteor help' to
// behave in a sane way in our current system. (XXX In the future
// we should separate the build system out into a package that is
// versioned with the release, and then take the CLI tool out of
// the release and always use the latest available version.)
// - function: some apps determine whether they use an app based on
// their arguments (eg, 'deploy' versus 'deploy --delete'). for
// these, set requiresApp to a function that takes 'options' (same as
// would be received by the actual command function) and returns
// true or false.
// - requiresRelease: defaults to true. Set to false if this command
// doesn't need a functioning Meteor release to be available (that
// is, if the command does not need the ability to resolve
// packages). There is only one case where this comes up: if you
// create an app with a checkout (so that it has no release), and
// then run that app with released Meteor. Normally this just prints
// an error saying that you have to pick a release, but you can
// disable that by setting this flag to false. Even if you set this
// flag, we will still *attempt* to run the correct Meteor release
// just like we always do; it's just that in that one case, instead
// of bailing out with an error we will run your command with
// release.current === null.
// - hidden: do not show in command list in help
//
// An error will be printed if an unrecognized option is passed on the
// command line (eg, '--foo' when you don't have a 'foo' key in
// options.options), or a required option is missing, or the number of
// other arguments isn't as required by minArgs / maxArgs.
//
// func: function to call when the command is chosen. receives one
// argument, an options dictionary that contains:
// - the values of any 'options' that were provided
// - args: an array of the other command-line arguments
// - appDir: if run from inside an app tree, the absolute path to the
// app's top-level directory
//
// func should do one of the following:
// - On success, return undefined (or 0). This indicates successful
// completion, and the program will exit with status 0.
// - On failure, return a positive number. The program will exit with that
// status.
// - If the command-line arguments aren't valid, 'throw new
// main.ShowUsage'. This will print usage info for the command and
// exit with status 1.
// - If you have started (for example) a subprocess or worker fiber
// and want to wait until it's finished to exit, 'throw new
// main.WaitForExit'. This will skip the call to process.exit and the
// program will keep running until node thinks that everything is
// done.
// - To quit, restart, and rerun the command with a latest available
// (downloaded) Meteor release, 'throw new main.SpringboardToLatestRelease'.
//
// Commands should never call process.exit()! They should instead
// return an appropriate value.
main.registerCommand = function (options, func) {
options = _.clone(options);
options.func = func;
var nameParts = options.name.trim().split(/\s+/);
options.name = nameParts.join(' ');
if (nameParts[0].indexOf('--') === 0) {
// "--foo" -> "--" "foo"
nameParts[0] = nameParts[0].substr(2);
nameParts.unshift('--');
}
var target = commands;
while (nameParts.length > 1) {
var part = nameParts.shift();
if (! _.has(target, part))
target[part] = {};
target = target[part];
}
if (_.has(target, nameParts[0])) {
throw Error("Duplicate command: " + options.name);
}
target[nameParts[0]] = new Command(options);
};
///////////////////////////////////////////////////////////////////////////////
// Load all the commands
///////////////////////////////////////////////////////////////////////////////
// NB: files required up to this point may not define commands
require('./commands.js');
require('./commands-packages.js');
///////////////////////////////////////////////////////////////////////////////
// Long-form help
///////////////////////////////////////////////////////////////////////////////
// Returns an array of entries with keys:
// - name (entry name, typically a command name)
// - body (contents of body, trimmed to end with a newline but no blank lines)
var loadHelp = function () {
var ret = [];
var raw = fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf8');
return _.map(raw.split(/^>>>/m).slice(1), function (r) {
var lines = r.split('\n');
var name = lines.shift().trim();
return {
name: name,
body: lines.join('\n').replace(/\s*$/, '') + '\n'
};
});
};
var longHelp = exports.longHelp = function (commandName) {
commandName = commandName.trim();
var parts = commandName.length ? commandName.split(' ') : [];
var node = commands;
_.each(parts, function (part) {
if (! _.has(node, part))
throw new Error("walked off edge of command tree?");
node = node[part];
});
var help = loadHelp();
var commandList = null;
if (! (node instanceof Command)) {
commandList = '';
var items = [];
var commandsWanted = {};
_.each(node, function (n, shortName) {
var fullName = commandName + (commandName.length > 0 ? " " : "") +
shortName;
// For now, we don't include commands with subcommands in the
// list -- if you have a command 'admin grant' then 'admin' does
// not appear in the top-level help. If we one day want to make
// these kinds of commands visible to casual users, we'll need a
// way to mark them as visible or hidden.
if (n instanceof Command && ! n.hidden)
commandsWanted[fullName] = { name: shortName };
});
var maxNameLength = _.max(_.map(commandsWanted, function (c) {
return c.name.length;
}));
// Assemble help text for subcommands.. in the order they appear
// in the help file
_.each(help, function (helpEntry) {
if (_.has(commandsWanted, helpEntry.name)) {
var shortName = commandsWanted[helpEntry.name].name;
commandList += " " + shortName +
new Array(maxNameLength + 1).join(' ').substr(shortName.length) +
" " + helpEntry.body.split('\n')[0] + "\n";
}
});
// Remove trailing newline so that you can write "{{commands}}" on
// a line by itself and it does what you think it would
commandList = commandList.substr(0, commandList.length - 1);
}
var entry = _.find(help, function (c) {
return c.name === commandName;
});
if (! entry)
throw new Error("help missing for " + commandName + "?");
var ret = entry.body.split('\n').slice(1).join('\n');
if (commandList !== null)
ret = ret.replace('{{commands}}', commandList);
return ret;
};
///////////////////////////////////////////////////////////////////////////////
// Springboarding
///////////////////////////////////////////////////////////////////////////////
// Exit and restart the program, with the same arguments, but using a
// different version of the tool and/or forcing a particular release.
//
// - toolsVersion: required. the version of the tool to run. must
// already be downloaded.
// - releaseOverride: optional. if provided, a release name to force
// us to use when restarting (this functions exactly like --release
// and will cause release.forced to be true).
var springboard = function (rel, releaseOverride) {
if (process.env.METEOR_DEBUG_SPRINGBOARD)
console.log("WILL SPRINGBOARD TO", rel.getToolsPackageAtVersion());
var archinfo = require('./archinfo.js');
var unipackage = require('./unipackage.js');
var toolsPkg = rel.getToolsPackage();
var toolsVersion = rel.getToolsVersion();
// XXX split better
try {
var messages = buildmessage.capture({
title: "downloading tools package " + toolsPkg + "@" + toolsVersion
}, function () {
tropohouse.default.maybeDownloadPackageForArchitectures({
packageName: toolsPkg,
version: toolsVersion,
architectures: [archinfo.host()]
});
});
} catch (err) {
// We have failed to download the tool that we are supposed to springboard
// to! That's bad. Let's exit.
process.stderr.write(
"Could not springboard to release: " + rel.name +
": could not download tool in " +
rel.getToolsPackageAtVersion() + "\n");
process.exit(1);
}
if (messages.hasMessages()) {
process.stderr.write(
"Could not springboard to release: " + rel.name + ".\n" +
messages.formatMessages());
process.exit(1);
}
var packagePath = tropohouse.default.packagePath(toolsPkg, toolsVersion);
var toolUnipackage = new unipackage.Unipackage;
toolUnipackage.initFromPath(toolsPkg, packagePath);
var toolRecord = _.findWhere(toolUnipackage.toolsOnDisk,
{arch: archinfo.host()});
if (!toolRecord)
throw Error("missing tool for " + archinfo.host() + " in " +
toolsPkg + "@" + toolsVersion);
var executable = path.join(packagePath, toolRecord.path, 'meteor');
// Strip off the "node" and "meteor.js" from argv and replace it with the
// appropriate tools's meteor shell script.
var newArgv = process.argv.slice(2);
if (releaseOverride !== undefined) {
// We used to just append --release=<releaseOverride> to the arguments, and
// though that's probably safe in practice, it makes us worry about things
// like other --release options. So now we use an environment
// variable. #SpringboardEnvironmentVar
process.env['METEOR_SPRINGBOARD_RELEASE'] = releaseOverride;
}
// Now exec; we're not coming back.
require('kexec')(executable, newArgv);
throw Error('exec failed?');
};
// Springboard to a pre-0.9.0 release.
var oldSpringboard = function (toolsVersion) {
// Strip off the "node" and "meteor.js" from argv and replace it with the
// appropriate tools's meteor shell script.
var newArgv = process.argv.slice(2);
var cmd = path.join(warehouse.getToolsDir(toolsVersion), 'bin', 'meteor');
// Now exec; we're not coming back.
require('kexec')(cmd, newArgv);
throw Error('exec failed?');
};
///////////////////////////////////////////////////////////////////////////////
// Main entry point
///////////////////////////////////////////////////////////////////////////////
// This is the main function that runs when you type 'meteor'.
// It's mostly concerned with validating command-line arguments,
// finding the requested command in the commands table, and making
// sure that you're using the version of the Meteor tools that match
// your project.
Fiber(function () {
// If running inside the Emacs shell, set stdin to be blocking,
// reversing node's normal setting of O_NONBLOCK on the evaluation
// of process.stdin (because Node unblocks stdio when forking). This
// fixes execution of Mongo from within Emacs shell.
if (process.env.EMACS == "t") {
process.stdin;
var child_process = require('child_process');
child_process.spawn('true', [], {stdio: 'inherit'});
}
// Check required Node version.
// This code is duplicated in tools/server/boot.js.
var MIN_NODE_VERSION = 'v0.10.29';
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
process.stderr.write(
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
process.exit(1);
}
// This is a bit of a hack, but: if we don't check this in the tool, then the
// first time we do a unipackage.load, it will fail due to the check in the
// meteor package, and that'll look a lot uglier.
if (process.env.ROOT_URL) {
var parsedUrl = require('url').parse(process.env.ROOT_URL);
if (!parsedUrl.host) {
process.stderr.write('$ROOT_URL, if specified, must be an URL.\n');
process.exit(1);
}
}
// Parse the arguments.
//
// We must first identify which options are boolean and which take
// arguments (which must be consistent across all defined
// commands). This is necessary to resolve cases like 'meteor --flag
// stuff thing'. Is the command 'stuff' with a boolean option
// 'flag', or in the command 'thing' with an option 'flag' that is
// set to 'stuff'? To resolve this we require that 'flag' be
// consistently declared as a boolean (or not a boolean) across all
// commands.
//
// XXX The problem with the above is that which commands are boolean
// may change across releases, and when we springboard, we actually
// have to parse the options with the *target* version's
// semantics. All in all, I think we might be better served to
// require options to come after the command, other than special
// options (--release, --help, and options that act as
// commands). Then we don't have to require consistency of boolean
// status between commands; we instead have to require consistency
// of boolean status of a particular option, for a command, across
// releases. Since we always start out by running the latest version
// of Meteor, which can have knowledge of all past versions
// (including the boolean status of formerly present but removed
// options, including options to removed commands), this should let
// us be 100% correct. (Of course, we could still do this if we
// required options to be consistent across commands as well, but I
// think this is a better tradeoff.) In this model, we'd do option
// parsing in two passes, where the first pass just pulls out the
// command, and the second parses the arguments with knowledge of
// the command. I would make this change right now but we're on a
// tight timetable for 1.0 and there is no advantage to doing it now
// rather than later. #ImprovingCrossVersionOptionParsing
var isBoolean = { "--help": true };
var walkCommands = function (node) {
_.each(node, function (value, key) {
if (value instanceof Command) {
_.each(value.options || {}, function (optionInfo, optionName) {
var names = ["--" + optionName];
if (_.has(optionInfo, 'short'))
names.push("-" + optionInfo.short);
_.each(names, function (name) {
var optionIsBoolean = (optionInfo.type === Boolean);
if (_.has(isBoolean, name)) {
if (isBoolean[name] !== optionIsBoolean) {
throw new Error("conflict: option '" + name + "' is used " +
"both as a boolean and as another type");
}
} else {
isBoolean[name] = optionIsBoolean;
}
});
});
} else {
walkCommands(value);
}
});
};
walkCommands(commands);
// This is for things like '--arch' and '--version' which look like
// options, but actually function pretty much like commands. That's
// a little weird but it feels good and it follows a grand Unix
// tradition.
_.each(commands['--'] || {}, function (value, key) {
if (_.has(isBoolean, "--" + key))
throw new Error("--" + key + " is both an option and a command?");
isBoolean["--" + key] = true;
});
// Now parse!
var argv = process.argv.slice(2);
var rawOptions = {}; // map from '--foo' or '-f' to array of values
var rawArgs = [];
for (var i = 0; i < argv.length; i++) {
var term = argv[i];
// --: stop-parsing marker
if (term === "--") {
// Remainder is unparsed
rawArgs = rawArgs.concat(argv.slice(i + 1));
break;
}
// -: just an argument named '-'
if (term === "-") {
rawArgs.push(term);
continue;
}
if (term.match(/^--?=/)) {
process.stderr.write("Option names cannot begin with '='.\n");
process.exit(1);
}
// A single option, like --foo or -f
if (term.match(/^--/) || term.match(/^-.$/)) {
var value = undefined;
// Split the term (once only!) on an equal sign.
var equals = term.indexOf('=');
if (equals !== -1) {
value = term.substr(equals + 1);
term = term.substr(0, equals);
}
if (! _.has(rawOptions, term))
rawOptions[term] = [];
// Save off the value of the option. true for (known) booleans,
// null if value is missing, else a string. Don't try to
// validate or interpret it yet.
if (isBoolean[term]) {
// If we got an '=' for a boolean, this is an error, which will be
// printed prettily later if we push false here.
rawOptions[term].push(value === undefined);
} else if (value !== undefined) {
// Handle '--foo=bar' and '--foo=' (which means "set to empty string").
rawOptions[term].push(value);
} else if (i === argv.length - 1) {
rawOptions[term].push(null);
} else {
rawOptions[term].push(argv[i + 1]);
i ++;
}
continue;
}
// Compound short option ('-abc', '-p45', '-abcp45')? Rewrite it
// in place into '-a -b -c', '-p 45', '-a -b -c -p 45'. Not that
// anyone really talks this way anymore.
if (term.match(/^-/)) {
if (term.match(/^-[-=]?$/))
throw Error("these cases should be handled above?");
var replacements = [];
for (var j = 1; j < term.length; j++) {
var subterm = "-" + term.charAt(j);
if (isBoolean[subterm] === false) {
// If we recognize this short option, and we're sure that it
// takes a value, and there are remaining characters in the
// short option, then those remaining characters are the value.
replacements.push(subterm);
var remainder = term.substr(j + 1);
if (remainder.length) {
// If there's an '=' here, don't include it in the option value. A
// trailing '=' *should* cause us to set the option value to ''.
if (remainder.charAt(0) === '=')
remainder = remainder.substr(1);
replacements.push(remainder);
break;
}
} else if (isBoolean[subterm] &&
j + 1 < term.length && term.charAt(j + 1) === '=') {
// We know it's a boolean, but we've been given an '='. This will
// cause a pretty error later.
if (! _.has(rawOptions, subterm))
rawOptions[subterm] = [];
rawOptions[subterm].push(false);
// Don't process the '=' on the next pass.
j ++;
} else {
// It's a boolean without an '=', or it's something we've never heard
// of. (In the latter case, assume it's boolean for now, and we'll
// print an error later.)
replacements.push(subterm);
}
}
_.partial(argv.splice, i, 1).apply(argv, replacements);
i --;
continue;
}
// It is a plain old argument!
rawArgs.push(term);
}
// Figure out if we're running in a directory that is part of a Meteor
// application or package. Determine any additional directories to
// search for packages.
var appDir = files.findAppDir();
if (appDir) {
appDir = path.resolve(appDir);
// Set the project root directory. This doesn't do any dependency
// calculation -- we can't do that until the release is initialized.
project.project.setRootDir(appDir);
}
// XXX compare this to the previous block's usesWarehouse...
if (files.inCheckout()) {
// When running from a checkout, uniload does use local packages, but *ONLY
// THOSE FROM THE CHECKOUT*: not app packages or $PACKAGE_DIRS packages.
// One side effect of this: we really really expect them to all build, and
// we're fine with dying if they don't (there's no worries about needing to
// springboard).
var messages = buildmessage.capture(function () {
catalog.uniload.initialize({
localPackageDirs: [path.join(files.getCurrentToolsDir(), 'packages')]
});
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while scanning core packages:\n\n");
process.stderr.write(messages.formatMessages());
process.exit(1);
}
} else {
// This doesn't need to be in a buildmessage, because the
// BuiltUniloadCatalog really shouldn't need to build anything: it's just a
// bunch of precompiled unipackages!
catalog.uniload.initialize({
uniloadDir: files.getUniloadDir()
});
}
// Initialize the server catalog. Among other things, this is where
// we get release information (used by springboarding). This doesn't
// build anything (except maybe, if running from a checkout, packages
// that we need to uniload, which really ought to build) so it's OK
// to die on errors.
var messages = buildmessage.capture(function () {
catalog.official.initialize({
offline: !!process.env.METEOR_OFFLINE_CATALOG
});
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while initializing package catalog:\n\n");
process.stderr.write(messages.formatMessages());
process.exit(1);
}
// We do NOT initialize catalog.complete yet. When we do that, we will build
// all local packages, and for both performance and correctness reasons, we
// will wait until after the springboard check to do so.
// Now before we do anything else, figure out the release to use,
// and if that release goes with a different version of the tools,
// quit and run those tools instead.
//
// Note that doing this correctly requires knowledge of which
// arguments are boolean (in 'meteor --option --release 1.0', is
// '--release' a flag or the values of '--option')? We have to use
// the flag definitions in the current (latest) version of meteor to
// decide whether to exec the other version of meteor that would
// interpret the flags. That's not ideal, but it should do fine in
// practice, and it's better than assuming that all options are or
// aren't boolean when interpreting --release. See
// #ImprovingCrossVersionOptionParsing.
var releaseOverride = null;
var releaseForced = false;
var releaseExplicit = false;
if (_.has(rawOptions, '--release')) {
if (rawOptions['--release'].length > 1) {
process.stderr.write(
"--release should only be passed once.\n" +
"Try 'meteor help' for help.\n");
process.exit(1);
}
releaseOverride = rawOptions['--release'][0];
releaseForced = true;
if (! releaseOverride) {
process.stderr.write(
"The --release option needs a value.\n" +
"Try 'meteor help' for help.\n");
process.exit(1);
}
delete rawOptions['--release'];
}
// Let's keep track of whether this is an explicit release, due to different
// update behavior.
if (releaseOverride) {
releaseExplicit = true;
}
if (_.has(process.env, 'METEOR_SPRINGBOARD_RELEASE')) {
// See #SpringboardEnvironmentVar
// Note that this does *NOT* cause release.forced to be true.
// release.forced should only be set when the user actually
// ran with --release, not just because (eg) they ran
// 'meteor update' and we springboarded to the latest release.
// (It's important that 'meteor update' be able to tell these
// conditions apart even after the springboard!)
releaseOverride = process.env['METEOR_SPRINGBOARD_RELEASE'];
}
var releaseName, appRelease;
if (appDir) {
// appRelease will be null if a super old project with no
// .meteor/release or 'none' if created by a checkout
appRelease = project.project.getMeteorReleaseVersion();
// This is what happens if the file exists and is empty. This really
// shouldn't happen unless the user did it manually.
if (appRelease === '') {
process.stderr.write(
"Problem! This project has a .meteor/release file which is empty.\n" +
"The file should either contain the release of Meteor that you want to use,\n" +
"or the word 'none' if you will only use the project with unreleased\n" +
"checkouts of Meteor. Please edit the .meteor/release file in the project\n" +
"and change it to a valid Meteor release or 'none'.\n");
process.exit(1);
} else if (appRelease === null) {
process.stderr.write(
"Problem! This project does not have a .meteor/release file.\n" +
"The file should either contain the release of Meteor that you want to use,\n" +
"or the word 'none' if you will only use the project with unreleased\n" +
"checkouts of Meteor. Please edit the .meteor/release file in the project\n" +
"and change it to a valid Meteor release or 'none'.\n");
process.exit(1);
}
}
if (! files.usesWarehouse()) {
// Running from a checkout
if (releaseOverride) {
process.stderr.write(
"Can't specify a release when running Meteor from a checkout.\n");
process.exit(1);
}
releaseName = null;
} else {
// Running from an install
if (releaseOverride) {
// Use the release explicitly specified on the command line.
releaseName = releaseOverride;
} else if (appDir) {
// Running from an app directory. Use release specified by app.
if (appRelease === 'none') {
// Looks like we don't have a release. Leave release.current === null.
} else {
// Use the project's desired release
releaseName = appRelease;
}
} else {
// Run outside an app dir with no --release flag. Use the latest
// release we know about (in the default track).
var messages = buildmessage.capture(function () {
releaseName = release.latestDownloaded();
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while determining latest release:\n" +
messages.formatMessages());
process.exit(1);
}
}
}
if (releaseName !== undefined) {
// First, if we know just by looking at our disk that this is a legacy
// pre-0.9.0 release, springboard to it immediately, before calling
// release.load (which will refresh the catalog because the release
// presumably doesn't exist on the new server, and this is a slow
// operation).
if (releaseName !== null &&
warehouse.releaseExistsInWarehouse(releaseName)) {
var manifest = warehouse.ensureReleaseExistsAndReturnManifest(
releaseName);
oldSpringboard(manifest.tools);
}
try {
var rel;
var messages = buildmessage.capture(function () {
rel = release.load(releaseName);
});
if (messages.hasMessages()) {
// XXX The errors that trigger this are likely things like failure to
// load livedata when trying to refresh, or maybe failure to build some
// local packages, or something. They probably aren't "release doesn't
// exist"? But who knows?
process.stderr.write("=> Errors while loading release:\n" +
messages.formatMessages());
process.exit(1);
}
} catch (e) {
var name = releaseName;
if (e instanceof files.OfflineError) {
if (appDir) {
process.stderr.write(
"Sorry, this project uses Meteor " + name + ", which is not installed and\n"+
"could not be downloaded. Please check to make sure that you are online.\n");
} else {
process.stderr.write(
"Sorry, Meteor " + name + " is not installed and could not be downloaded.\n"+
"Please check to make sure that you are online.\n");
}
process.exit(1);
} else if (e instanceof release.NoSuchReleaseError) {
// OK, this release doesn't exist... unless it's an old pre-0.9.0
// release. Let's try using the legacy "warehouse" module to load it.
try {
var manifest = warehouse.ensureReleaseExistsAndReturnManifest(
releaseName);
} catch (e) {
// XXX handle OfflineError too?
if (e instanceof warehouse.NoSuchReleaseError) {
if (releaseOverride) {
process.stderr.write(name + ": unknown release.\n");
} else if (appDir) {
process.stderr.write(
"Problem! This project says that it uses version " + name + " of Meteor,\n" +
"but you don't have that version of Meteor installed and the Meteor update\n" +
"servers don't have it either. Please edit the .meteor/release file in the\n" +
"project and change it to a valid Meteor release.\n");
} else {
throw new Error("can't load latest release?");
}
process.exit(1);
}
throw e;
}
// OK, it was an old release. We should old-springboard to it.
oldSpringboard(manifest.tools);
}
throw e;
}
// Let's keep track of whether this is an explicit release, due to different
// update behavior.
if (releaseOverride) {
releaseForced = true;
}
release.setCurrent(rel, releaseForced, releaseExplicit);
}
// If we're not running the correct version of the tools for this
// release, fetch it and re-run.
//
// This will never happen when we're springboarding as part of an
// update, because the correct tools version will have been chosen
// the first time around. It will also never happen if the current
// release is a checkout, because that doesn't make any sense.
if (release.current && release.current.isProperRelease() &&
release.current.getToolsPackageAtVersion() !== files.getToolsVersion()) {
springboard(release.current); // does not return!
}
// OK, now it's finally time to set up the complete catalog. Only after this
// can we use the build system (other than via uniload).
// Figure out the directories that we should search for local
// packages (in addition to packages downloaded from the package
// server)
var localPackageDirs = [];
if (appDir)
localPackageDirs.push(path.join(appDir, 'packages'));
if (process.env.PACKAGE_DIRS) {
// User can provide additional package directories to search in
// PACKAGE_DIRS (colon-separated).
localPackageDirs = localPackageDirs.concat(
_.map(process.env.PACKAGE_DIRS.split(':'), function (p) {
return path.resolve(p);
}));
}
if (!files.usesWarehouse()) {
// Running from a checkout, so use the Meteor core packages from
// the checkout.
localPackageDirs.push(path.join(
files.getCurrentToolsDir(), 'packages'));
}
var messages = buildmessage.capture(function () {
catalog.complete.initialize({
localPackageDirs: localPackageDirs
});
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while scanning packages:\n\n");
process.stderr.write(messages.formatMessages());
process.exit(1);
}
// Check for the '--help' option.
var showHelp = false;
if (_.has(rawOptions, '--help')) {
showHelp = true;
delete rawOptions['--help'];
}
var commandName = '';
var command = null;
// Check for a command like '--arch' or '--version'. Make sure
// it stands alone. (And this is ignored if you've passed --help.)
if (! showHelp) {
_.each(commands['--'] || {}, function (value, key) {
var fullName = "--" + key;
if (rawOptions[fullName]) {
if (rawOptions[fullName].length > 1) {
process.stderr.write("It doesn't make sense to pass " +
fullName + " more than once.\n");
process.exit(1);
}
if (_.size(rawOptions) > 1 || rawArgs.length !== 0 || command) {
process.stderr.write("Can't pass anything else along with " +
value.name + ".\n");
process.exit(1);
}
command = value;
commandName = command.name;
delete rawOptions['--' + key];
}
});
}
// OK, if not one of those, the first (non-'--') argument(s) should
// name the command.
if (! command) {
if (rawArgs.length === 0) {
// No arguments means 'run'. Unless it's 'meteor --help'.
if (! showHelp) {
command = commands.run
commandName = "run";
if (! command)
throw new Error("no 'run' command?");
}
} else {
// Find the command they specified.
var walk = commands;
for (var i = 0; i < rawArgs.length; i++) {
var word = rawArgs[i];
// Support "meteor help", "meteor help deploy", "meteor help admin",
// "meteor admin help", "meteor admin help grant", etc. (But not
// "meteor deploy help" or "meteor admin grant help": once we find an
// actual command, we assume "help" is an argument, eg a site called
// 'help'!)
if (word === "help") {
showHelp = true;
continue;
}
commandName += (commandName.length > 0 ? " " : "") + word;
if (! _.has(walk, word)) {
process.stderr.write(
"'" + commandName + "' is not a Meteor command. See 'meteor --help'.\n");
process.exit(1);
}
if (walk[word] instanceof Command) {
command = walk[word];
rawArgs = rawArgs.slice(i + 1); // consume arguments used
break;
}
walk = walk[word];
}
}
}
if (! command && ! showHelp) {
// They typed something like 'meteor admin' (when they were
// supposed to type 'meteor admin grant' or something).
process.stderr.write(
"Try 'meteor " + commandName + " help' for available commands.\n");
process.exit(1);
}
// At this point we have a command[*]. Did they ask for help, or do
// they actually want to run the command? If the former, print the
// help and don't criticize anything else they may have given us.
//
// [*] the one exception being 'meteor --help' or 'meteor help', in
// which case showHelp will be true and command will be null
if (showHelp) {
process.stdout.write(longHelp(commandName) + "\n");
process.exit(0);
}
// They want to run the command. Interpret the options and make sure
// that they're valid.
var options = { args: rawArgs };
_.each(command.options, function (optionInfo, optionName) {
var presentLong = _.has(rawOptions, "--" + optionName);
var presentShort = _.has(optionInfo, 'short') &&
_.has(rawOptions, "-" + optionInfo.short);
if (presentShort && presentLong) {
// this would get caught below, but give a clearer error message
process.stderr.write(
commandName + ": can't pass both -" + optionInfo.short + " and --" +
optionName + ".\n" +
"Try 'meteor help " + commandName + "' for help.\n");
process.exit(1);
}
var helpfulOptionName = "--" + optionName +
(presentShort ? " (-" + optionInfo.short + ")" : "");
// Collect all values we've received for this option, across the
// long and short versions, and across possibly multiple
// occurrences of the option on the command line
var values = [];
if (presentLong)
values = values.concat(rawOptions["--" + optionName]);
if (presentShort)
values = values.concat(rawOptions["-" + optionInfo.short]);
if (values.length > 1) {
// in the future, we could support multiple values, but we don't
// for now since no command needs it
process.stderr.write(
commandName + ": can only take one " + helpfulOptionName + " option.\n" +
"Try 'meteor help " + commandName + "' for help.\n");
process.exit(1);
} else if (values.length === 1) {
// OK, they provided exactly one value. Check its type and add
// to the output.
var value = values[0];
if (value === null) {
// This option requires a value and they didn't give it one
// (it was the last word on the command line).
process.stderr.write(
commandName + ": the " + helpfulOptionName + " option needs a value.\n" +
"Try 'meteor help " + commandName + "' for help.\n");
process.exit(1);
} else if (optionInfo.type === Number) {
if (! value.match(/^[0-9]+$/)) {
process.stderr.write(
commandName + ": " + helpfulOptionName + " must be a number.\n" +
"Try 'meteor help " + commandName + "' for help.\n");
process.exit(1);
}
value = parseInt(value);
} else if (optionInfo.type === Boolean) {
if (!value) {
process.stderr.write(
commandName + ": the " + helpfulOptionName + " option does not need a value.\n" +
"Try 'meteor help " + commandName + "' for help.\n");
process.exit(1);
}
value = true;
} else if (optionInfo.type === String) {
// nothing to do, 'value' needs no parsing or validation
} else {
throw new Error("unknown option type?");
}
options[optionName] = value;
// Remove from the list of input arguments so that later we can
// detect unrecognized arguments.
if (presentLong)
delete rawOptions["--" + optionName];
if (presentShort)
delete rawOptions["-" + optionInfo.short];
} else {
// Option not supplied. Throw an error if it was required,
// supply a default value if one is defined, or just leave it
// out.
if (_.has(optionInfo, 'default')) {
options[optionName] = optionInfo.default;
} else if (optionInfo.required) {
process.stderr.write(
commandName + ": the --" + optionName + " option is required.\n" +
longHelp(commandName) + "\n");
process.exit(1);
}
}
});
// Check for unrecognized options.
if (_.keys(rawOptions).length > 0) {
process.stderr.write(
_.keys(rawOptions)[0] + ": unknown option.\n" +
longHelp(commandName) + "\n");
process.exit(1);
}
// Check argument count.
if (options.args.length < command.minArgs) {
process.stderr.write(
commandName + ": not enough arguments.\n" +
longHelp(commandName) + "\n");
process.exit(1);
}
if (options.args.length > command.maxArgs) {
process.stderr.write(
commandName + ": too many arguments.\n" +
longHelp(commandName) + "\n");
process.exit(1);
}
// We know we have a valid command and options. Now check to see if
// the command can only be run from an app dir, and add the appDir
// option if running from an app.
var requiresApp = command.requiresApp;
if (typeof requiresApp === "function")
requiresApp = requiresApp(options);
if (appDir)
options.appDir = appDir;
if (requiresApp && ! options.appDir) {
// This is where you end up if you type 'meteor' with no args,
// since you'll default to the 'run' command which requires an
// app. Be welcoming to our new developers!
process.stderr.write(
commandName + ": You're not in a Meteor project directory.\n" +
"\n" +
"To create a new Meteor project:\n" +
" meteor create <project name>\n" +
"For example:\n" +
" meteor create myapp\n" +
"\n" +
"For more help, see 'meteor --help'.\n");
process.exit(1);
}
// Same check for commands that can only be run from a package dir.
var requiresPackage = command.requiresPackage;
if (typeof requiresPackage === "function") {
requiresPackage = requiresPackage(options);
}
var packageDir = files.findPackageDir();
if (packageDir)
packageDir = path.resolve(packageDir);
if (packageDir) {
options.packageDir = packageDir;
}
if (requiresPackage) {
if (! options.packageDir) {
process.stderr.write(
commandName + ": You're not in a Meteor package directory.\n");
process.exit(1);
}
// Commands that require you to be in a package directory add that package
// as a local package to the catalog. Other random commands don't (but if we
// see a reason for them to, we can change this rule).
messages = buildmessage.capture(function () {
catalog.complete.addLocalPackage(path.basename(options.packageDir),
options.packageDir);
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while scanning current package:\n\n");
process.stderr.write(messages.formatMessages());
process.exit(1);
}
}
if (command.requiresRelease && ! release.current) {
process.stderr.write(
"You must specify a Meteor version with --release when you work with this\n" +
"project. It was created from an unreleased Meteor checkout and doesn't\n" +
"have a version associated with it.\n" +
"\n" +
"You can permanently set a release for this project with 'meteor update'.\n");
process.exit(1);
}
if (command.requiresApp && release.current.isCheckout() &&
appRelease && appRelease !== "none") {
// For commands that work with apps, if we have overridden the
// app's usual release by using a checkout, print a reminder banner.
process.stderr.write(
"=> Running Meteor from a checkout -- overrides project version (" +
appRelease + ")\n");
}
// Now that we're ready to start executing the command, if we are in
// startup time profiling mode, print the profile.
if (showRequireProfile)
require('./profile-require.js').printReport();
// Run the command!
try {
var ret = command.func(options);
} catch (e) {
if (e === main.ShowUsage || e === main.WaitForExit ||
e === main.SpringboardToLatestRelease ||
e === main.WaitForExit) {
throw new Error(
"you meant 'throw new main.Foo', not 'throw main.Foo'");
} else if (e instanceof main.ShowUsage) {
process.stderr.write(longHelp(commandName) + "\n");
process.exit(1);
} else if (e instanceof main.SpringboardToLatestRelease) {
// Load the latest release's metadata so that we can figure out
// the tools version that it uses. We should only do this if
// we know there is some latest release on this track.
var latestRelease;
var messages = buildmessage.capture(function () {
latestRelease = release.load(release.latestDownloaded(e.track));
});
if (messages.hasMessages()) {
process.stderr.write("=> Errors while loading latest release:\n\n");
process.stderr.write(messages.formatMessages());
process.exit(1);
}
springboard(latestRelease, latestRelease.name);
// (does not return)
} else if (e instanceof main.WaitForExit) {
return;
} else if (e instanceof main.ExitWithCode) {
process.exit(e.code);
} else {
throw e;
}
}
// Exit. (We will not get here if the command threw an exception
// such as main.WaitForExit).
if (ret === undefined)
ret = 0;
if (typeof ret !== "number")
throw new Error("command returned non-number?");
process.exit(ret);
}).run();