1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | 'use strict';
|
14 |
|
15 | const 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 |
|
35 | let envCache;
|
36 |
|
37 |
|
38 | const 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',
|
46 | '~',
|
47 | '~/Library/Android'
|
48 | ];
|
49 |
|
50 |
|
51 | let 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 |
|
63 | exports.androidPackageJson = function (json) {
|
64 | androidPackageJson = json;
|
65 | };
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | exports.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();
|
94 | } else {
|
95 | next(null, result);
|
96 | }
|
97 | });
|
98 | }, 1);
|
99 |
|
100 | queue.drain(function () {
|
101 |
|
102 | next(null, null);
|
103 | });
|
104 |
|
105 | queue.push([
|
106 |
|
107 | function (cb) {
|
108 | findSDK(config.get('android.sdkPath'), config, androidPackageJson, cb);
|
109 | },
|
110 |
|
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 |
|
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 |
|
140 |
|
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 |
|
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();
|
161 | } else {
|
162 | next(null, result);
|
163 | }
|
164 | });
|
165 | }, 1);
|
166 |
|
167 | queue.drain(function () {
|
168 |
|
169 | next(null, null);
|
170 | });
|
171 |
|
172 | queue.push([
|
173 |
|
174 | function (cb) {
|
175 | findNDK(config.get('android.ndkPath'), config, cb);
|
176 | },
|
177 |
|
178 | function (cb) {
|
179 | findNDK(process.env.ANDROID_NDK, config, cb);
|
180 | },
|
181 |
|
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 |
|
204 |
|
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 |
|
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 |
|
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 |
|
231 | }
|
232 | });
|
233 | },
|
234 |
|
235 | linux64bit: function (next) {
|
236 |
|
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 |
|
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 |
|
290 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 = {};
|
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 |
|
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 |
|
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 |
|
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 |
|
715 | exports.findSDK = findSDK;
|
716 |
|
717 | function findSDK(dir, config, androidPackageJson, callback) {
|
718 | if (!dir) {
|
719 | return callback(true);
|
720 | }
|
721 |
|
722 | dir = afs.resolvePath(dir);
|
723 |
|
724 |
|
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 |
|
761 |
|
762 |
|
763 | let buildToolsSupported = false;
|
764 | if (fs.existsSync(buildToolsDir)) {
|
765 | let ver = config.get('android.buildTools.selectedVersion');
|
766 | if (!ver) {
|
767 |
|
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 |
|
780 | break;
|
781 | }
|
782 | }
|
783 | }
|
784 |
|
785 |
|
786 |
|
787 | if (!ver && (len > 0)) {
|
788 | ver = files[len - 1];
|
789 | buildToolsSupported = false;
|
790 | }
|
791 | }
|
792 | if (ver) {
|
793 |
|
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 |
|
808 | result.buildTools = {
|
809 | path: path.join(buildToolsDir, ver),
|
810 | notInstalled: true,
|
811 | version: ver
|
812 | };
|
813 | }
|
814 | }
|
815 | }
|
816 |
|
817 |
|
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 |
|
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 |
|
840 | if (!fs.existsSync(executables.adb) || !fs.existsSync(file)) {
|
841 | return callback(true);
|
842 | }
|
843 |
|
844 |
|
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 |
|
872 | function findNDK(dir, config, callback) {
|
873 | if (!dir) {
|
874 | return callback(true);
|
875 | }
|
876 |
|
877 |
|
878 | dir = afs.resolvePath(dir);
|
879 |
|
880 | if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
881 | return callback(true);
|
882 | }
|
883 |
|
884 |
|
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 |
|
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 |
|
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 |
|
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 |
|
930 | function isDir(dir) {
|
931 | try {
|
932 | return fs.statSync(dir).isDirectory();
|
933 | } catch (e) {
|
934 |
|
935 | }
|
936 | return false;
|
937 | }
|
938 |
|
939 | function isFile(file) {
|
940 | try {
|
941 | return fs.statSync(file).isFile();
|
942 | } catch (e) {
|
943 |
|
944 | }
|
945 | return false;
|
946 | }
|
947 |
|
948 | function 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 |
|
964 | function loadPlatform(dir, systemImages) {
|
965 |
|
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 |
|
973 | const sdkProps = readProps(path.join(dir, 'sdk.properties'));
|
974 |
|
975 |
|
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 |
|
1023 | function loadAddon(dir, platforms, _systemImages) {
|
1024 |
|
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 |
|
1056 | function versionStringComparer(text1, text2) {
|
1057 |
|
1058 | const array1 = text1.split('.');
|
1059 | const array2 = text2.split('.');
|
1060 |
|
1061 |
|
1062 |
|
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 | }
|