UNPKG

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