Files
meteor/tools/main.js
2014-10-28 00:58:06 -07:00

1403 lines
52 KiB
JavaScript

var showRequireProfile = ('METEOR_PROFILE_REQUIRE' in process.env);
if (showRequireProfile)
require('./profile-require.js').start();
var assert = require("assert");
var _ = require('underscore');
var Fiber = require('fibers');
var Console = require('./console.js').Console;
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
///////////////////////////////////////////////////////////////////////////////
function Command(options) {
assert.ok(this instanceof Command);
options = _.extend({
minArgs: 0,
options: {},
requiresApp: false,
requiresRelease: true,
hidden: false,
pretty: 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 ShowUsage() {
assert.ok(this instanceof ShowUsage);
};
// 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 ExitWithCode(code) {
assert.ok(this instanceof ExitWithCode);
this.code = code;
};
_.extend(main.ExitWithCode.prototype, {
toString: function () {
var self = this;
return "ExitWithCode:" + self.code;
}
});
// Exception to throw to skip the process.exit call.
main.WaitForExit = function WaitForExit() {
assert.ok(this instanceof WaitForExit);
};
// 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 SpringboardToLatestRelease(track) {
assert.ok(this instanceof SpringboardToLatestRelease);
this.track = track;
};
// Exception to throw from a command to exit, restart, and reinvoke
// the command with the given Meteor release.
main.SpringboardToSpecificRelease =
function SpringboardToSpecificRelease(releaseRecord, msg) {
assert.ok(this instanceof SpringboardToSpecificRelease);
this.releaseRecord = releaseRecord;
this.msg = msg;
};
// 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)
// - catalogRefresh: strategy object specifying when to refresh the catalog.
// - 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);
}
if (!options.catalogRefresh) {
throw Error("Command does not select a catalogRefresh strategy: " +
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();
// can use to see if there is help text for a particular command
var helpDict = {};
_.each(help, function (helpEntry) {
helpDict[helpEntry.name] = helpEntry;
});
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.
// Also, use helpDict to only include commands that have help text,
// otherwise there is nothing to display
if (n instanceof Command && ! n.hidden && helpDict[fullName])
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.
//
// - release: required. the version of the tool to run.
//
// options:
// - 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).
// - fromApp: this release was suggested because it is the app's
// release. affects error messages.
var springboard = function (rel, options) {
options = options || {};
if (process.env.METEOR_DEBUG_SPRINGBOARD)
console.log("WILL SPRINGBOARD TO", rel.getToolsPackageAtVersion());
var archinfo = require('./archinfo.js');
var isopack = require('./isopack.js');
var toolsPkg = rel.getToolsPackage();
var toolsVersion = rel.getToolsVersion();
// XXX split better
try {
Console.setPretty(true);
Console.enableProgressDisplay(true);
var messages = buildmessage.capture({
title: "Downloading tools package " + toolsPkg + "@" + toolsVersion
}, function () {
tropohouse.default.maybeDownloadPackageForArchitectures({
packageName: toolsPkg,
version: toolsVersion,
architectures: [archinfo.host()],
definitelyNotLocal: true
});
});
Console.enableProgressDisplay(false);
Console.setPretty(false);
} catch (err) {
// We have failed to download the tool that we are supposed to springboard
// to! That's bad. Let's exit.
if (options.fromApp) {
Console.error(
"Sorry, this project uses " + rel.getDisplayName() + ", which is not\n" +
"installed and could not be downloaded. Please check to make sure that you\n" +
"are online.");
} else {
Console.error(
"Sorry, " + rel.getDisplayName() + " is not installed and could not be\n" +
"downloaded. Please check to make sure that you are online.");
}
process.exit(1);
}
if (messages.hasMessages()) {
// XXX I'm pretty sure that maybeDownloadPackageForArchitectures can no
// longer create buildmessages
Console.error(
"Could not springboard to release: " + rel.getDisplayName() + ".\n" +
messages.formatMessages());
process.exit(1);
}
var packagePath = tropohouse.default.packagePath(toolsPkg, toolsVersion);
var toolIsopack = new isopack.Isopack;
toolIsopack.initFromPath(toolsPkg, packagePath);
var toolRecord = _.findWhere(toolIsopack.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 (_.has(options, 'releaseOverride')) {
// 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'] = options.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)) {
Console.error(
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.');
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 isopack.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) {
Console.error('$ROOT_URL, if specified, must be an URL.');
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 for " +
"command " + key);
}
} 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(/^--?=/)) {
Console.error("Option names cannot begin with '='.");
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({ title: "Initializing local packages" }, function () {
catalog.uniload.initialize({
localPackageDirs: [path.join(files.getCurrentToolsDir(), 'packages')]
});
});
if (messages.hasMessages()) {
Console.error("=> Errors while scanning core packages:\n");
Console.error(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 isopacks!
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({ title: "Initializing server catalog" }, function () {
catalog.official.initialize({
offline: !!process.env.METEOR_OFFLINE_CATALOG
});
});
if (messages.hasMessages()) {
Console.error("=> Errors while initializing package catalog:\n");
Console.error(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;
var releaseFromApp = false;
if (_.has(rawOptions, '--release')) {
if (rawOptions['--release'].length > 1) {
Console.error(
"--release should only be passed once.\n" +
"Try 'meteor help' for help.");
process.exit(1);
}
releaseOverride = rawOptions['--release'][0];
releaseForced = true;
releaseExplicit = true;
if (! releaseOverride) {
Console.error(
"The --release option needs a value.\n" +
"Try 'meteor help' for help.");
process.exit(1);
}
delete rawOptions['--release'];
}
if (_.has(process.env, 'METEOR_SPRINGBOARD_RELEASE')) {
// See #SpringboardEnvironmentVar
// Note that this causes release.forced to be true, but not
// release.explicit. release.forced means "we're using
// some sort of externally specified release, not the app
// release"; release.explicit means "the end-user typed
// --release".
releaseOverride = process.env['METEOR_SPRINGBOARD_RELEASE'];
releaseForced = true;
}
var releaseName, appReleaseUnnormalized;
if (appDir) {
// appRelease will be null if a super old project with no
// .meteor/release or 'none' if created by a checkout
appReleaseUnnormalized = 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 (appReleaseUnnormalized === '') {
Console.error(
"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'.");
process.exit(1);
} else if (appReleaseUnnormalized === null) {
Console.error(
"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'.");
process.exit(1);
}
}
var alreadyRefreshed = false;
if (! files.usesWarehouse()) {
// Running from a checkout
if (releaseOverride) {
Console.error(
"Can't specify a release when running Meteor from a checkout.");
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 (appReleaseUnnormalized === 'none') {
// Looks like we don't have a release. Leave release.current === null.
} else {
// Use the project's desired release
releaseName = appReleaseUnnormalized;
releaseFromApp = true;
}
} else {
// Run outside an app dir with no --release flag. Use the latest
// release we know about (in the default track).
releaseName = release.latestKnown();
if (!releaseName) {
// Somehow we have a catalog that doesn't have any releases on the
// default track. Try syncing, at least. (This is a pretty unlikely
// error case, since you should always start with a non-empty catalog.)
Console.setPretty(true);
Console.enableProgressDisplay(true);
alreadyRefreshed = catalog.refreshOrWarn();
Console.enableProgressDisplay(false);
Console.setPretty(false);
releaseName = release.latestKnown();
}
if (!releaseName) {
if (catalog.refreshFailed) {
Console.error(
"The package catalog has no information about any Meteor releases, and we\n" +
"had trouble connecting to the package server.");
} else {
Console.error(
"The package catalog has no information about any Meteor releases.");
}
process.exit(1);
}
}
}
if (releaseName !== undefined) {
// Yay, it's time to load releases!
//
// The release could be a modern (0.9.0+) tropohouse release or a legacy
// (pre-0.9.0) warehouse release.
//
// The release could be something we already know about on our local disk,
// or it could be something we have to ask a server about.
//
// We want to check both possibilities on disk before talking to any
// server. And we want to check for modern releases first in both cases.
var rel = null;
if (process.env.METEOR_TEST_FAIL_RELEASE_DOWNLOAD !== 'not-found') {
// ATTEMPT 1: modern release, on disk. (For modern releases, "on disk"
// just means we have the metadata about it in our catalog; it doesn't
// mean we've downloaded the tool or any packages yet.) release.load just
// does a single sqlite query; it doesn't refresh the catalog.
try {
rel = release.load(releaseName);
} catch (e) {
if (!(e instanceof release.NoSuchReleaseError))
throw e;
}
if (!rel) {
if (releaseName === null)
throw Error("huh? couldn't load from-checkout release?");
// ATTEMPT 2: legacy release, on disk. (And it's a "real" release, not a
// "red pill" release which has the same name as a modern release!)
if (warehouse.realReleaseExistsInWarehouse(releaseName)) {
var manifest = warehouse.ensureReleaseExistsAndReturnManifest(
releaseName);
oldSpringboard(manifest.tools); // doesn't return
}
// ATTEMPT 3: modern release, troposphere sync needed.
Console.setPretty(true);
Console.enableProgressDisplay(true);
alreadyRefreshed = catalog.refreshOrWarn();
Console.enableProgressDisplay(false);
Console.setPretty(false);
// Try to load the release even if the refresh failed, since it might
// have failed on a later page than the one we needed.
try {
rel = release.load(releaseName);
} catch (e) {
if (!(e instanceof release.NoSuchReleaseError)) {
throw e;
}
}
}
if (!rel) {
// ATTEMPT 4: legacy release, loading from warehouse server.
manifest = null;
try {
manifest = warehouse.ensureReleaseExistsAndReturnManifest(
releaseName);
} catch (e) {
// Note: this is WAREHOUSE's NoSuchReleaseError, not RELEASE's
if (e instanceof warehouse.NoSuchReleaseError) {
// pass ...
} else if (e instanceof files.OfflineError) {
if (!catalog.refreshFailed) {
// Warn if we didn't already warn.
Console.warn("Unable to contact release server (are you offline?)");
}
// Treat this like a failure to refresh the catalog
// (map the old world to the new world)
catalog.refreshFailed = true;
} else {
throw e;
}
}
if (manifest) {
// OK, it was an legacy release. We should old-springboard to it.
oldSpringboard(manifest.tools); // doesn't return
}
}
}
if (!rel) {
// Nope, still have no idea about this release!
// Let's do some processing here. If the user/release file specified a
// track, we need to display that correctly, and if they didn't, we should
// make it clear that we are talking about the default track.
var utils = require('./utils.js');
var trackAndVersion = utils.splitReleaseName(releaseName);
var displayRelease = utils.displayRelease(
trackAndVersion[0], trackAndVersion[1]);
// Now, let's process this.
if (releaseOverride) {
Console.error(displayRelease + ": unknown release.");
} else if (appDir) {
if (trackAndVersion[0] !== catalog.DEFAULT_TRACK) {
displayRelease = "Meteor release " + displayRelease;
}
if (catalog.refreshFailed) {
Console.error(
"This project says that it uses " + displayRelease + ", but\n" +
"you don't have that version of Meteor installed, and we were unable to\n" +
"contact Meteor's update servers to find out about it. Please edit the\n" +
".meteor/release file in the project and change it to a valid Meteor\n" +
"release, or go online.");
} else {
Console.error(
"This project says that it uses " + displayRelease + ", but you don't have\n" +
"that version of Meteor installed and the Meteor update servers\n" +
"don't have it either. Please edit the .meteor/release file in\n" +
"the project and change it to a valid Meteor release.");
}
} else {
throw new Error("can't load latest release?");
}
process.exit(1);
}
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, { fromApp: releaseFromApp });
// Does not return!
}
// 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) {
Console.error("It doesn't make sense to pass " +
fullName + " more than once.");
process.exit(1);
}
if (_.size(rawOptions) > 1 || rawArgs.length !== 0 || command) {
Console.error("Can't pass anything else along with " +
value.name + ".");
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)) {
Console.error(
"'" + commandName + "' is not a Meteor command. See 'meteor --help'.");
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).
Console.error(
"Try 'meteor " + commandName + " help' for available commands.");
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) {
Console.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
Console.error(
commandName + ": can't pass both -" + optionInfo.short + " and --" +
optionName + ".\n" +
"Try 'meteor help " + commandName + "' for help.");
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
Console.error(
commandName + ": can only take one " + helpfulOptionName + " option.\n" +
"Try 'meteor help " + commandName + "' for help.");
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).
Console.error(
commandName + ": the " + helpfulOptionName + " option needs a value.\n" +
"Try 'meteor help " + commandName + "' for help.");
process.exit(1);
} else if (optionInfo.type === Number) {
if (! value.match(/^[0-9]+$/)) {
Console.error(
commandName + ": " + helpfulOptionName + " must be a number.\n" +
"Try 'meteor help " + commandName + "' for help.");
process.exit(1);
}
value = parseInt(value);
} else if (optionInfo.type === Boolean) {
if (!value) {
Console.error(
commandName + ": the " + helpfulOptionName + " option does not need a value.\n" +
"Try 'meteor help " + commandName + "' for help.");
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) {
Console.error(
commandName + ": the --" + optionName + " option is required.\n" +
longHelp(commandName));
process.exit(1);
}
}
});
// Check for unrecognized options.
if (_.keys(rawOptions).length > 0) {
Console.error(
_.keys(rawOptions)[0] + ": unknown option.\n" +
longHelp(commandName));
process.exit(1);
}
// Check argument count.
if (options.args.length < command.minArgs) {
Console.error(
commandName + ": not enough arguments.\n" +
longHelp(commandName));
process.exit(1);
}
if (options.args.length > command.maxArgs) {
Console.error(
commandName + ": too many arguments.\n" +
longHelp(commandName));
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!
Console.error(
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'.");
process.exit(1);
}
if (!command.catalogRefresh.doesNotUsePackages) {
// 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).
// XXX This code is duplicated a bit inside the create command. Sorry.
// 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({ title: "Initializing catalog" }, function () {
catalog.complete.initialize({
localPackageDirs: localPackageDirs
});
});
if (messages.hasMessages()) {
Console.error("=> Errors while scanning packages:\n");
Console.error(messages.formatMessages());
process.exit(1);
}
// Same check for commands that can only be run from a package dir.
// You can't specify this on a Refresh.Never command.
var requiresPackage = command.requiresPackage;
if (typeof requiresPackage === "function") {
requiresPackage = requiresPackage(options);
}
if (requiresPackage) {
var packageDir = files.findPackageDir();
if (packageDir)
packageDir = path.resolve(packageDir);
if (packageDir) {
options.packageDir = packageDir;
}
if (! options.packageDir) {
Console.error(
commandName + ": You're not in a Meteor package directory.");
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(options.packageDir);
});
if (messages.hasMessages()) {
Console.error("=> Errors while scanning current package:\n");
Console.error(messages.formatMessages());
process.exit(1);
}
}
}
if (command.requiresRelease && ! release.current) {
Console.error(
"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'.");
process.exit(1);
}
if (command.requiresApp && release.current.isCheckout() &&
appReleaseUnnormalized && appReleaseUnnormalized !== "none") {
var utils = require("./utils.js");
var appReleaseParts = utils.splitReleaseName(appReleaseUnnormalized);
// For commands that work with apps, if we have overridden the
// app's usual release by using a checkout, print a reminder banner.
Console.warn(
"=> Running Meteor from a checkout -- overrides project version (" +
utils.displayRelease(appReleaseParts[0], appReleaseParts[1]) + ")");
}
// 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();
Console.setPretty(command.pretty);
Console.enableProgressDisplay(true);
// Run the command!
try {
// Before run, do a package sync if one is configured
var catalogRefreshStrategy = command.catalogRefresh;
if (!alreadyRefreshed && catalogRefreshStrategy.beforeCommand) {
buildmessage.enterJob({title: 'Updating package catalog'}, function () {
catalogRefreshStrategy.beforeCommand();
});
}
var ret = command.func(options);
} catch (e) {
Console.enableProgressDisplay(false);
if (e === main.ShowUsage || e === main.WaitForExit ||
e === main.SpringboardToLatestRelease ||
e === main.SpringboardToSpecificReleaseg ||
e === main.WaitForExit) {
throw new Error(
"you meant 'throw new main.Foo', not 'throw main.Foo'");
} else if (e instanceof main.ShowUsage) {
Console.error(longHelp(commandName));
process.exit(1);
} else if (e instanceof main.SpringboardToLatestRelease) {
// Load the metadata for the latest release (or at least, the latest
// release we know about locally). We should only do this if we know there
// is some latest release on this track. Note that this is only throw by
// 'update' and 'create', which are both catalog.Refresh.OnceAtStart
// commands, so we ought to have decent knowledge of the latest release.
var latestRelease = release.load(release.latestKnown(e.track));
springboard(latestRelease, { releaseOverride: latestRelease.name });
// (does not return)
} else if (e instanceof main.SpringboardToSpecificRelease) {
// Springboard to a specific release. This is only throw by
// publish-for-arch, which is catalog.Refresh.OnceAtStart, so we ought to
// have decent knowledge of the latest release.
var relName = e.releaseRecord.track + "@" + e.releaseRecord.version;
var nextRelease = release.load(relName);
springboard(nextRelease, { releaseOverride: relName });
// (does not return)
} else if (e instanceof main.WaitForExit) {
return;
} else if (e instanceof main.ExitWithCode) {
process.exit(e.code);
} else {
throw e;
}
}
Console.enableProgressDisplay(false);
// 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();