UNPKG

37.1 kBJavaScriptView Raw
1/**
2 * Detects the Android development environment and its dependencies.
3 *
4 * @module lib/android
5 *
6 * @copyright
7 * Copyright (c) 2009-2017 by Appcelerator, Inc. All Rights Reserved.
8 *
9 * @license
10 * Licensed under the terms of the Apache Public License
11 * Please see the LICENSE included with this distribution for details.
12 */
13'use strict';
14
15const fs = require('fs'),
16 path = require('path'),
17 async = require('async'),
18 appc = require('node-appc'),
19 manifestJson = appc.pkginfo.manifest(module),
20 i18n = appc.i18n(__dirname),
21 __ = i18n.__,
22 __n = i18n.__n,
23 afs = appc.fs,
24 run = appc.subprocess.run,
25 findExecutable = appc.subprocess.findExecutable,
26 exe = process.platform === 'win32' ? '.exe' : '',
27 cmd = process.platform === 'win32' ? '.cmd' : '',
28 bat = process.platform === 'win32' ? '.bat' : '',
29 commandPrefix = process.env.APPC_ENV ? 'appc ' : '',
30 requiredSdkTools = {
31 adb: exe,
32 emulator: exe,
33 mksdcard: exe,
34 zipalign: exe,
35 aapt: exe,
36 aidl: exe,
37 dx: bat
38 },
39 pkgPropRegExp = /^([^=]*)=\s*(.+)$/;
40
41let envCache;
42
43// Common paths to scan for Android SDK/NDK
44const dirs = process.platform === 'win32'
45 ? [ '%SystemDrive%', '%ProgramFiles%', '%ProgramFiles(x86)%', '%CommonProgramFiles%', '~', '%LOCALAPPDATA%/Android' ]
46 : [
47 '/opt',
48 '/opt/local',
49 '/usr',
50 '/usr/local',
51 '/usr/local/share', // homebrew cask installs sdk/ndk (symlinks) to /usr/local/share/android-(sdk|ndk)
52 '~',
53 '~/Library/Android' // Android Studio installs the NDK to ~/Library/Android/Sdk/ndk-bundle
54 ];
55
56// need to find the android module and its package.json
57let androidPackageJson = {};
58(function findPackageJson(dir) {
59 if (dir !== '/') {
60 const file = path.join(dir, 'android', 'package.json');
61 if (fs.existsSync(file)) {
62 androidPackageJson = require(file);
63 } else {
64 findPackageJson(path.dirname(dir));
65 }
66 }
67}(path.join(__dirname, '..', '..', '..')));
68// allow overridding for tests
69exports.androidPackageJson = function (json) {
70 androidPackageJson = json;
71};
72
73/**
74 * Detects current Android environment.
75 * @param {Object} config - The CLI config object
76 * @param {Object} opts - Detect options
77 * @param {Boolean} [opts.bypassCache=false] - Bypasses the Android environment detection cache and re-queries the system
78 * @param {Function} finished - Callback when detection is finished
79 * @returns {void}
80 */
81exports.detect = function detect(config, opts, finished) {
82 opts || (opts = {});
83
84 if (envCache && !opts.bypassCache) {
85 return finished(envCache);
86 }
87
88 async.parallel({
89 jdk: function (next) {
90 appc.jdk.detect(config, opts, function (results) {
91 next(null, results);
92 });
93 },
94
95 sdk: function (next) {
96 var queue = async.queue(function (task, callback) {
97 task(function (err, result) {
98 if (err) {
99 callback(); // go to next item in the queue
100 } else {
101 next(null, result);
102 }
103 });
104 }, 1);
105
106 queue.drain(function () {
107 // we have completely exhausted all search paths
108 next(null, null);
109 });
110
111 queue.push([
112 // first let's check the config's value
113 function (cb) {
114 findSDK(config.get('android.sdkPath'), config, androidPackageJson, cb);
115 },
116 // try the environment variables
117 function (cb) {
118 findSDK(process.env.ANDROID_SDK_ROOT, config, androidPackageJson, cb);
119 },
120 function (cb) {
121 findSDK(process.env.ANDROID_SDK, config, androidPackageJson, cb);
122 },
123 // try finding the 'adb' executable
124 function (cb) {
125 findExecutable([ config.get('android.executables.adb'), 'adb' + exe ], function (err, result) {
126 if (err) {
127 cb(err);
128 } else {
129 findSDK(path.resolve(result, '..', '..'), config, androidPackageJson, cb);
130 }
131 });
132 }
133 ]);
134
135 dirs.forEach(function (dir) {
136 dir = afs.resolvePath(dir);
137 try {
138 fs.existsSync(dir) && fs.readdirSync(dir).forEach(function (name) {
139 var subdir = path.join(dir, name);
140 if (/android|sdk/i.test(name) && fs.existsSync(subdir) && fs.statSync(subdir).isDirectory()) {
141 queue.push(function (cb) {
142 findSDK(subdir, config, androidPackageJson, cb);
143 });
144
145 // this dir may be the Android SDK, but just in case,
146 // let's see if there's an Android folder in this one
147 fs.statSync(subdir).isDirectory() && fs.readdirSync(subdir).forEach(function (name) {
148 if (/android/i.test(name)) {
149 queue.push(function (cb) {
150 findSDK(path.join(subdir, name), config, androidPackageJson, cb);
151 });
152 }
153 });
154 }
155 });
156 } catch (e) {
157 // Ignore
158 }
159 });
160 },
161
162 ndk: function (next) {
163 var queue = async.queue(function (task, callback) {
164 task(function (err, result) {
165 if (err) {
166 callback(); // go to next item in the queue
167 } else {
168 next(null, result);
169 }
170 });
171 }, 1);
172
173 queue.drain(function () {
174 // we have completely exhausted all search paths
175 next(null, null);
176 });
177
178 queue.push([
179 // first let's check the config's value
180 function (cb) {
181 findNDK(config.get('android.ndkPath'), config, cb);
182 },
183 // try the environment variable
184 function (cb) {
185 findNDK(process.env.ANDROID_NDK, config, cb);
186 },
187 // try finding the 'ndk-build' executable
188 function (cb) {
189 findExecutable([ config.get('android.executables.ndkbuild'), 'ndk-build' + cmd ], function (err, result) {
190 if (err) {
191 cb(err);
192 } else {
193 findNDK(path.dirname(result), config, cb);
194 }
195 });
196 }
197 ]);
198
199 dirs.forEach(function (dir) {
200 dir = afs.resolvePath(dir);
201 try {
202 fs.existsSync(dir) && fs.readdirSync(dir).forEach(function (name) {
203 var subdir = path.join(dir, name);
204 if (/android|sdk/i.test(name)) {
205 queue.push(function (cb) {
206 findNDK(subdir, config, cb);
207 });
208
209 // Check under NDK side-by-side directory which contains multiple NDK installations.
210 // Each subfolder is named after the version of NDK installed under it. Favor newest version.
211 const ndkSideBySidePath = path.join(subdir, 'ndk');
212 if (fs.existsSync(ndkSideBySidePath) && fs.statSync(ndkSideBySidePath).isDirectory()) {
213 const fileNames = fs.readdirSync(ndkSideBySidePath);
214 fileNames.sort((text1, text2) => {
215 // Flip result to sort in descending order. (ie: Highest version is first.)
216 return versionStringComparer(text1, text2) * (-1);
217 });
218 for (const nextFileName of fileNames) {
219 const nextFilePath = path.join(ndkSideBySidePath, nextFileName);
220 queue.push(function (cb) {
221 findNDK(nextFilePath, config, cb);
222 });
223 }
224 }
225
226 // Android Studio used to install under Android SDK subfolder "ndk-bundle". (Deprecated in 2019.)
227 const ndkBundlePath = path.join(subdir, 'ndk-bundle');
228 if (fs.existsSync(ndkBundlePath) && fs.statSync(ndkBundlePath).isDirectory()) {
229 queue.push(function (cb) {
230 findNDK(ndkBundlePath, config, cb);
231 });
232 }
233 }
234 });
235 } catch (e) {
236 // Ignore
237 }
238 });
239 },
240
241 linux64bit: function (next) {
242 // detect if we're using a 64-bit Linux OS that's missing 32-bit libraries
243 if (process.platform === 'linux' && process.arch === 'x64') {
244 var result = {
245 libGL: fs.existsSync('/usr/lib/libGL.so'),
246 i386arch: null,
247 'libc6:i386': null,
248 'libncurses5:i386': null,
249 'libstdc++6:i386': null,
250 'zlib1g:i386': null,
251 glibc: null,
252 libstdcpp: null
253 };
254 async.parallel([
255 function (cb) {
256 findExecutable([ config.get('linux.dpkg'), 'dpkg' ], function (err, dpkg) {
257 if (err || !dpkg) {
258 return cb();
259 }
260
261 var archs = {};
262 run(dpkg, '--print-architecture', function (code, stdout, stderr) {
263 stdout.split('\n').forEach(function (line) {
264 (line = line.trim()) && (archs[line] = 1);
265 });
266 run(dpkg, '--print-foreign-architectures', function (code, stdout, stderr) {
267 stdout.split('\n').forEach(function (line) {
268 (line = line.trim()) && (archs[line] = 1);
269 });
270
271 // now that we have the architectures, make sure we have the i386 architecture
272 result.i386arch = !!archs.i386;
273 cb();
274 });
275 });
276 });
277 },
278 function (cb) {
279 findExecutable([ config.get('linux.dpkgquery'), 'dpkg-query' ], function (err, dpkgquery) {
280 if (err || !dpkgquery) {
281 return cb();
282 }
283
284 async.each(
285 [ 'libc6:i386', 'libncurses5:i386', 'libstdc++6:i386', 'zlib1g:i386' ],
286 function (pkg, next) {
287 run(dpkgquery, [ '-l', pkg ], function (code, out, err) {
288 result[pkg] = false;
289 if (!code) {
290 var lines = out.split('\n'),
291 i = 0,
292 l = lines.length;
293 for (; i < l; i++) {
294 if (lines[i].indexOf(pkg) !== -1) {
295 // we look for "ii" which means we want the "desired action"
296 // to be "installed" and the "status" to be "installed"
297 if (lines[i].indexOf('ii') === 0) {
298 result[pkg] = true;
299 }
300 break;
301 }
302 }
303 }
304 next();
305 });
306 },
307 function () {
308 cb();
309 }
310 );
311 });
312 },
313 function (cb) {
314 findExecutable([ config.get('linux.rpm'), 'rpm' ], function (err, rpm) {
315 if (err || !rpm) {
316 return cb();
317 }
318
319 run(rpm, '-qa', function (code, stdout, stderr) {
320 stdout.split('\n').forEach(function (line) {
321 if (/^glibc-/.test(line)) {
322 if (/\.i[36]86$/.test(line)) {
323 result.glibc = true;
324 } else if (result.glibc !== true) {
325 result.glibc = false;
326 }
327 }
328 if (/^libstdc\+\+-/.test(line)) {
329 if (/\.i[36]86$/.test(line)) {
330 result.libstdcpp = true;
331 } else if (result.libstdcpp !== true) {
332 result.libstdcpp = false;
333 }
334 }
335 });
336 cb();
337 });
338 });
339 }
340 ], function () {
341 next(null, result);
342 });
343 } else {
344 next(null, null);
345 }
346 }
347
348 }, function (err, results) {
349 var sdkHome = process.env.ANDROID_SDK_HOME && afs.resolvePath(process.env.ANDROID_SDK_HOME),
350 jdkInfo = results.jdk;
351
352 delete results.jdk;
353
354 results.home = sdkHome && fs.existsSync(sdkHome) && fs.statSync(sdkHome).isDirectory() ? sdkHome : afs.resolvePath('~/.android');
355 results.detectVersion = '2.0';
356 results.vendorDependencies = androidPackageJson.vendorDependencies;
357 results.targets = {};
358 results.avds = [];
359 results.issues = [];
360
361 function finalize() {
362 finished(envCache = results);
363 }
364
365 if (!jdkInfo.home) {
366 results.issues.push({
367 id: 'ANDROID_JDK_NOT_FOUND',
368 type: 'error',
369 message: __('JDK (Java Development Kit) not found.') + '\n'
370 + __('If you already have installed the JDK, verify your __JAVA_HOME__ environment variable is correctly set.') + '\n'
371 + __('The JDK can be downloaded and installed from %s.', '__https://www.oracle.com/technetwork/java/javase/downloads/index.html__')
372 });
373 results.sdk = null;
374 return finalize();
375 }
376
377 if (process.platform === 'win32' && jdkInfo.home.indexOf('&') !== -1) {
378 results.issues.push({
379 id: 'ANDROID_JDK_PATH_CONTAINS_AMPERSANDS',
380 type: 'error',
381 message: __('The JDK (Java Development Kit) path must not contain ampersands (&) on Windows.') + '\n'
382 + __('Please move the JDK into a path without an ampersand and update the __JAVA_HOME__ environment variable.')
383 });
384 results.sdk = null;
385 return finalize();
386 }
387
388 if (results.linux64bit !== null) {
389 if (!results.linux64bit.libGL) {
390 results.issues.push({
391 id: 'ANDROID_MISSING_LIBGL',
392 type: 'warning',
393 message: __('Unable to locate an /usr/lib/libGL.so.') + '\n'
394 + __('Without the libGL library, the Android Emulator may not work properly.') + '\n'
395 + __('You may be able to fix it by reinstalling your graphics drivers and make sure it installs the 32-bit version.')
396 });
397 }
398 }
399
400 if (!results.ndk) {
401 results.issues.push({
402 id: 'ANDROID_NDK_NOT_FOUND',
403 type: 'warning',
404 message: __('Unable to locate an Android NDK.') + '\n'
405 + __('Without the NDK, you will not be able to build native Android Titanium modules.') + '\n'
406 + __('If you have already downloaded and installed the Android NDK, you can tell Titanium where the Android NDK is located by running \'%s\', otherwise you can install it by running \'%s\' or manually downloading from %s.',
407 '__' + commandPrefix + 'titanium config android.ndkPath /path/to/android-ndk__',
408 '__' + commandPrefix + 'titanium setup android__',
409 '__https://developer.android.com/ndk__')
410 });
411 }
412
413 // if we don't have an android sdk, then nothing else to do
414 if (!results.sdk) {
415 results.issues.push({
416 id: 'ANDROID_SDK_NOT_FOUND',
417 type: 'error',
418 message: __('Unable to locate an Android SDK.') + '\n'
419 + __('If you have already downloaded and installed the Android SDK, you can tell Titanium where the Android SDK is located by running \'%s\', otherwise you can install it by running \'%s\' or manually downloading from %s.',
420 '__' + commandPrefix + 'titanium config android.sdkPath /path/to/android-sdk__',
421 '__' + commandPrefix + 'titanium setup android__',
422 '__https://developer.android.com/studio__')
423 });
424 return finalize();
425 }
426
427 if (results.sdk.buildTools.tooNew === 'maybe') {
428 results.issues.push({
429 id: 'ANDROID_BUILD_TOOLS_TOO_NEW',
430 type: 'warning',
431 message: '\n' + __('Android Build Tools %s are too new and may or may not work with Titanium.', results.sdk.buildTools.version) + '\n'
432 + __('If you encounter problems, select a supported version with:') + '\n'
433 + ' __' + commandPrefix + 'ti config android.buildTools.selectedVersion ##.##.##__'
434 + __('\n where ##.##.## is a version in ') + results.sdk.buildTools.path.split('/').slice(0, -1).join('/') + __(' that is ') + results.sdk.buildTools.maxSupported
435 });
436 }
437
438 if (!results.sdk.buildTools.supported) {
439 results.issues.push({
440 id: 'ANDROID_BUILD_TOOLS_NOT_SUPPORTED',
441 type: 'error',
442 message: createAndroidSdkInstallationErrorMessage(__('Android Build Tools %s are not supported by Titanium', results.sdk.buildTools.version))
443
444 });
445 }
446
447 if (results.sdk.buildTools.notInstalled) {
448 results.issues.push({
449 id: 'ANDROID_BUILD_TOOLS_CONFIG_SETTING_NOT_INSTALLED',
450 type: 'error',
451 message: createAndroidSdkInstallationErrorMessage(__('The selected version of Android SDK Build Tools (%s) are not installed. Please either remove the setting using %s or install it', results.sdk.buildTools.version, `${commandPrefix} ti config --remove android.buildTools.selectedVersion`))
452 });
453 }
454
455 // check if we're running Windows and if the sdk path contains ampersands
456 if (process.platform === 'win32' && results.sdk.path.indexOf('&') !== -1) {
457 results.issues.push({
458 id: 'ANDROID_SDK_PATH_CONTAINS_AMPERSANDS',
459 type: 'error',
460 message: __('The Android SDK path must not contain ampersands (&) on Windows.') + '\n'
461 + __('Please move the Android SDK into a path without an ampersand and re-run __' + commandPrefix + 'titanium setup android__.')
462 });
463 results.sdk = null;
464 return finalize();
465 }
466
467 // check if the sdk is missing any commands
468 var missing = Object.keys(requiredSdkTools).filter(cmd => !results.sdk.executables[cmd]);
469 if (missing.length && results.sdk.buildTools.supported) {
470 var dummyPath = path.join(path.resolve('/'), 'path', 'to', 'android-sdk'),
471 msg = '';
472
473 if (missing.length) {
474 msg += __n('Missing required Android SDK tool: %%s', 'Missing required Android SDK tools: %%s', missing.length, '__' + missing.join(', ') + '__') + '\n\n';
475 }
476
477 msg = createAndroidSdkInstallationErrorMessage(msg);
478
479 if (missing.length) {
480 msg += '\n' + __('You can also specify the exact location of these required tools by running:') + '\n';
481 missing.forEach(function (m) {
482 msg += ' ' + commandPrefix + 'ti config android.executables.' + m + ' "' + path.join(dummyPath, m + requiredSdkTools[m]) + '"\n';
483 });
484 }
485
486 msg += '\n' + __('If you need to, run "%s" to reconfigure the Titanium Android settings.', commandPrefix + 'titanium setup android');
487
488 results.issues.push({
489 id: 'ANDROID_SDK_MISSING_PROGRAMS',
490 type: 'error',
491 message: msg
492 });
493 }
494
495 /**
496 * Detect system images
497 */
498 var systemImages = {};
499 var systemImagesByPath = {};
500 var systemImagesDir = path.join(results.sdk.path, 'system-images');
501 if (isDir(systemImagesDir)) {
502 fs.readdirSync(systemImagesDir).forEach(function (platform) {
503 var platformDir = path.join(systemImagesDir, platform);
504 if (isDir(platformDir)) {
505 fs.readdirSync(platformDir).forEach(function (tag) {
506 var tagDir = path.join(platformDir, tag);
507 if (isDir(tagDir)) {
508 fs.readdirSync(tagDir).forEach(function (abi) {
509 var abiDir = path.join(tagDir, abi);
510 var props = readProps(path.join(abiDir, 'source.properties'));
511 if (props && props['AndroidVersion.ApiLevel'] && props['SystemImage.TagId'] && props['SystemImage.Abi']) {
512 var id = 'android-' + (props['AndroidVersion.CodeName'] || props['AndroidVersion.ApiLevel']);
513 var tag = props['SystemImage.TagId'];
514 var skinsDir = path.join(abiDir, 'skins');
515
516 systemImages[id] || (systemImages[id] = {});
517 systemImages[id][tag] || (systemImages[id][tag] = []);
518 systemImages[id][tag].push({
519 abi: props['SystemImage.Abi'],
520 skins: isDir(skinsDir) ? fs.readdirSync(skinsDir).map(name => {
521 return isFile(path.join(skinsDir, name, 'hardware.ini')) ? name : null;
522 }).filter(x => x) : []
523 });
524
525 systemImagesByPath[path.relative(results.sdk.path, abiDir)] = {
526 id: id,
527 tag: tag,
528 abi: abi
529 };
530 }
531 });
532 }
533 });
534 }
535 });
536 }
537
538 /**
539 * Detect targets
540 */
541 var platformsDir = path.join(results.sdk.path, 'platforms');
542 var platforms = [];
543 var platformsById = {};
544 if (isDir(platformsDir)) {
545 fs.readdirSync(platformsDir).forEach(function (name) {
546 var info = loadPlatform(path.join(platformsDir, name), systemImages);
547 if (info) {
548 platforms.push(info);
549 platformsById[info.id] = info;
550 }
551 });
552 }
553
554 var addonsDir = path.join(results.sdk.path, 'add-ons');
555 var addons = [];
556 if (isDir(addonsDir)) {
557 fs.readdirSync(addonsDir).forEach(function (name) {
558 var info = loadAddon(path.join(addonsDir, name), platforms, systemImages);
559 info && addons.push(info);
560 });
561 }
562
563 function sortFn(a, b) {
564 if (a.codename === null) {
565 if (b.codename !== null && a.apiLevel === b.apiLevel) {
566 // sort GA releases before preview releases
567 return -1;
568 }
569 } else if (a.apiLevel === b.apiLevel) {
570 return b.codename === null ? 1 : a.codename.localeCompare(b.codename);
571 }
572
573 return a.apiLevel - b.apiLevel;
574 }
575
576 var index = 1;
577 platforms.sort(sortFn).concat(addons.sort(sortFn)).forEach(function (platform) {
578 var abis = [];
579 if (platform.abis) {
580 Object.keys(platform.abis).forEach(function (type) {
581 platform.abis[type].forEach(function (abi) {
582 if (abis.indexOf(abi) === -1) {
583 abis.push(abi);
584 }
585 });
586 });
587 }
588
589 var info = {
590 id: platform.id,
591 abis: abis,
592 skins: platform.skins,
593 name: platform.name,
594 type: platform.type,
595 path: platform.path,
596 revision: platform.revision,
597 androidJar: platform.androidJar,
598 aidl: platform.aidl
599 };
600
601 if (platform.type === 'platform') {
602 info['api-level'] = platform.apiLevel;
603 info.sdk = platform.apiLevel;
604 info.version = platform.version;
605 info.supported = !~~platform.apiLevel || appc.version.satisfies(platform.apiLevel, androidPackageJson.vendorDependencies['android sdk'], true);
606 } else if (platform.type === 'add-on' && platform.basedOn) {
607 info.vendor = platform.vendor;
608 info.description = platform.description;
609 info.version = platform.basedOn.version || parseInt(String(platform.basedOn).replace(/^android-/, '')) || null;
610 info['based-on'] = {
611 'android-version': platform.basedOn.version,
612 'api-level': platform.basedOn.apiLevel
613 };
614 info.supported = !~~platform.basedOn.apiLevel || appc.version.satisfies(platform.basedOn.apiLevel, androidPackageJson.vendorDependencies['android sdk'], true);
615 info.libraries = {}; // not supported any more
616 }
617
618 results.targets[index++] = info;
619
620 if (!info.supported) {
621 results.issues.push({
622 id: 'ANDROID_API_TOO_OLD',
623 type: 'warning',
624 message: __('Android API %s is too old and is no longer supported by Titanium SDK %s.', '__' + info.name + ' (' + info.id + ')__', manifestJson.version) + '\n'
625 + __('The minimum supported Android API level by Titanium SDK %s is API level %s.', manifestJson.version, appc.version.parseMin(androidPackageJson.vendorDependencies['android sdk']))
626 });
627 } else if (info.supported === 'maybe') {
628 results.issues.push({
629 id: 'ANDROID_API_TOO_NEW',
630 type: 'warning',
631 message: __('Android API %s is too new and may or may not work with Titanium SDK %s.', '__' + info.name + ' (' + info.id + ')__', manifestJson.version) + '\n'
632 + __('The maximum supported Android API level by Titanium SDK %s is API level %s.', manifestJson.version, appc.version.parseMax(androidPackageJson.vendorDependencies['android sdk']))
633 });
634 }
635 });
636
637 // check that we found at least one target
638 if (!Object.keys(results.targets).length) {
639 results.issues.push({
640 id: 'ANDROID_NO_APIS',
641 type: 'error',
642 message: __('No Android APIs found.') + '\n'
643 + __('Run \'%s\' to install the latest Android APIs.', 'Android Studio')
644 });
645 }
646
647 // check that we found at least one valid target
648 if (!Object.keys(results.targets).some(t => !!results.targets[t].supported)) {
649 results.issues.push({
650 id: 'ANDROID_NO_VALID_APIS',
651 type: 'warning',
652 message: __('No valid Android APIs found that are supported by Titanium SDK %s.', manifestJson.version) + '\n'
653 + __('Run \'%s\' to install the latest Android APIs.', 'Android Studio')
654 });
655 }
656
657 // parse the avds
658 var avdDir = afs.resolvePath('~/.android/avd');
659 var iniRegExp = /^(.+)\.ini$/;
660 if (isDir(avdDir)) {
661 fs.readdirSync(avdDir).forEach(function (name) {
662 var m = name.match(iniRegExp);
663 if (!m) {
664 return;
665 }
666
667 var ini = readProps(path.join(avdDir, name));
668 if (!ini) {
669 return;
670 }
671
672 var q;
673 var p = isDir(ini.path) ? ini.path : (ini['path.rel'] && isDir(q = path.join(avdDir, ini['path.rel'])) ? q : null);
674 if (!p) {
675 return;
676 }
677
678 var config = readProps(path.join(p, 'config.ini'));
679 if (!config) {
680 return;
681 }
682
683 var sdcard = path.join(p, 'sdcard.img');
684 var target = null;
685 var sdk = null;
686 var apiLevel = null;
687
688 var info = config['image.sysdir.1'] && systemImagesByPath[config['image.sysdir.1'].replace(/\/$/, '')];
689 if (info) {
690 var platform = platformsById[info.id];
691 if (platform) {
692 target = platform.name + ' (API level ' + platform.apiLevel + ')';
693 sdk = platform.version;
694 apiLevel = platform.apiLevel;
695 }
696 }
697
698 results.avds.push({
699 type: 'avd',
700 id: config['AvdId'] || m[1],
701 name: config['avd.ini.displayname'] || m[1],
702 device: config['hw.device.name'] + ' (' + config['hw.device.manufacturer'] + ')',
703 path: p,
704 target: target,
705 abi: config['abi.type'],
706 skin: config['skin.name'],
707 sdcard: config['hw.sdCard'] === 'yes' && isFile(sdcard) ? sdcard : null,
708 googleApis: config['tag.id'] === 'google_apis',
709 'sdk-version': sdk,
710 'api-level': apiLevel
711 });
712 });
713 }
714
715 finalize();
716
717 function createAndroidSdkInstallationErrorMessage(message) {
718 if (!message) {
719 message = '';
720 } else if (message.length > 0) {
721 message += '\n';
722 }
723 message +=
724 __('Current installed Android SDK tools:') + '\n'
725 + ' Android SDK Tools: ' + (results.sdk.tools.version || 'not installed') + ' (Supported: ' + androidPackageJson.vendorDependencies['android tools'] + ')\n'
726 + ' Android SDK Platform Tools: ' + (results.sdk.platformTools.version || 'not installed') + ' (Supported: ' + androidPackageJson.vendorDependencies['android platform tools'] + ')\n'
727 + ' Android SDK Build Tools: ' + (results.sdk.buildTools.version || 'not installed') + ' (Supported: ' + androidPackageJson.vendorDependencies['android build tools'] + ')\n\n'
728 + __('Make sure you have the latest Android SDK Tools, Platform Tools, and Build Tools installed.') + '\n';
729 return message;
730 }
731 });
732};
733
734exports.findSDK = findSDK;
735
736function findSDK(dir, config, androidPackageJson, callback) {
737 if (!dir) {
738 return callback(true);
739 }
740
741 dir = afs.resolvePath(dir);
742
743 // check if the supplied directory exists and is actually a directory
744 if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
745 return callback(true);
746 }
747
748 const dxJarPath = path.join(dir, 'platform-tools', 'lib', 'dx.jar'),
749 proguardPath = path.join(dir, 'tools', 'proguard', 'lib', 'proguard.jar'),
750 emulatorPath = path.join(dir, 'emulator', 'emulator' + exe),
751 result = {
752 path: dir,
753 executables: {
754 adb: path.join(dir, 'platform-tools', 'adb' + exe),
755 android: null, // this tool has been deprecated
756 emulator: fs.existsSync(emulatorPath) ? emulatorPath : path.join(dir, 'tools', 'emulator' + exe),
757 mksdcard: path.join(dir, 'tools', 'mksdcard' + exe),
758 zipalign: path.join(dir, 'tools', 'zipalign' + exe),
759 // Android SDK Tools v21 and older puts aapt and aidl in the platform-tools dir.
760 // For SDK Tools v22 and later, they live in the build-tools/<ver> directory.
761 aapt: path.join(dir, 'platform-tools', 'aapt' + exe),
762 aidl: path.join(dir, 'platform-tools', 'aidl' + exe),
763 dx: path.join(dir, 'platform-tools', 'dx' + bat),
764 apksigner: null
765 },
766 dx: fs.existsSync(dxJarPath) ? dxJarPath : null,
767 proguard: fs.existsSync(proguardPath) ? proguardPath : null,
768 tools: {
769 path: null,
770 supported: null,
771 version: null
772 },
773 platformTools: {
774 path: null,
775 supported: null,
776 version: null
777 },
778 buildTools: {
779 path: null,
780 supported: null,
781 version: null,
782 tooNew: null,
783 maxSupported: null
784 }
785 },
786 tasks = {},
787 buildToolsDir = path.join(dir, 'build-tools');
788
789 /*
790 Determine build tools version to use based on either config setting
791 (android.buildTools.selectedVersion) or latest version
792 */
793 let buildToolsSupported = false;
794 if (fs.existsSync(buildToolsDir)) {
795 let ver = config.get('android.buildTools.selectedVersion');
796 if (!ver) {
797 // No selected version, so find the newest, supported build tools version
798 const ignoreDirs = new RegExp(config.get('cli.ignoreDirs'));
799 const ignoreFiles = new RegExp(config.get('cli.ignoreFiles'));
800 const files = fs.readdirSync(buildToolsDir).sort().reverse().filter(item => !(ignoreFiles.test(item) || ignoreDirs.test(item)));
801 const len = files.length;
802 let i = 0;
803 for (; i < len; i++) {
804 var isSupported = appc.version.satisfies(files[i], androidPackageJson.vendorDependencies['android build tools'], true);
805 if (isSupported) {
806 buildToolsSupported = isSupported;
807 ver = files[i];
808 if (buildToolsSupported === true) {
809 // The version found is fully supported (not set to 'maybe'). So, stop here.
810 break;
811 }
812 }
813 }
814
815 // If we've failed to find a build-tools version that Titanium supports up above,
816 // then grab the newest old version installed to be logged as unsupported later.
817 if (!ver && (len > 0)) {
818 ver = files[len - 1];
819 buildToolsSupported = false;
820 }
821 }
822 if (ver) {
823 // A selectedVersion specified or supported version has been found
824 let file = path.join(buildToolsDir, ver, 'source.properties');
825 if (fs.existsSync(file) && fs.statSync(path.join(buildToolsDir, ver)).isDirectory()) {
826 var m = fs.readFileSync(file).toString().match(/Pkg\.Revision\s*?=\s*?([^\s]+)/);
827 if (m) {
828 result.buildTools = {
829 path: path.join(buildToolsDir, ver),
830 supported: appc.version.satisfies(m[1], androidPackageJson.vendorDependencies['android build tools'], true),
831 version: m[1],
832 tooNew: buildToolsSupported,
833 maxSupported: appc.version.parseMax(androidPackageJson.vendorDependencies['android build tools'], true)
834 };
835 fs.existsSync(file = path.join(buildToolsDir, ver, 'aapt' + exe)) && (result.executables.aapt = file);
836 fs.existsSync(file = path.join(buildToolsDir, ver, 'aidl' + exe)) && (result.executables.aidl = file);
837 fs.existsSync(file = path.join(buildToolsDir, ver, 'apksigner' + bat)) && (result.executables.apksigner = file);
838 fs.existsSync(file = path.join(buildToolsDir, ver, 'dx' + bat)) && (result.executables.dx = file);
839 fs.existsSync(file = path.join(buildToolsDir, ver, 'lib', 'dx.jar')) && (result.dx = file);
840 fs.existsSync(file = path.join(buildToolsDir, ver, 'zipalign' + exe)) && (result.executables.zipalign = file);
841 }
842 } else {
843 // build tools don't exist at the given location
844 result.buildTools = {
845 path: path.join(buildToolsDir, ver),
846 notInstalled: true,
847 version: ver
848 };
849 }
850 }
851 }
852
853 // see if this sdk has all the executables we need
854 Object.keys(requiredSdkTools).forEach(function (cmd) {
855 tasks[cmd] = function (next) {
856 findExecutable([
857 config.get('android.executables.' + cmd),
858 result.executables[cmd]
859 ], function (err, r) {
860 next(null, !err && r ? r : null);
861 });
862 };
863 });
864
865 async.parallel(tasks, function (err, executables) {
866 appc.util.mix(result.executables, executables);
867
868 // check that we have all required sdk programs
869 if (Object.keys(requiredSdkTools).every(cmd => !executables[cmd])) {
870 return callback(true);
871 }
872
873 var file = path.join(dir, 'tools', 'source.properties');
874
875 // check if this directory contains an android sdk
876 if (!fs.existsSync(executables.adb) || !fs.existsSync(file)) {
877 return callback(true);
878 }
879
880 // looks like we found an android sdk, check what version
881 if (fs.existsSync(file)) {
882 const m = fs.readFileSync(file).toString().match(/Pkg\.Revision\s*?=\s*?([^\s]+)/);
883 if (m) {
884 result.tools = {
885 path: path.join(dir, 'tools'),
886 supported: appc.version.satisfies(m[1], androidPackageJson.vendorDependencies['android tools'], true),
887 version: m[1]
888 };
889 }
890 }
891
892 file = path.join(dir, 'platform-tools', 'source.properties');
893 if (fs.existsSync(file)) {
894 const m = fs.readFileSync(file).toString().match(/Pkg\.Revision\s*?=\s*?([^\s]+)/);
895 if (m) {
896 result.platformTools = {
897 path: path.join(dir, 'platform-tools'),
898 supported: appc.version.satisfies(m[1], androidPackageJson.vendorDependencies['android platform tools'], true),
899 version: m[1]
900 };
901 }
902 }
903
904 callback(null, result);
905 });
906}
907
908function findNDK(dir, config, callback) {
909 if (!dir) {
910 return callback(true);
911 }
912
913 // check if the supplied directory exists and is actually a directory
914 dir = afs.resolvePath(dir);
915
916 if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
917 return callback(true);
918 }
919
920 // check that the ndk files/folders exist
921 const things = [ 'ndk-build' + cmd, 'build', 'prebuilt', 'platforms' ];
922 if (!things.every(thing => fs.existsSync(path.join(dir, thing)))) {
923 return callback(true);
924 }
925
926 // try to determine the version
927 let version;
928 const sourceProps = path.join(dir, 'source.properties');
929 if (fs.existsSync(sourceProps)) {
930 const m = fs.readFileSync(sourceProps).toString().match(/Pkg\.Revision\s*=\s*(.+)/m);
931 if (m && m[1]) {
932 version = m[1].trim();
933 }
934 }
935
936 if (!version) {
937 // try the release.txt
938 let releasetxt;
939 fs.readdirSync(dir).some(function (file) {
940 if (file.toLowerCase() === 'release.txt') {
941 releasetxt = path.join(dir, file);
942 return true;
943 }
944 return false;
945 });
946
947 if (releasetxt && fs.existsSync(releasetxt)) {
948 version = fs.readFileSync(releasetxt).toString().split(/\r?\n/).shift().trim();
949 }
950 }
951
952 if (!version) {
953 // no version, not an ndk
954 return callback(true);
955 }
956
957 callback(null, {
958 path: dir,
959 executables: {
960 ndkbuild: path.join(dir, 'ndk-build' + cmd)
961 },
962 version: version
963 });
964}
965
966function isDir(dir) {
967 try {
968 return fs.statSync(dir).isDirectory();
969 } catch (e) {
970 // squeltch
971 }
972 return false;
973}
974
975function isFile(file) {
976 try {
977 return fs.statSync(file).isFile();
978 } catch (e) {
979 // squeltch
980 }
981 return false;
982}
983
984function readProps(file) {
985 if (!isFile(file)) {
986 return null;
987 }
988
989 const props = {};
990 fs.readFileSync(file).toString().split(/\r?\n/).forEach(function (line) {
991 const m = line.match(pkgPropRegExp);
992 if (m) {
993 props[m[1].trim()] = m[2].trim();
994 }
995 });
996
997 return props;
998}
999
1000function loadPlatform(dir, systemImages) {
1001 // read in the properties
1002 const sourceProps = readProps(path.join(dir, 'source.properties'));
1003 const apiLevel = sourceProps ? ~~sourceProps['AndroidVersion.ApiLevel'] : null;
1004 if (!sourceProps || !apiLevel || !isFile(path.join(dir, 'build.prop'))) {
1005 return null;
1006 }
1007
1008 // read in the sdk properties, if exists
1009 const sdkProps = readProps(path.join(dir, 'sdk.properties'));
1010
1011 // detect the available skins
1012 const skinsDir = path.join(dir, 'skins');
1013 const skins = isDir(skinsDir) ? fs.readdirSync(skinsDir).map(name => {
1014 return isFile(path.join(skinsDir, name, 'hardware.ini')) ? name : null;
1015 }).filter(x => x) : [];
1016 let defaultSkin = sdkProps && sdkProps['sdk.skin.default'];
1017 if (skins.indexOf(defaultSkin) === -1 && skins.indexOf(defaultSkin = 'WVGA800') === -1) {
1018 defaultSkin = skins[skins.length - 1] || null;
1019 }
1020
1021 const apiName = sourceProps['AndroidVersion.CodeName'] || apiLevel;
1022 const id = `android-${apiName}`;
1023
1024 const abis = {};
1025 if (systemImages[id]) {
1026 Object.keys(systemImages[id]).forEach(function (type) {
1027 systemImages[id][type].forEach(function (info) {
1028 abis[type] || (abis[type] = []);
1029 abis[type].push(info.abi);
1030
1031 info.skins.forEach(function (skin) {
1032 if (skins.indexOf(skin) === -1) {
1033 skins.push(skin);
1034 }
1035 });
1036 });
1037 });
1038 }
1039
1040 let tmp;
1041 return {
1042 id: id,
1043 name: 'Android ' + sourceProps['Platform.Version'] + (sourceProps['AndroidVersion.CodeName'] ? ' (Preview)' : ''),
1044 type: 'platform',
1045 apiLevel: apiLevel,
1046 codename: sourceProps['AndroidVersion.CodeName'] || null,
1047 revision: +sourceProps['Layoutlib.Revision'] || null,
1048 path: dir,
1049 version: sourceProps['Platform.Version'],
1050 abis: abis,
1051 skins: skins,
1052 defaultSkin: defaultSkin,
1053 minToolsRev: +sourceProps['Platform.MinToolsRev'] || null,
1054 androidJar: isFile(tmp = path.join(dir, 'android.jar')) ? tmp : null,
1055 aidl: isFile(tmp = path.join(dir, 'framework.aidl')) ? tmp : null
1056 };
1057}
1058
1059function loadAddon(dir, platforms, systemImages) {
1060 // read in the properties
1061 const sourceProps = readProps(path.join(dir, 'source.properties'));
1062 const apiLevel = sourceProps ? ~~sourceProps['AndroidVersion.ApiLevel'] : null;
1063 if (!sourceProps || !apiLevel || !sourceProps['Addon.VendorDisplay'] || !sourceProps['Addon.NameDisplay']) {
1064 return null;
1065 }
1066
1067 const basedOn = platforms.find(p => p.codename === null && p.apiLevel === apiLevel);
1068
1069 return {
1070 id: sourceProps['Addon.VendorDisplay'] + ':' + sourceProps['Addon.NameDisplay'] + ':' + apiLevel,
1071 name: sourceProps['Addon.NameDisplay'],
1072 type: 'add-on',
1073 vendor: sourceProps['Addon.VendorDisplay'],
1074 description: sourceProps['Pkg.Desc'],
1075 apiLevel: apiLevel,
1076 revision: +sourceProps['Pkg.Revision'] || null,
1077 codename: sourceProps['AndroidVersion.CodeName'] || null,
1078 path: dir,
1079 basedOn: basedOn ? {
1080 version: basedOn.version,
1081 apiLevel: basedOn.apiLevel
1082 } : null,
1083 abis: basedOn && basedOn.abis || null,
1084 skins: basedOn && basedOn.skins || null,
1085 defaultSkin: basedOn && basedOn.defaultSkin || null,
1086 minToolsRev: basedOn && basedOn.minToolsRev || null,
1087 androidJar: basedOn && basedOn.androidJar || null,
1088 aidl: basedOn && basedOn.aidl || null
1089 };
1090}
1091
1092function versionStringComparer(text1, text2) {
1093 // Split strings into version component arrays. Example: '1.2.3' -> ['1', '2', '3']
1094 const array1 = text1.split('.');
1095 const array2 = text2.split('.');
1096
1097 // Compare the 2 given strings by their numeric components.
1098 // If they match numerically, then do a string comparison.
1099 const maxLength = Math.max(array1.length, array2.length);
1100 for (let index = 0; index < maxLength; index++) {
1101 const value1 = (index < array1.length) ? (Number.parseInt(array1[index], 10) || 0) : 0;
1102 const value2 = (index < array2.length) ? (Number.parseInt(array2[index], 10) || 0) : 0;
1103 const delta = value1 - value2;
1104 if (delta !== 0) {
1105 return delta;
1106 }
1107 }
1108 return text1.localeCompare(text2);
1109}