forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
1329 lines (1191 loc) · 49.1 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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 warehouse = require('./warehouse.js');
var tropohouse = require('./tropohouse.js');
var release = require('./release.js');
var projectContextModule = require('./project-context.js');
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(fullReleaseName, msg) {
assert.ok(this instanceof SpringboardToSpecificRelease);
this.fullReleaseName = fullReleaseName;
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);
};
main.captureAndExit = function (header, title, f) {
var messages;
if (f) {
messages = buildmessage.capture({ title: title }, f);
} else {
messages = buildmessage.capture(title); // title is really f
}
if (messages.hasMessages()) {
Console.error(header);
Console.printMessages(messages);
throw new main.ExitWithCode(1);
}
};
///////////////////////////////////////////////////////////////////////////////
// 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 = files.readFile(files.pathJoin(__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();
var packageMapModule = require('./package-map.js');
var versionMap = {};
versionMap[toolsPkg] = toolsVersion;
var packageMap = new packageMapModule.PackageMap(versionMap);
// XXX split better
Console.withProgressDisplayVisible(function () {
var messages = buildmessage.capture(function () {
tropohouse.default.downloadPackagesMissingFromMap(packageMap);
});
if (messages.hasMessages()) {
// 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",
"installed and could not be downloaded. Please check to make sure",
"that you are online.");
} else {
Console.error(
"Sorry, " + rel.getDisplayName() + " is not installed and could not",
"be downloaded. Please check to make sure that you are online.");
}
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 = files.pathJoin(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 =
files.pathJoin(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.33';
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 = files.pathResolve(appDir);
}
require('./isopackets.js').ensureIsopacketsLoadable();
// Initialize the server catalog. Among other things, this is where we get
// release information (used by springboarding). We do not at this point talk
// to the server and refresh it.
catalog.official.initialize({
offline: !!process.env.METEOR_OFFLINE_CATALOG
});
// 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. " +
"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. " +
"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, appReleaseFile;
if (appDir) {
appReleaseFile = new projectContextModule.ReleaseFile({
projectDir: appDir
});
// This is what happens if the file exists and is empty. This really
// shouldn't happen unless the user did it manually.
if (appReleaseFile.noReleaseSpecified()) {
Console.error(
"Problem! This project has a .meteor/release file which is empty.",
"The file should either contain the release of Meteor that you want",
"to use, or the word 'none' if you will only use the project with",
"unreleased checkouts of Meteor. Please edit the .meteor/release",
"file in the project and change it to a valid Meteor release or",
"'none'.");
process.exit(1);
} else if (appReleaseFile.fileMissing()) {
Console.error(
"Problem! This project does not have a .meteor/release file.",
"The file should either contain the release of Meteor that you",
"want to use, or the word 'none' if you will only use the project",
"with unreleased checkouts of Meteor. Please edit the",
".meteor/release file in the project 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 (appReleaseFile.isCheckout()) {
// Looks like we don't have a release. Leave release.current === null.
} else {
// Use the project's desired release
releaseName = appReleaseFile.unnormalizedReleaseName;
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.withProgressDisplayVisible(function () {
alreadyRefreshed = catalog.refreshOrWarn();
});
releaseName = release.latestKnown();
}
if (!releaseName) {
if (catalog.refreshFailed) {
Console.error(
"The package catalog has no information about any Meteor",
"releases, and we 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.withProgressDisplayVisible(function () {
alreadyRefreshed = catalog.refreshOrWarn();
});
// 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",
"you don't have that version of Meteor installed, and we were",
"unable to contact Meteor's update servers to find out about it.",
"Please edit the .meteor/release file in the project and change",
"it to a valid Meteor release, or go online.");
} else {
Console.error(
"This project says that it uses " + displayRelease + ", but you",
"don't have that version of Meteor installed and the Meteor",
"update servers don't have it either. Please edit the",
".meteor/release file in 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?");
}