/
_build.js
4073 lines (3591 loc) Β· 145 KB
/
_build.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
/**
* Android build command.
*
* @module cli/_build
*
* @copyright
* Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved.
*
* Copyright (c) 2012-2013 Chris Talkington, contributors.
* {@link https://github.com/ctalkington/node-archiver}
*
* @license
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
var ADB = require('titanium-sdk/lib/adb'),
AdmZip = require('adm-zip'),
android = require('titanium-sdk/lib/android'),
androidDetect = require('../lib/detect').detect,
AndroidManifest = require('../lib/AndroidManifest'),
appc = require('node-appc'),
archiver = require('archiver'),
archiverCore = require('archiver/lib/archiver/core'),
async = require('async'),
Builder = require('titanium-sdk/lib/builder'),
cleanCSS = require('clean-css'),
crypto = require('crypto'),
DOMParser = require('xmldom').DOMParser,
ejs = require('ejs'),
EmulatorManager = require('titanium-sdk/lib/emulator'),
fields = require('fields'),
fs = require('fs'),
i18n = require('titanium-sdk/lib/i18n'),
jsanalyze = require('titanium-sdk/lib/jsanalyze'),
path = require('path'),
temp = require('temp'),
ti = require('titanium-sdk'),
tiappxml = require('titanium-sdk/lib/tiappxml'),
util = require('util'),
wrench = require('wrench'),
afs = appc.fs,
i18nLib = appc.i18n(__dirname),
__ = i18nLib.__,
__n = i18nLib.__n,
version = appc.version,
xml = appc.xml;
// Archiver 0.4.10 has a problem where the stack size is exceeded if the project
// has lots and lots of files. Below is a function copied directly from
// lib/archiver/core.js and modified to use a setTimeout to collapse the call
// stack. Copyright (c) 2012-2013 Chris Talkington, contributors.
archiverCore.prototype._processQueue = function _processQueue() {
if (this.archiver.processing) {
return;
}
if (this.archiver.queue.length > 0) {
var next = this.archiver.queue.shift();
var nextCallback = function(err, file) {
next.callback(err);
if (!err) {
this.archiver.files.push(file);
this.archiver.processing = false;
// do a setTimeout to collapse the call stack
setTimeout(function () {
this._processQueue();
}.bind(this), 0);
}
}.bind(this);
this.archiver.processing = true;
this._processFile(next.source, next.data, nextCallback);
} else if (this.archiver.finalized && this.archiver.writableEndCalled === false) {
this.archiver.writableEndCalled = true;
this.end();
} else if (this.archiver.finalize && this.archiver.queue.length === 0) {
this._finalize();
}
};
function hash(s) {
return crypto.createHash('md5').update(s || '').digest('hex');
}
function AndroidBuilder() {
Builder.apply(this, arguments);
this.devices = null; // set by findTargetDevices() during 'config' phase
this.keystoreAliases = [];
this.tiSymbols = {};
this.minSupportedApiLevel = parseInt(version.parseMin(this.packageJson.vendorDependencies['android sdk']));
this.maxSupportedApiLevel = parseInt(version.parseMax(this.packageJson.vendorDependencies['android sdk']));
this.deployTypes = {
'emulator': 'development',
'device': 'test',
'dist-playstore': 'production'
};
this.targets = ['emulator', 'device', 'dist-playstore'];
this.validABIs = ['armeabi', 'armeabi-v7a', 'x86'];
this.xmlMergeRegExp = /^(strings|attrs|styles|bools|colors|dimens|ids|integers|arrays)\.xml$/;
this.uncompressedTypes = [
'jpg', 'jpeg', 'png', 'gif',
'wav', 'mp2', 'mp3', 'ogg', 'aac',
'mpg', 'mpeg', 'mid', 'midi', 'smf', 'jet',
'rtttl', 'imy', 'xmf', 'mp4', 'm4a',
'm4v', '3gp', '3gpp', '3g2', '3gpp2',
'amr', 'awb', 'wma', 'wmv'
];
}
util.inherits(AndroidBuilder, Builder);
AndroidBuilder.prototype.config = function config(logger, config, cli) {
Builder.prototype.config.apply(this, arguments);
var _t = this;
this.ignoreDirs = new RegExp(config.get('cli.ignoreDirs'));
this.ignoreFiles = new RegExp(config.get('cli.ignoreFiles'));
// we hook into the pre-validate event so that we can stop the build before
// prompting if we know the build is going to fail.
//
// this is also where we can detect android and jdk environments before
// prompting occurs. because detection is expensive we also do it here instead
// of during config() because there's no sense detecting if config() is being
// called because of the help command.
cli.on('cli:pre-validate', function (obj, callback) {
if (cli.argv.platform && cli.argv.platform != 'android') {
return callback();
}
function assertIssue(logger, issues, name, exit) {
var i = 0,
len = issues.length;
for (; i < len; i++) {
if ((typeof name == 'string' && issues[i].id == name) || (typeof name == 'object' && name.test(issues[i].id))) {
issues[i].message.split('\n').forEach(function (line) {
logger.error(line.replace(/(__(.+?)__)/g, '$2'.bold));
});
logger.log();
exit && process.exit(1);
}
}
}
async.series([
function (next) {
// detect android environment
androidDetect(config, { packageJson: _t.packageJson }, function (androidInfo) {
_t.androidInfo = androidInfo;
assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_NOT_FOUND', true);
assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_PATH_CONTAINS_AMPERSANDS', true);
if (!cli.argv.prompt) {
// check that the Android SDK is found and sane
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_NOT_FOUND');
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_MISSING_PROGRAMS');
// make sure we have an Android SDK and some Android targets
if (!Object.keys(androidInfo.targets).filter(function (id) {
var t = androidInfo.targets[id];
return t.type == 'platform' && t['api-level'] >= _t.minSupportedApiLevel;
}).length) {
if (Object.keys(androidInfo.targets).length) {
logger.error(__('No valid Android SDK targets found.'));
} else {
logger.error(__('No Android SDK targets found.'));
}
logger.error(__('Please download an Android SDK target API level %s or newer from the Android SDK Manager and try again.', _t.minSupportedApiLevel) + '\n');
process.exit(1);
}
}
// if --android-sdk was not specified, then we simply try to set a default android sdk
if (!cli.argv['android-sdk']) {
var androidSdkPath = config.android && config.android.sdkPath;
if (!androidSdkPath && androidInfo.sdk) {
androidSdkPath = androidInfo.sdk.path;
}
androidSdkPath && (cli.argv['android-sdk'] = afs.resolvePath(androidSdkPath));
}
next();
});
},
function (next) {
// detect java development kit
appc.jdk.detect(config, null, function (jdkInfo) {
assertIssue(logger, jdkInfo.issues, 'JDK_NOT_INSTALLED', true);
assertIssue(logger, jdkInfo.issues, 'JDK_MISSING_PROGRAMS', true);
assertIssue(logger, jdkInfo.issues, 'JDK_INVALID_JAVA_HOME', true);
if (!jdkInfo.version) {
logger.error(__('Unable to locate the Java Development Kit') + '\n');
logger.log(__('You can specify the location by setting the %s environment variable.', 'JAVA_HOME'.cyan) + '\n');
process.exit(1);
}
if (!version.satisfies(jdkInfo.version, _t.packageJson.vendorDependencies.java)) {
logger.error(__('JDK version %s detected, but only version %s is supported', jdkInfo.version, _t.packageJson.vendorDependencies.java) + '\n');
process.exit(1);
}
_t.jdkInfo = jdkInfo;
next();
});
}
], callback);
});
var targetDeviceCache = {},
findTargetDevices = function findTargetDevices(target, callback) {
if (targetDeviceCache[target]) {
return callback(null, targetDeviceCache[target]);
}
if (target == 'device') {
new ADB(config).devices(function (err, devices) {
if (err) {
callback(err);
} else {
this.devices = devices.filter(function (d) { return !d.emulator && d.state == 'device'; });
if (this.devices.length > 1) {
// we have more than 1 device, so we should show 'all'
this.devices.push({
id: 'all',
model: 'All Devices'
});
}
callback(null, targetDeviceCache[target] = this.devices.map(function (d) {
return {
name: d.model || d.manufacturer,
id: d.id,
version: d.release,
abi: Array.isArray(d.abi) ? d.abi.join(',') : d.abi,
type: 'device'
};
}));
}
}.bind(this));
} else if (target == 'emulator') {
new EmulatorManager(config).detect(function (err, emus) {
if (err) {
callback(err);
} else {
this.devices = emus;
callback(null, targetDeviceCache[target] = emus.map(function (emu) {
// normalize the emulator info
if (emu.type == 'avd') {
return {
name: emu.name,
id: emu.name,
version: emu['sdk-version'],
abi: emu.abi,
type: emu.type,
googleApis: emu.googleApis,
sdcard: emu.sdcard
};
} else if (emu.type == 'genymotion') {
return {
name: emu.name,
id: emu.name,
version: emu['sdk-version'],
abi: emu.abi,
type: emu.type,
googleApis: emu.googleApis,
sdcard: true
};
}
return emu; // not good
}));
}
}.bind(this));
} else {
callback();
}
}.bind(this);
return function (finished) {
cli.createHook('build.android.config', this, function (callback) {
var conf = {
options: {
'alias': {
abbr: 'L',
desc: __('the alias for the keystore'),
hint: 'alias',
order: 155,
prompt: function (callback) {
callback(fields.select({
title: __("What is the name of the keystore's certificate alias?"),
promptLabel: __('Select a certificate alias by number or name'),
margin: '',
optionLabel: 'name',
optionValue: 'name',
numbered: true,
relistOnError: true,
complete: true,
suggest: false,
options: _t.keystoreAliases,
validate: conf.options.alias.validate
}));
},
validate: function (value, callback) {
// if there's a value, then they entered something, otherwise let the cli prompt
if (value) {
var selectedAlias = value.toLowerCase(),
alias = _t.keystoreAlias = _t.keystoreAliases.filter(function (a) { return a.name && a.name.toLowerCase() == selectedAlias; }).shift();
if (!alias) {
return callback(new Error(__('Invalid "--alias" value "%s"', value)));
}
if (alias.sigalg && alias.sigalg.toLowerCase() == 'sha256withrsa') {
logger.warn(__('The selected alias %s uses the %s signature algorithm which will likely have issues with Android 4.3 and older.', ('"' + value + '"').cyan, ('"' + alias.sigalg + '"').cyan));
logger.warn(__('Certificates that use the %s or %s signature algorithm will provide better compatibility.', '"SHA1withRSA"'.cyan, '"MD5withRSA"'.cyan));
}
}
callback(null, value);
}
},
'android-sdk': {
abbr: 'A',
default: config.android && config.android.sdkPath && afs.resolvePath(config.android.sdkPath),
desc: __('the path to the Android SDK'),
hint: __('path'),
order: 100,
prompt: function (callback) {
var androidSdkPath = config.android && config.android.sdkPath;
if (!androidSdkPath && _t.androidInfo.sdk) {
androidSdkPath = _t.androidInfo.sdk.path;
}
if (androidSdkPath) {
androidSdkPath = afs.resolvePath(androidSdkPath);
if (process.platform == 'win32' || androidSdkPath.indexOf('&') != -1) {
androidSdkPath = undefined;
}
}
callback(fields.file({
promptLabel: __('Where is the Android SDK?'),
default: androidSdkPath,
complete: true,
showHidden: true,
ignoreDirs: _t.ignoreDirs,
ignoreFiles: _t.ignoreFiles,
validate: _t.conf.options['android-sdk'].validate.bind(_t)
}));
},
required: true,
validate: function (value, callback) {
if (!value) {
callback(new Error(__('Invalid Android SDK path')));
} else if (process.platform == 'win32' && value.indexOf('&') != -1) {
callback(new Error(__('The Android SDK path cannot contain ampersands (&) on Windows')));
} else if (_t.androidInfo.sdk && _t.androidInfo.sdk.path == afs.resolvePath(value)) {
// no sense doing the detection again
callback(null, value);
} else {
// do a quick scan to see if the path is correct
android.findSDK(value, config, appc.pkginfo.package(module), function (err, results) {
if (err) {
callback(new Error(__('Invalid Android SDK path: %s', value)));
} else {
function next() {
// set the android sdk in the config just in case a plugin or something needs it
config.set('android.sdkPath', value);
// path looks good, do a full scan again
androidDetect(config, { packageJson: _t.packageJson, bypassCache: true }, function (androidInfo) {
_t.androidInfo = androidInfo;
callback(null, value);
});
}
// new android sdk path looks good
// if we found an android sdk in the pre-validate hook, then we need to kill the other sdk's adb server
if (_t.androidInfo.sdk) {
new ADB(config).stopServer(next);
} else {
next();
}
}
});
}
}
},
'avd-abi': {
abbr: 'B',
desc: __('the abi for the Android emulator; deprecated, use --device-id'),
hint: __('abi')
},
'avd-id': {
abbr: 'I',
desc: __('the id for the Android emulator; deprecated, use --device-id'),
hint: __('id')
},
'avd-skin': {
abbr: 'S',
desc: __('the skin for the Android emulator; deprecated, use --device-id'),
hint: __('skin')
},
'debug-host': {
hidden: true
},
'deploy-type': {
abbr: 'D',
desc: __('the type of deployment; only used when target is %s or %s', 'emulator'.cyan, 'device'.cyan),
hint: __('type'),
order: 110,
values: ['test', 'development']
},
'device-id': {
abbr: 'C',
desc: __('the name of the Android emulator or the device id to install the application to'),
hint: __('name'),
order: 130,
prompt: function (callback) {
findTargetDevices(cli.argv.target, function (err, results) {
var opts = {},
title,
promptLabel;
// we need to sort all results into groups for the select field
if (cli.argv.target == 'device' && results.length) {
opts[__('Devices')] = results;
title = __('Which device do you want to install your app on?');
promptLabel = __('Select a device by number or name');
} else if (cli.argv.target == 'emulator') {
// for emulators, we sort by type
var emus = results.filter(function (e) {
return e.type == 'avd';
});
if (emus.length) {
opts[__('Android Emulators')] = emus;
}
emus = results.filter(function (e) {
return e.type == 'genymotion';
});
if (emus.length) {
opts[__('Genymotion Emulators')] = emus;
logger.log(__('NOTE: Genymotion emulator must be running to detect Google API support').magenta + '\n');
}
title = __('Which emulator do you want to launch your app in?');
promptLabel = __('Select an emulator by number or name');
}
// if there are no devices/emulators, error
if (!Object.keys(opts).length) {
if (cli.argv.target == 'device') {
logger.error(__('Unable to find any devices') + '\n');
logger.log(__('Please plug in an Android device, then try again.') + '\n');
} else {
logger.error(__('Unable to find any emulators') + '\n');
logger.log(__('Please create an Android emulator, then try again.') + '\n');
}
process.exit(1);
}
callback(fields.select({
title: title,
promptLabel: promptLabel,
formatters: {
option: function (opt, idx, num) {
return ' ' + num + opt.name.cyan + (opt.version ? ' (' + opt.version + ')' : '') + (opt.googleApis
? (' (' + __('Google APIs supported') + ')').grey
: opt.googleApis === null
? (' (' + __('Google APIs support unknown') + ')').grey
: '');
}
},
autoSelectOne: true,
margin: '',
optionLabel: 'name',
optionValue: 'id',
numbered: true,
relistOnError: true,
complete: true,
suggest: true,
options: opts
}));
});
},
required: true,
validate: function (device, callback) {
var dev = device.toLowerCase();
findTargetDevices(cli.argv.target, function (err, devices) {
if (cli.argv.target == 'device' && dev == 'all') {
// we let 'all' slide by
return callback(null, dev);
}
var i = 0,
l = devices.length;
for (; i < l; i++) {
if (devices[i].id.toLowerCase() == dev) {
return callback(null, devices[i].id);
}
}
callback(new Error(cli.argv.target ? __('Invalid Android device "%s"', device) : __('Invalid Android emulator "%s"', device)));
});
},
verifyIfRequired: function (callback) {
if (cli.argv['build-only']) {
// not required if we're build only
return callback();
}
findTargetDevices(cli.argv.target, function (err, results) {
if (cli.argv.target == 'emulator' && cli.argv['device-id'] == undefined && cli.argv['avd-id']) {
// if --device-id was not specified, but --avd-id was, then we need to
// try to resolve a device based on the legacy --avd-* options
var avds = results.filter(function (a) { return a.type == 'avd'; }).map(function (a) { return a.name; }),
name = 'titanium_' + cli.argv['avd-id'] + '_';
if (avds.length) {
// try finding the first avd that starts with the avd id
avds = avds.filter(function (avd) { return avd.indexOf(name) == 0; });
if (avds.length == 1) {
cli.argv['device-id'] = avds[0];
return callback();
} else if (avds.length > 1) {
// next try using the avd skin
if (!cli.argv['avd-skin']) {
// we have more than one match
logger.error(__n('Found %s avd with id "%%s"', 'Found %s avds with id "%%s"', avds.length, cli.argv['avd-id']));
logger.error(__('Specify --avd-skin and --avd-abi to select a specific emulator') + '\n');
} else {
name += cli.argv['avd-skin'];
// try exact match
var tmp = avds.filter(function (avd) { return avd == name; });
if (tmp.length) {
avds = tmp;
} else {
// try partial match
avds = avds.filter(function (avd) { return avd.indexOf(name + '_') == 0; });
}
if (avds.length == 0) {
logger.error(__('No emulators found with id "%s" and skin "%s"', cli.argv['avd-id'], cli.argv['avd-skin']) + '\n');
} else if (avds.length == 1) {
cli.argv['device-id'] = avds[0];
return callback();
} else if (!cli.argv['avd-abi']) {
// we have more than one matching avd, but no abi to filter by so we have to error
logger.error(__n('Found %s avd with id "%%s" and skin "%%s"', 'Found %s avds with id "%%s" and skin "%%s"', avds.length, cli.argv['avd-id'], cli.argv['avd-skin']));
logger.error(__('Specify --avd-abi to select a specific emulator') + '\n');
} else {
name += '_' + cli.argv['avd-abi'];
// try exact match
tmp = avds.filter(function (avd) { return avd == name; });
if (tmp.length) {
avds = tmp;
} else {
avds = avds.filter(function (avd) { return avd.indexOf(name + '_') == 0; });
}
if (avds.length == 0) {
logger.error(__('No emulators found with id "%s", skin "%s", and abi "%s"', cli.argv['avd-id'], cli.argv['avd-skin'], cli.argv['avd-abi']) + '\n');
} else {
// there is one or more avds, but we'll just return the first one
cli.argv['device-id'] = avds[0];
return callback();
}
}
}
}
logger.warn(__('%s options have been %s, please use %s', '--avd-*'.cyan, 'deprecated'.red, '--device-id'.cyan) + '\n');
// print list of available avds
if (results.length && !cli.argv.prompt) {
logger.log(__('Available Emulators:'))
results.forEach(function (emu) {
logger.log(' ' + emu.name.cyan + ' (' + emu.version + ')');
});
logger.log();
}
}
} else if (cli.argv['device-id'] == undefined && results.length && config.get('android.autoSelectDevice', true)) {
// we set the device-id to an array of devices so that later in validate()
// after the tiapp.xml has been parsed, we can auto select the best device
cli.argv['device-id'] = results.sort(function (a, b) {
var eq = appc.version.eq(a.version, b.version),
gt = appc.version.gt(a.version, b.version);
if (eq) {
if (a.type == b.type) {
if (a.googleApis == b.googleApis) {
return 0;
} else if (b.googleApis) {
return 1;
} else if (a.googleApis === false && b.googleApis === null) {
return 1;
}
return -1;
}
return a.type == 'avd' ? -1 : 1;
}
return gt ? 1 : -1;
});
return callback();
}
// yup, still required
callback(true);
});
}
},
'key-password': {
desc: __('the password for the keystore private key (defaults to the store-password)'),
hint: 'keypass',
order: 160,
prompt: function (callback) {
callback(fields.text({
promptLabel: __("What is the keystore's __key password__?") + ' ' + __('(leave blank to use the store password)').grey,
password: true,
validate: _t.conf.options['key-password'].validate.bind(_t)
}));
},
secret: true,
validate: function (keyPassword, callback) {
// sanity check the keystore and store password
_t.conf.options['store-password'].validate(cli.argv['store-password'], function (err, storePassword) {
if (err) {
// we have a bad --keystore or --store-password arg
cli.argv.keystore = cli.argv['store-password'] = undefined;
return callback(err);
}
var keystoreFile = cli.argv.keystore,
alias = cli.argv.alias,
tmpKeystoreFile = temp.path({ suffix: '.jks' });
if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) {
// the only way to test the key password is to export the cert
appc.subprocess.run(_t.jdkInfo.executables.keytool, [
'-J-Duser.language=en',
'-importkeystore',
'-v',
'-srckeystore', keystoreFile,
'-destkeystore', tmpKeystoreFile,
'-srcstorepass', storePassword,
'-deststorepass', storePassword,
'-srcalias', alias,
'-destalias', alias,
'-srckeypass', keyPassword || storePassword,
'-noprompt'
], function (code, out, err) {
if (code) {
if (out.indexOf('java.security.UnrecoverableKeyException') != -1) {
return callback(new Error(__('Bad key password')));
}
return callback(new Error(out.trim()));
}
// remove the temp keystore
fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile);
callback(null, keyPassword);
});
} else {
callback(null, keyPassword);
}
});
}
},
'keystore': {
abbr: 'K',
callback: function (value) {
_t.conf.options['alias'].required = true;
_t.conf.options['store-password'].required = true;
},
desc: __('the location of the keystore file'),
hint: 'path',
order: 140,
prompt: function (callback) {
_t.conf.options['key-password'].required = true;
callback(fields.file({
promptLabel: __('Where is the __keystore file__ used to sign the app?'),
complete: true,
showHidden: true,
ignoreDirs: _t.ignoreDirs,
ignoreFiles: _t.ignoreFiles,
validate: _t.conf.options.keystore.validate.bind(_t)
}));
},
validate: function (keystoreFile, callback) {
if (!keystoreFile) {
callback(new Error(__('Please specify the path to your keystore file')));
} else {
keystoreFile = afs.resolvePath(keystoreFile);
if (!fs.existsSync(keystoreFile) || !fs.statSync(keystoreFile).isFile()) {
callback(new Error(__('Invalid keystore file')));
} else {
callback(null, keystoreFile);
}
}
}
},
'output-dir': {
abbr: 'O',
desc: __('the output directory when using %s', 'dist-playstore'.cyan),
hint: 'dir',
order: 180,
prompt: function (callback) {
callback(fields.file({
promptLabel: __('Where would you like the output APK file saved?'),
default: cli.argv['project-dir'] && afs.resolvePath(cli.argv['project-dir'], 'dist'),
complete: true,
showHidden: true,
ignoreDirs: _t.ignoreDirs,
ignoreFiles: /.*/,
validate: _t.conf.options['output-dir'].validate.bind(_t)
}));
},
validate: function (outputDir, callback) {
callback(outputDir || !_t.conf.options['output-dir'].required ? null : new Error(__('Invalid output directory')), outputDir);
}
},
'profiler-host': {
hidden: true
},
'store-password': {
abbr: 'P',
desc: __('the password for the keystore'),
hint: 'password',
order: 150,
prompt: function (callback) {
callback(fields.text({
next: function (err, value) {
return err && err.next || null;
},
promptLabel: __("What is the keystore's __password__?"),
password: true,
// if the password fails due to bad keystore file,
// we need to prompt for the keystore file again
repromptOnError: false,
validate: _t.conf.options['store-password'].validate.bind(_t)
}));
},
secret: true,
validate: function (storePassword, callback) {
if (!storePassword) {
return callback(new Error(__('Please specify a keystore password')));
}
// sanity check the keystore
_t.conf.options.keystore.validate(cli.argv.keystore, function (err, keystoreFile) {
if (err) {
// we have a bad --keystore arg
cli.argv.keystore = undefined;
return callback(err);
}
if (keystoreFile && _t.jdkInfo && _t.jdkInfo.executables.keytool) {
appc.subprocess.run(_t.jdkInfo.executables.keytool, [
'-J-Duser.language=en',
'-list',
'-v',
'-keystore', keystoreFile,
'-storepass', storePassword
], function (code, out, err) {
if (code) {
var msg = out.split('\n').shift().split('java.io.IOException:');
if (msg.length > 1) {
msg = msg[1].trim();
if (/invalid keystore format/i.test(msg)) {
msg = __('Invalid keystore file');
cli.argv.keystore = undefined;
_t.conf.options.keystore.required = true;
}
} else {
msg = out.trim();
}
return callback(new Error(msg));
}
// empty the alias array. it is important that we don't destory the original
// instance since it was passed by reference to the alias select list
while (_t.keystoreAliases.length) {
_t.keystoreAliases.pop();
}
var aliasRegExp = /Alias name\: (.+)/,
sigalgRegExp = /Signature algorithm name\: (.+)/;
out.split('\n\n').forEach(function (chunk) {
chunk = chunk.trim();
var m = chunk.match(aliasRegExp);
if (m) {
var sigalg = chunk.match(sigalgRegExp);
_t.keystoreAliases.push({
name: m[1],
sigalg: sigalg && sigalg[1]
});
}
});
if (_t.keystoreAliases.length == 0) {
cli.argv.keystore = undefined;
return callback(new Error(__('Keystore does not contain any certificates')));
} else if (!cli.argv.alias && _t.keystoreAliases.length == 1) {
cli.argv.alias = _t.keystoreAliases[0].name;
}
// check if this keystore requires a key password
var keystoreFile = cli.argv.keystore,
alias = cli.argv.alias,
tmpKeystoreFile = temp.path({ suffix: '.jks' });
if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) {
// the only way to test the key password is to export the cert
appc.subprocess.run(_t.jdkInfo.executables.keytool, [
'-J-Duser.language=en',
'-importkeystore',
'-v',
'-srckeystore', keystoreFile,
'-destkeystore', tmpKeystoreFile,
'-srcstorepass', storePassword,
'-deststorepass', storePassword,
'-srcalias', alias,
'-destalias', alias,
'-srckeypass', storePassword,
'-noprompt'
], function (code, out, err) {
if (code) {
if (out.indexOf('Alias <' + alias + '> does not exist') != -1) {
// bad alias, we'll let --alias find it again
_t.conf.options['alias'].required = true;
}
// since we have an error, force the key password to be required
_t.conf.options['key-password'].required = true;
} else {
// remove the temp keystore
fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile);
}
callback(null, storePassword);
});
} else {
callback(null, storePassword);
}
}.bind(_t));
} else {
callback(null, storePassword);
}
});
}
},
'target': {
abbr: 'T',
callback: function (value) {
// as soon as we know the target, toggle required options for validation
if (value === 'dist-playstore') {
_t.conf.options['alias'].required = true;
_t.conf.options['deploy-type'].values = ['production'];
_t.conf.options['device-id'].required = false;
_t.conf.options['keystore'].required = true;
_t.conf.options['output-dir'].required = true;
_t.conf.options['store-password'].required = true;
}
},
default: 'emulator',
desc: __('the target to build for'),
order: 120,
required: true,
values: _t.targets
}
}
};
// we need to map store-password to password for backwards compatibility
// because we needed to change it as to not conflict with the login
// password and be more descriptive compared to the --key-password
conf.options.password = appc.util.mix({
hidden: true
}, conf.options['store-password']);
delete conf.options.password.abbr;
callback(null, _t.conf = conf);
})(function (err, result) {
finished(result);
});
}.bind(this);
};
AndroidBuilder.prototype.validate = function validate(logger, config, cli) {
this.target = cli.argv.target;
this.deployType = /^device|emulator$/.test(this.target) && cli.argv['deploy-type'] ? cli.argv['deploy-type'] : this.deployTypes[this.target];
// ti.deploytype is deprecated and so we force the real deploy type
if (cli.tiapp.properties['ti.deploytype']) {
logger.warn(__('The %s tiapp.xml property has been deprecated, please use the %s option', 'ti.deploytype'.cyan, '--deploy-type'.cyan));
}
cli.tiapp.properties['ti.deploytype'] = { type: 'string', value: this.deployType };
// get the javac params
this.javacMaxMemory = cli.tiapp.properties['android.javac.maxmemory'] && cli.tiapp.properties['android.javac.maxmemory'].value || config.get('android.javac.maxMemory', '256M');
this.javacSource = cli.tiapp.properties['android.javac.source'] && cli.tiapp.properties['android.javac.source'].value || config.get('android.javac.source', '1.6');
this.javacTarget = cli.tiapp.properties['android.javac.target'] && cli.tiapp.properties['android.javac.target'].value || config.get('android.javac.target', '1.6');
this.dxMaxMemory = cli.tiapp.properties['android.dx.maxmemory'] && cli.tiapp.properties['android.dx.maxmemory'].value || config.get('android.dx.maxMemory', '1024M');
// manually inject the build profile settings into the tiapp.xml
switch (this.deployType) {
case 'production':
this.minifyJS = true;
this.encryptJS = true;
this.allowDebugging = false;
this.allowProfiling = false;
this.includeAllTiModules = false;
this.proguard = false;
break;
case 'test':
this.minifyJS = true;
this.encryptJS = true;
this.allowDebugging = true;
this.allowProfiling = true;
this.includeAllTiModules = false;
this.proguard = false;
break;
case 'development':
default:
this.minifyJS = false;
this.encryptJS = false;
this.allowDebugging = true;
this.allowProfiling = true;
this.includeAllTiModules = true;
this.proguard = false;
}
if (cli.tiapp.properties['ti.android.compilejs']) {
logger.warn(__('The %s tiapp.xml property has been deprecated, please use the %s option to bypass JavaScript minification', 'ti.android.compilejs'.cyan, '--skip-js-minify'.cyan));
}
if (cli.argv['skip-js-minify']) {
this.minifyJS = false;
}
// check the app name
if (cli.tiapp.name.indexOf('&') != -1) {
if (config.get('android.allowAppNameAmpersands', false)) {
logger.warn(__('The app name "%s" contains an ampersand (&) which will most likely cause problems.', cli.tiapp.name));
logger.warn(__('It is recommended that you define the app name using i18n strings.'));
logger.warn(__('Refer to %s for more information.', 'http://appcelerator.com/i18n-app-name'.cyan));
} else {
logger.error(__('The app name "%s" contains an ampersand (&) which will most likely cause problems.', cli.tiapp.name));
logger.error(__('It is recommended that you define the app name using i18n strings.'));
logger.error(__('Refer to %s for more information.', 'http://appcelerator.com/i18n-app-name'));
logger.error(__('To allow ampersands in the app name, run:'));
logger.error(' ti config android.allowAppNameAmpersands true\n');
process.exit(1);
}
}
// check the Android specific app id rules
if (!config.get('app.skipAppIdValidation') && !cli.tiapp.properties['ti.skipAppIdValidation']) {
if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_-]*(\.[a-zA-Z0-9_-]*)*)$/.test(cli.tiapp.id)) {
logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id));
logger.error(__('The app id must consist only of letters, numbers, dashes, and underscores.'));
logger.error(__('Note: Android does not allow dashes.'));
logger.error(__('The first character must be a letter or underscore.'));
logger.error(__("Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)") + '\n');
process.exit(1);
}
if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_]*(\.[a-zA-Z_]{1}[a-zA-Z0-9_]*)*)$/.test(cli.tiapp.id)) {
logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id));
logger.error(__('The app id must consist of letters, numbers, and underscores.'));
logger.error(__('The first character must be a letter or underscore.'));
logger.error(__('The first character after a period must not be a number.'));
logger.error(__("Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)") + '\n');
process.exit(1);
}
if (!ti.validAppId(cli.tiapp.id)) {
logger.error(__('Invalid app id "%s"', cli.tiapp.id));
logger.error(__('The app id must not contain Java reserved words.') + '\n');
process.exit(1);
}
}
// check the default unit