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 | 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 |
|
41 | let envCache;
|
42 |
|
43 |
|
44 | const 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',
|
52 | '~',
|
53 | '~/Library/Android'
|
54 | ];
|
55 |
|
56 |
|
57 | let 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 |
|
69 | exports.androidPackageJson = function (json) {
|
70 | androidPackageJson = json;
|
71 | };
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | exports.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();
|
100 | } else {
|
101 | next(null, result);
|
102 | }
|
103 | });
|
104 | }, 1);
|
105 |
|
106 | queue.drain(function () {
|
107 |
|
108 | next(null, null);
|
109 | });
|
110 |
|
111 | queue.push([
|
112 |
|
113 | function (cb) {
|
114 | findSDK(config.get('android.sdkPath'), config, androidPackageJson, cb);
|
115 | },
|
116 |
|
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 |
|
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 |
|
146 |
|
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 |
|
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();
|
167 | } else {
|
168 | next(null, result);
|
169 | }
|
170 | });
|
171 | }, 1);
|
172 |
|
173 | queue.drain(function () {
|
174 |
|
175 | next(null, null);
|
176 | });
|
177 |
|
178 | queue.push([
|
179 |
|
180 | function (cb) {
|
181 | findNDK(config.get('android.ndkPath'), config, cb);
|
182 | },
|
183 |
|
184 | function (cb) {
|
185 | findNDK(process.env.ANDROID_NDK, config, cb);
|
186 | },
|
187 |
|
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 |
|
210 |
|
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 |
|
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 |
|
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 |
|
237 | }
|
238 | });
|
239 | },
|
240 |
|
241 | linux64bit: function (next) {
|
242 |
|
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 |
|
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 |
|
296 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 = {};
|
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 |
|
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 |
|
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 |
|
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 |
|
734 | exports.findSDK = findSDK;
|
735 |
|
736 | function findSDK(dir, config, androidPackageJson, callback) {
|
737 | if (!dir) {
|
738 | return callback(true);
|
739 | }
|
740 |
|
741 | dir = afs.resolvePath(dir);
|
742 |
|
743 |
|
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,
|
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 |
|
760 |
|
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 |
|
791 |
|
792 |
|
793 | let buildToolsSupported = false;
|
794 | if (fs.existsSync(buildToolsDir)) {
|
795 | let ver = config.get('android.buildTools.selectedVersion');
|
796 | if (!ver) {
|
797 |
|
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 |
|
810 | break;
|
811 | }
|
812 | }
|
813 | }
|
814 |
|
815 |
|
816 |
|
817 | if (!ver && (len > 0)) {
|
818 | ver = files[len - 1];
|
819 | buildToolsSupported = false;
|
820 | }
|
821 | }
|
822 | if (ver) {
|
823 |
|
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 |
|
844 | result.buildTools = {
|
845 | path: path.join(buildToolsDir, ver),
|
846 | notInstalled: true,
|
847 | version: ver
|
848 | };
|
849 | }
|
850 | }
|
851 | }
|
852 |
|
853 |
|
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 |
|
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 |
|
876 | if (!fs.existsSync(executables.adb) || !fs.existsSync(file)) {
|
877 | return callback(true);
|
878 | }
|
879 |
|
880 |
|
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 |
|
908 | function findNDK(dir, config, callback) {
|
909 | if (!dir) {
|
910 | return callback(true);
|
911 | }
|
912 |
|
913 |
|
914 | dir = afs.resolvePath(dir);
|
915 |
|
916 | if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
917 | return callback(true);
|
918 | }
|
919 |
|
920 |
|
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 |
|
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 |
|
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 |
|
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 |
|
966 | function isDir(dir) {
|
967 | try {
|
968 | return fs.statSync(dir).isDirectory();
|
969 | } catch (e) {
|
970 |
|
971 | }
|
972 | return false;
|
973 | }
|
974 |
|
975 | function isFile(file) {
|
976 | try {
|
977 | return fs.statSync(file).isFile();
|
978 | } catch (e) {
|
979 |
|
980 | }
|
981 | return false;
|
982 | }
|
983 |
|
984 | function 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 |
|
1000 | function loadPlatform(dir, systemImages) {
|
1001 |
|
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 |
|
1009 | const sdkProps = readProps(path.join(dir, 'sdk.properties'));
|
1010 |
|
1011 |
|
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 |
|
1059 | function loadAddon(dir, platforms, systemImages) {
|
1060 |
|
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 |
|
1092 | function versionStringComparer(text1, text2) {
|
1093 |
|
1094 | const array1 = text1.split('.');
|
1095 | const array2 = text2.split('.');
|
1096 |
|
1097 |
|
1098 |
|
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 | }
|