diff --git a/.eslintrc b/.eslintrc index 655e3fad978..f7d4a1a19d6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,17 +1,20 @@ { "extends": [ "axway/env-node"], "parserOptions": { - "sourceType": "script" + "sourceType": "module" }, "rules": { "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": true }], - "promise/catch-or-return": [ "warn", { "terminationMethod": [ "catch", "finally" ]} ] + "promise/catch-or-return": [ "warn", { "terminationMethod": [ "catch", "finally" ]} ], + "promise/always-return": "off", + "promise/no-callback-in-promise": "off", + "security/detect-child-process": "off" }, "overrides": [ { "files": [ "android/runtime/common/src/js/**/*.js", "android/modules/**/src/js/**/*.js" ], "parserOptions": { - "ecmaVersion": 2020 + "ecmaVersion": 2024 }, "globals": { "kroll": "readonly", @@ -65,7 +68,7 @@ { "files": [ "cli/lib/tasks/*.js", "cli/hooks/webpack.js", "cli/lib/webpack/**/*.js" ], "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2020, "sourceType": "module" } } diff --git a/android/cli/commands/_build.js b/android/cli/commands/_build.js index ee4a6d17c9b..e688237b6a9 100644 --- a/android/cli/commands/_build.js +++ b/android/cli/commands/_build.js @@ -11,1137 +11,1126 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const ADB = require('node-titanium-sdk/lib/adb'), - android = require('node-titanium-sdk/lib/android'), - androidDetect = require('../lib/detect').detect, - AndroidManifest = require('../lib/android-manifest'), - appc = require('node-appc'), - async = require('async'), - Builder = require('node-titanium-sdk/lib/builder'), - GradleWrapper = require('../lib/gradle-wrapper'), - ProcessJsTask = require('../../../cli/lib/tasks/process-js-task'), - ProcessDrawablesTask = require('../lib/process-drawables-task'), - ProcessSplashesTask = require('../lib/process-splashes-task'), - Color = require('../../../common/lib/color'), - ProcessCSSTask = require('../../../cli/lib/tasks/process-css-task'), - CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task'), - DOMParser = require('xmldom').DOMParser, - ejs = require('ejs'), - EmulatorManager = require('node-titanium-sdk/lib/emulator'), - fields = require('fields'), - fs = require('fs-extra'), - i18n = require('node-titanium-sdk/lib/i18n'), - path = require('path'), - temp = require('temp'), - ti = require('node-titanium-sdk'), - tiappxml = require('node-titanium-sdk/lib/tiappxml'), - util = require('util'), - - afs = appc.fs, - i18nLib = appc.i18n(__dirname), - __ = i18nLib.__, - __n = i18nLib.__n, - version = appc.version, - V8_STRING_VERSION_REGEXP = /(\d+)\.(\d+)\.\d+\.\d+/; - +import ADB from 'node-titanium-sdk/lib/adb.js'; +import android from 'node-titanium-sdk/lib/android.js'; +import { detect as androidDetect } from '../lib/detect.js'; +import { AndroidManifest } from '../lib/android-manifest.js'; +import appc from 'node-appc'; +import async from 'async'; +import Builder from 'node-titanium-sdk/lib/builder.js'; +import { GradleWrapper } from '../lib/gradle-wrapper.js'; +import { ProcessJsTask } from '../../../cli/lib/tasks/process-js-task.js'; +import { ProcessDrawablesTask } from '../lib/process-drawables-task.js'; +import { ProcessSplashesTask } from '../lib/process-splashes-task.js'; +import Color from '../../../common/lib/color.js'; +import { ProcessCSSTask } from '../../../cli/lib/tasks/process-css-task.js'; +import { CopyResourcesTask } from '../../../cli/lib/tasks/copy-resources-task.js'; +import { DOMParser } from 'xmldom'; +import ejs from 'ejs'; +import EmulatorManager from 'node-titanium-sdk/lib/emulator.js'; +import fields from 'fields'; +import fs from 'fs-extra'; +import i18n from 'node-titanium-sdk/lib/i18n.js'; +import path from 'node:path'; +import temp from 'temp'; +import ti from 'node-titanium-sdk'; +import tiappxml from 'node-titanium-sdk/lib/tiappxml.js'; +import util from 'node:util'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const afs = appc.fs; +const version = appc.version; +const V8_STRING_VERSION_REGEXP = /(\d+)\.(\d+)\.\d+\.\d+/; const platformsRegExp = new RegExp('^(' + ti.allPlatformNames.join('|') + ')$'); // eslint-disable-line security/detect-non-literal-regexp -function AndroidBuilder() { - Builder.apply(this, arguments); - - this.devices = null; // set by findTargetDevices() during 'config' phase - this.devicesToAutoSelectFrom = []; +class AndroidBuilder extends Builder { + constructor(buildModule) { + super(buildModule); - this.keystoreAliases = []; + this.devices = null; // set by findTargetDevices() during 'config' phase + this.devicesToAutoSelectFrom = []; - this.tiSymbols = {}; + this.keystoreAliases = []; - this.validABIs = this.packageJson.architectures; - this.compileSdkVersion = this.packageJson.compileSDKVersion; // this should always be >= maxSupportedApiLevel - this.minSupportedApiLevel = parseInt(this.packageJson.minSDKVersion); - this.minTargetApiLevel = parseInt(version.parseMin(this.packageJson.vendorDependencies['android sdk'])); - this.maxSupportedApiLevel = parseInt(version.parseMax(this.packageJson.vendorDependencies['android sdk'])); + this.tiSymbols = {}; - this.deployTypes = { - emulator: 'development', - device: 'test', - 'dist-playstore': 'production' - }; + this.validABIs = this.packageJson.architectures; + this.compileSdkVersion = this.packageJson.compileSDKVersion; // this should always be >= maxSupportedApiLevel + this.minSupportedApiLevel = parseInt(this.packageJson.minSDKVersion); + this.minTargetApiLevel = parseInt(version.parseMin(this.packageJson.vendorDependencies['android sdk'])); + this.maxSupportedApiLevel = parseInt(version.parseMax(this.packageJson.vendorDependencies['android sdk'])); - this.targets = [ 'emulator', 'device', 'dist-playstore' ]; -} + this.deployTypes = { + emulator: 'development', + device: 'test', + 'dist-playstore': 'production' + }; -util.inherits(AndroidBuilder, Builder); + this.targets = [ 'emulator', 'device', 'dist-playstore' ]; + } -AndroidBuilder.prototype.config = function config(logger, config, cli) { - Builder.prototype.config.apply(this, arguments); + config(logger, config, cli) { + super.config(logger, config, cli); - const _t = this; + const _t = this; - function assertIssue(logger, issues, name) { - for (let i = 0; i < issues.length; i++) { - if ((typeof name === 'string' && issues[i].id === name) || (typeof name === 'object' && name.test(issues[i].id))) { - issues[i].message.split('\n').forEach(function (line) { - logger[issues[i].type === 'error' ? 'error' : 'warn'](line.replace(/(__(.+?)__)/g, '$2'.bold)); - }); - logger.log(); - if (issues[i].type === 'error') { - process.exit(1); + function assertIssue(logger, issues, name) { + for (let i = 0; i < issues.length; i++) { + if ((typeof name === 'string' && issues[i].id === name) || (typeof name === 'object' && name.test(issues[i].id))) { + issues[i].message.split('\n').forEach(function (line) { + logger[issues[i].type === 'error' ? 'error' : 'warn'](line.replace(/(__(.+?)__)/g, '$2'.bold)); + }); + logger.log(); + if (issues[i].type === 'error') { + process.exit(1); + } } } } - } - // we hook into the pre-validate event so that we can stop the build before - // prompting if we know the build is going to fail. - // - // this is also where we can detect android and jdk environments before - // prompting occurs. because detection is expensive we also do it here instead - // of during config() because there's no sense detecting if config() is being - // called because of the help command. - cli.on('cli:pre-validate', function (obj, callback) { - if (cli.argv.platform && cli.argv.platform !== 'android') { - return callback(); - } - - _t.buildOnly = cli.argv['build-only']; - - async.series([ - function (next) { - // detect android environment - androidDetect(config, { packageJson: _t.packageJson }, function (androidInfo) { - _t.androidInfo = androidInfo; - assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_NOT_FOUND'); - assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_PATH_CONTAINS_AMPERSANDS'); - - // if --android-sdk was not specified, then we simply try to set a default android sdk - if (!cli.argv['android-sdk']) { - let androidSdkPath = config.android && config.android.sdkPath; - if (!androidSdkPath && androidInfo.sdk) { - androidSdkPath = androidInfo.sdk.path; + // we hook into the pre-validate event so that we can stop the build before + // prompting if we know the build is going to fail. + // + // this is also where we can detect android and jdk environments before + // prompting occurs. because detection is expensive we also do it here instead + // of during config() because there's no sense detecting if config() is being + // called because of the help command. + cli.on('cli:pre-validate', function (obj, callback) { + if (cli.argv.platform && cli.argv.platform !== 'android') { + return callback(); + } + + _t.buildOnly = cli.argv['build-only']; + + async.series([ + function (next) { + // detect android environment + androidDetect(config, { packageJson: _t.packageJson }, function (androidInfo) { + _t.androidInfo = androidInfo; + assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_NOT_FOUND'); + assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_PATH_CONTAINS_AMPERSANDS'); + + // if --android-sdk was not specified, then we simply try to set a default android sdk + if (!cli.argv['android-sdk']) { + let androidSdkPath = config.android && config.android.sdkPath; + if (!androidSdkPath && androidInfo.sdk) { + androidSdkPath = androidInfo.sdk.path; + } + androidSdkPath && (cli.argv['android-sdk'] = afs.resolvePath(androidSdkPath)); } - androidSdkPath && (cli.argv['android-sdk'] = afs.resolvePath(androidSdkPath)); - } - next(); - }); - }, - - function (next) { - // detect java development kit - appc.jdk.detect(config, null, function (jdkInfo) { - assertIssue(logger, jdkInfo.issues, 'JDK_NOT_INSTALLED'); - assertIssue(logger, jdkInfo.issues, 'JDK_MISSING_PROGRAMS'); - assertIssue(logger, jdkInfo.issues, 'JDK_INVALID_JAVA_HOME'); - - if (!jdkInfo.version) { - logger.error(__('Unable to locate the Java Development Kit') + '\n'); - logger.log(__('You can specify the location by setting the %s environment variable.', 'JAVA_HOME'.cyan) + '\n'); - process.exit(1); - } + next(); + }); + }, - if (!version.satisfies(jdkInfo.version, _t.packageJson.vendorDependencies.java)) { - logger.error(__('JDK version %s detected, but only version %s is supported', jdkInfo.version, _t.packageJson.vendorDependencies.java) + '\n'); - process.exit(1); - } + function (next) { + // detect java development kit + appc.jdk.detect(config, null, function (jdkInfo) { + assertIssue(logger, jdkInfo.issues, 'JDK_NOT_INSTALLED'); + assertIssue(logger, jdkInfo.issues, 'JDK_MISSING_PROGRAMS'); + assertIssue(logger, jdkInfo.issues, 'JDK_INVALID_JAVA_HOME'); - _t.jdkInfo = jdkInfo; - next(); - }); - } - ], callback); - }); + if (!jdkInfo.version) { + logger.error('Unable to locate the Java Development Kit\n'); + logger.log(`You can specify the location by setting the ${'JAVA_HOME'.cyan} environment variable.\n`); + process.exit(1); + } - const targetDeviceCache = {}, - findTargetDevices = function findTargetDevices(target, callback) { - if (targetDeviceCache[target]) { - return callback(null, targetDeviceCache[target]); - } + if (!version.satisfies(jdkInfo.version, _t.packageJson.vendorDependencies.java)) { + logger.error(`JDK version ${ + jdkInfo.version + } detected, but only version ${ + _t.packageJson.vendorDependencies.java + } is supported\n`); + process.exit(1); + } - if (target === 'device') { - new ADB(config).devices(function (err, devices) { - if (err) { - callback(err); - } else { - this.devices = devices.filter(function (d) { - return !d.emulator && d.state === 'device'; - }); - if (this.devices.length > 1) { - // we have more than 1 device, so we should show 'all' - this.devices.push({ - id: 'all', - model: 'All Devices' + _t.jdkInfo = jdkInfo; + next(); + }); + } + ], callback); + }); + + const targetDeviceCache = {}, + findTargetDevices = function findTargetDevices(target, callback) { + if (targetDeviceCache[target]) { + return callback(null, targetDeviceCache[target]); + } + + if (target === 'device') { + new ADB(config).devices(function (err, devices) { + if (err) { + callback(err); + } else { + this.devices = devices.filter(function (d) { + return !d.emulator && d.state === 'device'; }); - } - callback(null, targetDeviceCache[target] = this.devices.map(function (d) { - return { - name: d.model || d.manufacturer, - id: d.id, - version: d.release, - abi: Array.isArray(d.abi) ? d.abi.join(',') : d.abi, - type: 'device' - }; - })); - } - }.bind(this)); - } else if (target === 'emulator') { - new EmulatorManager(config).detect(function (err, emus) { - if (err) { - callback(err); - } else { - this.devices = emus; - callback(null, targetDeviceCache[target] = emus.map(function (emu) { - // normalize the emulator info - if (emu.type === 'avd') { - return { - name: emu.name, - id: emu.id, - api: emu['api-level'], - version: emu['sdk-version'], - abi: emu.abi, - type: emu.type, - googleApis: emu.googleApis, - sdcard: emu.sdcard - }; - } else if (emu.type === 'genymotion') { + if (this.devices.length > 1) { + // we have more than 1 device, so we should show 'all' + this.devices.push({ + id: 'all', + model: 'All Devices' + }); + } + callback(null, targetDeviceCache[target] = this.devices.map(function (d) { return { - name: emu.name, - id: emu.name, - api: emu['api-level'], - version: emu['sdk-version'], - abi: emu.abi, - type: emu.type, - googleApis: emu.googleApis, - sdcard: true + name: d.model || d.manufacturer, + id: d.id, + version: d.release, + abi: Array.isArray(d.abi) ? d.abi.join(',') : d.abi, + type: 'device' }; - } - return emu; // not good - })); - } - }.bind(this)); - } else { - callback(); - } - }.bind(this); - - return function (finished) { - cli.createHook('build.android.config', this, function (callback) { - const conf = { - flags: { - launch: { - desc: __('disable launching the app after installing'), - default: true, - hideDefault: true, - negate: true - } - }, - options: { - alias: { - abbr: 'L', - desc: __('the alias for the keystore'), - hint: 'alias', - order: 155, - prompt: function (callback) { - callback(fields.select({ - title: __('What is the name of the keystore\'s certificate alias?'), - promptLabel: __('Select a certificate alias by number or name'), - margin: '', - optionLabel: 'name', - optionValue: 'name', - numbered: true, - relistOnError: true, - complete: true, - suggest: false, - options: _t.keystoreAliases, - validate: conf.options.alias.validate })); - }, - validate: function (value, callback) { - // if there's a value, then they entered something, otherwise let the cli prompt - if (value) { - const selectedAlias = value.toLowerCase(), - alias = _t.keystoreAlias = _t.keystoreAliases.filter(function (a) { return a.name && a.name.toLowerCase() === selectedAlias; }).shift(); - if (!alias) { - return callback(new Error(__('Invalid "--alias" value "%s"', value))); + } + }.bind(this)); + } else if (target === 'emulator') { + new EmulatorManager(config).detect(function (err, emus) { + if (err) { + callback(err); + } else { + this.devices = emus; + callback(null, targetDeviceCache[target] = emus.map(function (emu) { + // normalize the emulator info + if (emu.type === 'avd') { + return { + name: emu.name, + id: emu.id, + api: emu['api-level'], + version: emu['sdk-version'], + abi: emu.abi, + type: emu.type, + googleApis: emu.googleApis, + sdcard: emu.sdcard + }; } - } - callback(null, value); + return emu; // not good + })); + } + }.bind(this)); + } else { + callback(); + } + }.bind(this); + + return function (finished) { + cli.createHook('build.android.config', this, function (callback) { + const conf = { + flags: { + launch: { + desc: 'disable launching the app after installing', + default: true, + hideDefault: true, + negate: true } }, - 'android-sdk': { - abbr: 'A', - default: config.android && config.android.sdkPath && afs.resolvePath(config.android.sdkPath), - desc: __('the path to the Android SDK'), - hint: __('path'), - order: 100, - prompt: function (callback) { - let androidSdkPath = config.android && config.android.sdkPath; - if (!androidSdkPath && _t.androidInfo.sdk) { - androidSdkPath = _t.androidInfo.sdk.path; - } - if (androidSdkPath) { - androidSdkPath = afs.resolvePath(androidSdkPath); - if (process.platform === 'win32' || androidSdkPath.indexOf('&') !== -1) { - androidSdkPath = undefined; + options: { + alias: { + abbr: 'L', + desc: 'the alias for the keystore', + hint: 'alias', + order: 155, + prompt: function (callback) { + callback(fields.select({ + title: 'What is the name of the keystore\'s certificate alias?', + promptLabel: 'Select a certificate alias by number or name', + margin: '', + optionLabel: 'name', + optionValue: 'name', + numbered: true, + relistOnError: true, + complete: true, + suggest: false, + options: _t.keystoreAliases, + validate: conf.options.alias.validate + })); + }, + validate: function (value, callback) { + // if there's a value, then they entered something, otherwise let the cli prompt + if (value) { + const selectedAlias = value.toLowerCase(), + alias = _t.keystoreAlias = _t.keystoreAliases.filter(function (a) { return a.name && a.name.toLowerCase() === selectedAlias; }).shift(); + if (!alias) { + return callback(new Error(`Invalid "--alias" value "${value}"`)); + } } + callback(null, value); } - - callback(fields.file({ - promptLabel: __('Where is the Android SDK?'), - default: androidSdkPath, - complete: true, - showHidden: true, - ignoreDirs: _t.ignoreDirs, - ignoreFiles: _t.ignoreFiles, - validate: _t.conf.options['android-sdk'].validate.bind(_t) - })); }, - required: true, - validate: function (value, callback) { - if (!value) { - callback(new Error(__('Invalid Android SDK path'))); - } else if (process.platform === 'win32' && value.indexOf('&') !== -1) { - callback(new Error(__('The Android SDK path cannot contain ampersands (&) on Windows'))); - } else if (_t.androidInfo.sdk && _t.androidInfo.sdk.path === afs.resolvePath(value)) { - callback(null, value); - } else { - // attempt to find android sdk - android.findSDK(value, config, appc.pkginfo.package(module), function () { - - // NOTE: ignore errors when finding sdk, let gradle validate the sdk + 'android-sdk': { + abbr: 'A', + default: config.android && config.android.sdkPath && afs.resolvePath(config.android.sdkPath), + desc: 'the path to the Android SDK', + hint: 'path', + order: 100, + prompt: function (callback) { + let androidSdkPath = config.android && config.android.sdkPath; + if (!androidSdkPath && _t.androidInfo.sdk) { + androidSdkPath = _t.androidInfo.sdk.path; + } + if (androidSdkPath) { + androidSdkPath = afs.resolvePath(androidSdkPath); + if (process.platform === 'win32' || androidSdkPath.indexOf('&') !== -1) { + androidSdkPath = undefined; + } + } - function next() { - // set the android sdk in the config just in case a plugin or something needs it - config.set('android.sdkPath', value); + callback(fields.file({ + promptLabel: 'Where is the Android SDK?', + default: androidSdkPath, + complete: true, + showHidden: true, + ignoreDirs: _t.ignoreDirs, + ignoreFiles: _t.ignoreFiles, + validate: _t.conf.options['android-sdk'].validate.bind(_t) + })); + }, + required: true, + validate: function (value, callback) { + if (!value) { + callback(new Error('Invalid Android SDK path')); + } else if (process.platform === 'win32' && value.indexOf('&') !== -1) { + callback(new Error('The Android SDK path cannot contain ampersands (&) on Windows')); + } else if (_t.androidInfo.sdk && _t.androidInfo.sdk.path === afs.resolvePath(value)) { + callback(null, value); + } else { + // attempt to find android sdk + android.findSDK(value, config, appc.pkginfo.package(module), function () { - // path looks good, do a full scan again - androidDetect(config, { packageJson: _t.packageJson, bypassCache: true }, function (androidInfo) { + // NOTE: ignore errors when finding sdk, let gradle validate the sdk - // assume sdk is valid, let gradle validate the sdk - if (!androidInfo.sdk) { - androidInfo.sdk = { path: value }; - } + function next() { + // set the android sdk in the config just in case a plugin or something needs it + config.set('android.sdkPath', value); - _t.androidInfo = androidInfo; - callback(null, value); - }); - } + // path looks good, do a full scan again + androidDetect(config, { packageJson: _t.packageJson, bypassCache: true }, function (androidInfo) { - // new android sdk path looks good - // if we found an android sdk in the pre-validate hook, then we need to kill the other sdk's adb server - if (_t.androidInfo.sdk) { - new ADB(config).stopServer(next); - } else { - next(); - } - }); - } - } - }, - 'avd-abi': { - abbr: 'B', - desc: __('the abi for the Android emulator; deprecated, use --device-id'), - hint: __('abi') - }, - 'avd-id': { - abbr: 'I', - desc: __('the id for the Android emulator; deprecated, use --device-id'), - hint: __('id') - }, - 'avd-skin': { - abbr: 'S', - desc: __('the skin for the Android emulator; deprecated, use --device-id'), - hint: __('skin') - }, - 'build-type': { - hidden: true - }, - 'debug-host': { - hidden: true - }, - 'deploy-type': { - abbr: 'D', - desc: __('the type of deployment; only used when target is %s or %s', 'emulator'.cyan, 'device'.cyan), - hint: __('type'), - order: 110, - values: [ 'test', 'development' ] - }, - 'device-id': { - abbr: 'C', - desc: __('the id of the Android emulator or the device id to install the application to'), - hint: __('name'), - order: 130, - prompt: function (callback) { - findTargetDevices(cli.argv.target, function (err, results) { - var opts = {}, - title, - promptLabel; - - // we need to sort all results into groups for the select field - if (cli.argv.target === 'device' && results.length) { - opts[__('Devices')] = results; - title = __('Which device do you want to install your app on?'); - promptLabel = __('Select a device by number or name'); - } else if (cli.argv.target === 'emulator') { - // for emulators, we sort by type - let emus = results.filter(function (e) { - return e.type === 'avd'; - }); + // assume sdk is valid, let gradle validate the sdk + if (!androidInfo.sdk) { + androidInfo.sdk = { path: value }; + } - if (emus.length) { - opts[__('Android Emulators')] = emus; - } + _t.androidInfo = androidInfo; + callback(null, value); + }); + } - emus = results.filter(function (e) { - return e.type === 'genymotion'; + // new android sdk path looks good + // if we found an android sdk in the pre-validate hook, then we need to kill the other sdk's adb server + if (_t.androidInfo.sdk) { + new ADB(config).stopServer(next); + } else { + next(); + } }); - if (emus.length) { - opts[__('Genymotion Emulators')] = emus; + } + } + }, + 'avd-abi': { + abbr: 'B', + desc: 'the abi for the Android emulator; deprecated, use --device-id', + hint: 'abi' + }, + 'avd-id': { + abbr: 'I', + desc: 'the id for the Android emulator; deprecated, use --device-id', + hint: 'id' + }, + 'avd-skin': { + abbr: 'S', + desc: 'the skin for the Android emulator; deprecated, use --device-id', + hint: 'skin' + }, + 'build-type': { + hidden: true + }, + 'debug-host': { + hidden: true + }, + 'deploy-type': { + abbr: 'D', + desc: `the type of deployment; only used when target is ${'emulator'.cyan} or ${'device'.cyan}`, + hint: 'type', + order: 110, + values: [ 'test', 'development' ] + }, + 'device-id': { + abbr: 'C', + desc: 'the id of the Android emulator or the device id to install the application to', + hint: 'name', + order: 130, + prompt: function (callback) { + findTargetDevices(cli.argv.target, function (err, results) { + var opts = {}, + title, + promptLabel; + + // we need to sort all results into groups for the select field + if (cli.argv.target === 'device' && results.length) { + opts['Devices'] = results; + title = 'Which device do you want to install your app on?'; + promptLabel = 'Select a device by number or name'; + } else if (cli.argv.target === 'emulator') { + // for emulators, we sort by type + let emus = results.filter(function (e) { + return e.type === 'avd'; + }); - logger.log(__('NOTE: Genymotion emulator must be running to detect Google API support').magenta + '\n'); - } + if (emus.length) { + opts['Android Emulators'] = emus; + } - title = __('Which emulator do you want to launch your app in?'); - promptLabel = __('Select an emulator by number or name'); - } + title = 'Which emulator do you want to launch your app in?'; + promptLabel = 'Select an emulator by number or name'; + } - // if there are no devices/emulators, error - if (!Object.keys(opts).length) { - if (cli.argv.target === 'device') { - logger.warn(__('Unable to find any devices, possibly due to missing dependencies.') + '\n'); - logger.log(__('Continuing with build... (will attempt to install missing dependencies)') + '\n'); - } else { - logger.warn(__('Unable to find any emulators, possibily due to missing dependencies.') + '\n'); - logger.log(__('Continuing with build... (will attempt to install missing dependencies)') + '\n'); + // if there are no devices/emulators, error + if (!Object.keys(opts).length) { + if (cli.argv.target === 'device') { + logger.warn('Unable to find any devices, possibly due to missing dependencies.\n'); + logger.log('Continuing with build... (will attempt to install missing dependencies)\n'); + } else { + logger.warn('Unable to find any emulators, possibly due to missing dependencies.\n'); + logger.log('Continuing with build... (will attempt to install missing dependencies)\n'); + } + _t.buildOnly = true; + return callback(); } - _t.buildOnly = true; - return callback(); - } - callback(fields.select({ - title: title, - promptLabel: promptLabel, - formatters: { - option: function (opt, idx, num) { - return ' ' + num + opt.name.cyan + (opt.version ? ' (' + opt.version + ')' : '') + (opt.googleApis - ? (' (' + __('Google APIs supported') + ')').grey - : opt.googleApis === null - ? (' (' + __('Google APIs support unknown') + ')').grey - : ''); + callback(fields.select({ + title: title, + promptLabel: promptLabel, + formatters: { + option: function (opt, idx, num) { + return ' ' + num + opt.name.cyan + (opt.version ? ' (' + opt.version + ')' : '') + (opt.googleApis + ? (' (Google APIs supported)').grey + : opt.googleApis === null + ? (' (Google APIs support unknown)').grey + : ''); + } + }, + autoSelectOne: true, + margin: '', + optionLabel: 'name', + optionValue: 'id', + numbered: true, + relistOnError: true, + complete: true, + suggest: true, + options: opts + })); + }); + }, + required: true, + validate: function (device, callback) { + const dev = device.toLowerCase(); + findTargetDevices(cli.argv.target, function (err, devices) { + if (cli.argv.target === 'device' && dev === 'all') { + // we let 'all' slide by + return callback(null, dev); + } + for (let i = 0; i < devices.length; i++) { + if (devices[i].id.toLowerCase() === dev) { + return callback(null, devices[i].id); } - }, - autoSelectOne: true, - margin: '', - optionLabel: 'name', - optionValue: 'id', - numbered: true, - relistOnError: true, - complete: true, - suggest: true, - options: opts - })); - }); - }, - required: true, - validate: function (device, callback) { - const dev = device.toLowerCase(); - findTargetDevices(cli.argv.target, function (err, devices) { - if (cli.argv.target === 'device' && dev === 'all') { - // we let 'all' slide by - return callback(null, dev); - } - for (let i = 0; i < devices.length; i++) { - if (devices[i].id.toLowerCase() === dev) { - return callback(null, devices[i].id); } + callback(new Error(`Invalid Android ${cli.argv.target ? 'device' : 'emulator'} "${device}"`)); + }); + }, + verifyIfRequired: function (callback) { + if (_t.buildOnly) { + // not required if we're build only + return callback(); } - callback(new Error(cli.argv.target ? __('Invalid Android device "%s"', device) : __('Invalid Android emulator "%s"', device))); - }); - }, - verifyIfRequired: function (callback) { - if (_t.buildOnly) { - // not required if we're build only - return callback(); - } - findTargetDevices(cli.argv.target, function (err, results) { - if (cli.argv.target === 'emulator' && cli.argv['device-id'] === undefined && cli.argv['avd-id']) { - // if --device-id was not specified, but --avd-id was, then we need to - // try to resolve a device based on the legacy --avd-* options - let avds = results.filter(function (a) { - return a.type === 'avd'; - }).map(function (a) { - return a.name; - }), - name = 'titanium_' + cli.argv['avd-id'] + '_'; - - if (avds.length) { - // try finding the first avd that starts with the avd id - avds = avds.filter(function (avd) { - return avd.indexOf(name) === 0; - }); - if (avds.length === 1) { - cli.argv['device-id'] = avds[0]; - return callback(); - } else if (avds.length > 1) { - // next try using the avd skin - if (!cli.argv['avd-skin']) { - // we have more than one match - logger.error(__n('Found %s avd with id "%%s"', 'Found %s avds with id "%%s"', avds.length, cli.argv['avd-id'])); - logger.error(__('Specify --avd-skin and --avd-abi to select a specific emulator') + '\n'); - } else { - name += cli.argv['avd-skin']; - // try exact match - let tmp = avds.filter(function (avd) { - return avd === name; - }); - if (tmp.length) { - avds = tmp; - } else { - // try partial match - avds = avds.filter(function (avd) { - return avd.indexOf(name + '_') === 0; - }); - } - if (avds.length === 0) { - logger.error(__('No emulators found with id "%s" and skin "%s"', cli.argv['avd-id'], cli.argv['avd-skin']) + '\n'); - } else if (avds.length === 1) { - cli.argv['device-id'] = avds[0]; - return callback(); - } else if (!cli.argv['avd-abi']) { - // we have more than one matching avd, but no abi to filter by so we have to error - logger.error(__n('Found %s avd with id "%%s" and skin "%%s"', 'Found %s avds with id "%%s" and skin "%%s"', avds.length, cli.argv['avd-id'], cli.argv['avd-skin'])); - logger.error(__('Specify --avd-abi to select a specific emulator') + '\n'); + findTargetDevices(cli.argv.target, function (err, results) { + if (cli.argv.target === 'emulator' && cli.argv['device-id'] === undefined && cli.argv['avd-id']) { + // if --device-id was not specified, but --avd-id was, then we need to + // try to resolve a device based on the legacy --avd-* options + let avds = results.filter(function (a) { + return a.type === 'avd'; + }).map(function (a) { + return a.name; + }), + name = 'titanium_' + cli.argv['avd-id'] + '_'; + + if (avds.length) { + // try finding the first avd that starts with the avd id + avds = avds.filter(function (avd) { + return avd.indexOf(name) === 0; + }); + if (avds.length === 1) { + cli.argv['device-id'] = avds[0]; + return callback(); + } else if (avds.length > 1) { + // next try using the avd skin + if (!cli.argv['avd-skin']) { + // we have more than one match + logger.error(`Found ${avds.length} avd${avds.length === 1 ? '' : 's'} with id "${cli.argv['avd-id']}"`); + logger.error('Specify --avd-skin and --avd-abi to select a specific emulator\n'); } else { - name += '_' + cli.argv['avd-abi']; + name += cli.argv['avd-skin']; // try exact match - tmp = avds.filter(function (avd) { + let tmp = avds.filter(function (avd) { return avd === name; }); - /* eslint-disable max-depth */ if (tmp.length) { avds = tmp; } else { + // try partial match avds = avds.filter(function (avd) { return avd.indexOf(name + '_') === 0; }); } if (avds.length === 0) { - logger.error(__('No emulators found with id "%s", skin "%s", and abi "%s"', cli.argv['avd-id'], cli.argv['avd-skin'], cli.argv['avd-abi']) + '\n'); - } else { - // there is one or more avds, but we'll just return the first one + logger.error(`No emulators found with id "${ + cli.argv['avd-id'] + }" and skin "${ + cli.argv['avd-skin'] + }"\n`); + } else if (avds.length === 1) { cli.argv['device-id'] = avds[0]; return callback(); + } else if (!cli.argv['avd-abi']) { + // we have more than one matching avd, but no abi to filter by so we have to error + logger.error(`Found ${ + avds.length + } avd${avds.length === 1 ? '' : 's'} with id "${ + cli.argv['avd-id'] + }" and skin "${ + cli.argv['avd-skin'] + }"`); + logger.error('Specify --avd-abi to select a specific emulator\n'); + } else { + name += '_' + cli.argv['avd-abi']; + // try exact match + tmp = avds.filter(function (avd) { + return avd === name; + }); + /* eslint-disable max-depth */ + if (tmp.length) { + avds = tmp; + } else { + avds = avds.filter(function (avd) { + return avd.indexOf(name + '_') === 0; + }); + } + if (avds.length === 0) { + logger.error(`No emulators found with id "${cli.argv['avd-id']}", skin "${cli.argv['avd-skin']}", and abi "${cli.argv['avd-abi']}"\n`); + } else { + // there is one or more avds, but we'll just return the first one + cli.argv['device-id'] = avds[0]; + return callback(); + } + /* eslint-enable max-depth */ } - /* eslint-enable max-depth */ } } - } - logger.warn(__('%s options have been %s, please use %s', '--avd-*'.cyan, 'deprecated'.red, '--device-id'.cyan) + '\n'); + logger.warn(`${'--avd-*'.cyan} options have been ${'deprecated'.red}, please use ${'--device-id'.cyan}\n`); - // print list of available avds - if (results.length && !cli.argv.prompt) { - logger.log(__('Available Emulators:')); - results.forEach(function (emu) { - logger.log(' ' + emu.name.cyan + ' (' + emu.version + ')'); - }); - logger.log(); + // print list of available avds + if (results.length && !cli.argv.prompt) { + logger.log('Available Emulators:'); + results.forEach(function (emu) { + logger.log(' ' + emu.name.cyan + ' (' + emu.version + ')'); + }); + logger.log(); + } } - } - } else if (cli.argv['device-id'] === undefined && results && results.length && config.get('android.autoSelectDevice', true)) { - // we set the device-id to an array of devices so that later in validate() - // after the tiapp.xml has been parsed, we can auto select the best device - _t.devicesToAutoSelectFrom = results.sort(function (a, b) { - var eq = a.api && b.api && appc.version.eq(a.api, b.api), - gt = a.api && b.api && appc.version.gt(a.api, b.api); - - if (eq) { - if (a.type === b.type) { - if (a.googleApis === b.googleApis) { - return 0; - } else if (b.googleApis) { - return 1; - } else if (a.googleApis === false && b.googleApis === null) { - return 1; + } else if (cli.argv['device-id'] === undefined && results && results.length && config.get('android.autoSelectDevice', true)) { + // we set the device-id to an array of devices so that later in validate() + // after the tiapp.xml has been parsed, we can auto select the best device + _t.devicesToAutoSelectFrom = results.sort(function (a, b) { + var eq = a.api && b.api && appc.version.eq(a.api, b.api), + gt = a.api && b.api && appc.version.gt(a.api, b.api); + + if (eq) { + if (a.type === b.type) { + if (a.googleApis === b.googleApis) { + return 0; + } else if (b.googleApis) { + return 1; + } else if (a.googleApis === false && b.googleApis === null) { + return 1; + } + return -1; } - return -1; + return a.type === 'avd' ? -1 : 1; } - return a.type === 'avd' ? -1 : 1; - } - return gt ? 1 : -1; - }); - return callback(); - } + return gt ? 1 : -1; + }); + return callback(); + } - // Failed to find devices, fallback to buildOnly. - logger.warn('Unable to find any emulators or devices, possibily due to missing dependencies.'); - logger.warn('Continuing with build... (will attempt to install missing dependencies)'); - _t.buildOnly = true; - return callback(); - }); - } - }, - 'key-password': { - desc: __('the password for the keystore private key (defaults to the store-password)'), - hint: 'keypass', - order: 160, - prompt: function (callback) { - callback(fields.text({ - promptLabel: __('What is the keystore\'s __key password__?') + ' ' + __('(leave blank to use the store password)').grey, - password: true, - validate: _t.conf.options['key-password'].validate.bind(_t) - })); + // Failed to find devices, fallback to buildOnly. + logger.warn('Unable to find any emulators or devices, possibily due to missing dependencies.'); + logger.warn('Continuing with build... (will attempt to install missing dependencies)'); + _t.buildOnly = true; + return callback(); + }); + } }, - secret: true, - validate: function (keyPassword, callback) { - // sanity check the keystore and store password - _t.conf.options['store-password'].validate(cli.argv['store-password'], function (err, storePassword) { - if (err) { - // we have a bad --keystore or --store-password arg - cli.argv.keystore = cli.argv['store-password'] = undefined; - return callback(err); - } + 'key-password': { + desc: 'the password for the keystore private key (defaults to the store-password)', + hint: 'keypass', + order: 160, + prompt: function (callback) { + callback(fields.text({ + promptLabel: `What is the keystore's __key password__?' ${'(leave blank to use the store password)'.grey}`, + password: true, + validate: _t.conf.options['key-password'].validate.bind(_t) + })); + }, + secret: true, + validate: function (keyPassword, callback) { + // sanity check the keystore and store password + _t.conf.options['store-password'].validate(cli.argv['store-password'], function (err, storePassword) { + if (err) { + // we have a bad --keystore or --store-password arg + cli.argv.keystore = cli.argv['store-password'] = undefined; + return callback(err); + } - const keystoreFile = cli.argv.keystore, - alias = cli.argv.alias, - tmpKeystoreFile = temp.path({ suffix: '.jks' }); - - if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) { - // the only way to test the key password is to export the cert - appc.subprocess.run(_t.jdkInfo.executables.keytool, [ - '-J-Duser.language=en', - '-importkeystore', - '-v', - '-srckeystore', keystoreFile, - '-destkeystore', tmpKeystoreFile, - '-srcstorepass', storePassword, - '-deststorepass', storePassword, - '-srcalias', alias, - '-destalias', alias, - '-srckeypass', keyPassword || storePassword, - '-noprompt' - ], function (code, out) { - if (code) { - if (out.indexOf('java.security.UnrecoverableKeyException') !== -1) { - return callback(new Error(__('Bad key password'))); + const keystoreFile = cli.argv.keystore, + alias = cli.argv.alias, + tmpKeystoreFile = temp.path({ suffix: '.jks' }); + + if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) { + // the only way to test the key password is to export the cert + appc.subprocess.run(_t.jdkInfo.executables.keytool, [ + '-J-Duser.language=en', + '-importkeystore', + '-v', + '-srckeystore', keystoreFile, + '-destkeystore', tmpKeystoreFile, + '-srcstorepass', storePassword, + '-deststorepass', storePassword, + '-srcalias', alias, + '-destalias', alias, + '-srckeypass', keyPassword || storePassword, + '-noprompt' + ], function (code, out) { + if (code) { + if (out.indexOf('java.security.UnrecoverableKeyException') !== -1) { + return callback(new Error('Bad key password')); + } + return callback(new Error(out.trim())); } - return callback(new Error(out.trim())); - } - // remove the temp keystore - fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile); + // remove the temp keystore + fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile); + callback(null, keyPassword); + }); + } else { callback(null, keyPassword); - }); - } else { - callback(null, keyPassword); - } - }); - } - }, - keystore: { - abbr: 'K', - callback: function () { - _t.conf.options['alias'].required = true; - _t.conf.options['store-password'].required = true; - }, - desc: __('the location of the keystore file'), - hint: 'path', - order: 140, - prompt: function (callback) { - _t.conf.options['key-password'].required = true; - callback(fields.file({ - promptLabel: __('Where is the __keystore file__ used to sign the app?'), - complete: true, - showHidden: true, - ignoreDirs: _t.ignoreDirs, - ignoreFiles: _t.ignoreFiles, - validate: _t.conf.options.keystore.validate.bind(_t) - })); + } + }); + } }, - validate: function (keystoreFile, callback) { - if (!keystoreFile) { - callback(new Error(__('Please specify the path to your keystore file'))); - } else { - keystoreFile = afs.resolvePath(keystoreFile); - if (!fs.existsSync(keystoreFile) || !fs.statSync(keystoreFile).isFile()) { - callback(new Error(__('Invalid keystore file'))); + keystore: { + abbr: 'K', + callback: function () { + _t.conf.options['alias'].required = true; + _t.conf.options['store-password'].required = true; + }, + desc: 'the location of the keystore file', + hint: 'path', + order: 140, + prompt: function (callback) { + _t.conf.options['key-password'].required = true; + callback(fields.file({ + promptLabel: 'Where is the __keystore file__ used to sign the app?', + complete: true, + showHidden: true, + ignoreDirs: _t.ignoreDirs, + ignoreFiles: _t.ignoreFiles, + validate: _t.conf.options.keystore.validate.bind(_t) + })); + }, + validate: function (keystoreFile, callback) { + if (!keystoreFile) { + callback(new Error('Please specify the path to your keystore file')); } else { - callback(null, keystoreFile); + keystoreFile = afs.resolvePath(keystoreFile); + if (!fs.existsSync(keystoreFile) || !fs.statSync(keystoreFile).isFile()) { + callback(new Error('Invalid keystore file')); + } else { + callback(null, keystoreFile); + } } } - } - }, - 'output-dir': { - abbr: 'O', - desc: __('the output directory when using %s', 'dist-playstore'.cyan), - hint: 'dir', - order: 180, - prompt: function (callback) { - callback(fields.file({ - promptLabel: __('Where would you like the output APK file saved?'), - default: cli.argv['project-dir'] && afs.resolvePath(cli.argv['project-dir'], 'dist'), - complete: true, - showHidden: true, - ignoreDirs: _t.ignoreDirs, - ignoreFiles: /.*/, - validate: _t.conf.options['output-dir'].validate.bind(_t) - })); }, - validate: function (outputDir, callback) { - callback(outputDir || !_t.conf.options['output-dir'].required ? null : new Error(__('Invalid output directory')), outputDir); - } - }, - 'profiler-host': { - hidden: true - }, - 'store-password': { - abbr: 'P', - desc: __('the password for the keystore'), - hint: 'password', - order: 150, - prompt: function (callback) { - callback(fields.text({ - next: function (err) { - return err && err.next || null; - }, - promptLabel: __('What is the keystore\'s __password__?'), - password: true, - // if the password fails due to bad keystore file, - // we need to prompt for the keystore file again - repromptOnError: false, - validate: _t.conf.options['store-password'].validate.bind(_t) - })); - }, - secret: true, - validate: function (storePassword, callback) { - if (!storePassword) { - return callback(new Error(__('Please specify a keystore password'))); + 'output-dir': { + abbr: 'O', + desc: `the output directory when using ${'dist-playstore'.cyan}`, + hint: 'dir', + order: 180, + prompt: function (callback) { + callback(fields.file({ + promptLabel: 'Where would you like the output APK file saved?', + default: cli.argv['project-dir'] && afs.resolvePath(cli.argv['project-dir'], 'dist'), + complete: true, + showHidden: true, + ignoreDirs: _t.ignoreDirs, + ignoreFiles: /.*/, + validate: _t.conf.options['output-dir'].validate.bind(_t) + })); + }, + validate: function (outputDir, callback) { + callback(outputDir || !_t.conf.options['output-dir'].required ? null : new Error('Invalid output directory'), outputDir); } - - // sanity check the keystore - _t.conf.options.keystore.validate(cli.argv.keystore, function (err, keystoreFile) { - if (err) { - // we have a bad --keystore arg - cli.argv.keystore = undefined; - return callback(err); + }, + 'profiler-host': { + hidden: true + }, + 'store-password': { + abbr: 'P', + desc: 'the password for the keystore', + hint: 'password', + order: 150, + prompt: function (callback) { + callback(fields.text({ + next: function (err) { + return err && err.next || null; + }, + promptLabel: 'What is the keystore\'s __password__?', + password: true, + // if the password fails due to bad keystore file, + // we need to prompt for the keystore file again + repromptOnError: false, + validate: _t.conf.options['store-password'].validate.bind(_t) + })); + }, + secret: true, + validate: function (storePassword, callback) { + if (!storePassword) { + return callback(new Error('Please specify a keystore password')); } - if (keystoreFile && _t.jdkInfo && _t.jdkInfo.executables.keytool) { - appc.subprocess.run(_t.jdkInfo.executables.keytool, [ - '-J-Duser.language=en', - '-list', - '-v', - '-keystore', keystoreFile, - '-storepass', storePassword - ], function (code, out) { - if (code) { - let msg = out.split('\n').shift().split('java.io.IOException:'); - if (msg.length > 1) { - msg = msg[1].trim(); - if (/invalid keystore format/i.test(msg)) { - msg = __('Invalid keystore file'); - cli.argv.keystore = undefined; - _t.conf.options.keystore.required = true; + // sanity check the keystore + _t.conf.options.keystore.validate(cli.argv.keystore, function (err, keystoreFile) { + if (err) { + // we have a bad --keystore arg + cli.argv.keystore = undefined; + return callback(err); + } + + if (keystoreFile && _t.jdkInfo && _t.jdkInfo.executables.keytool) { + appc.subprocess.run(_t.jdkInfo.executables.keytool, [ + '-J-Duser.language=en', + '-list', + '-v', + '-keystore', keystoreFile, + '-storepass', storePassword + ], function (code, out) { + if (code) { + let msg = out.split('\n').shift().split('java.io.IOException:'); + if (msg.length > 1) { + msg = msg[1].trim(); + if (/invalid keystore format/i.test(msg)) { + msg = 'Invalid keystore file'; + cli.argv.keystore = undefined; + _t.conf.options.keystore.required = true; + } + } else { + msg = out.trim(); } - } else { - msg = out.trim(); + + return callback(new Error(msg)); } - return callback(new Error(msg)); - } + // empty the alias array. it is important that we don't destroy the original + // instance since it was passed by reference to the alias select list + while (_t.keystoreAliases.length) { + _t.keystoreAliases.pop(); + } - // empty the alias array. it is important that we don't destroy the original - // instance since it was passed by reference to the alias select list - while (_t.keystoreAliases.length) { - _t.keystoreAliases.pop(); - } + // Parse the keystore's alias name and signature algorithm. + // Note: Algorithm can return "MD5withRSA (weak)" on JDK 8 and higher. + // Only extract 1st token since we need a valid algorithm name. + const aliasRegExp = /Alias name: (.+)/, + sigalgRegExp = /Signature algorithm name: (.[^\s]+)/; + out.split('\n\n').forEach(function (chunk) { + chunk = chunk.trim(); + const m = chunk.match(aliasRegExp); + if (m) { + const sigalg = chunk.match(sigalgRegExp); + _t.keystoreAliases.push({ + name: m[1], + sigalg: sigalg && sigalg[1] && sigalg[1].trim() + }); + } + }); - // Parse the keystore's alias name and signature algorithm. - // Note: Algorithm can return "MD5withRSA (weak)" on JDK 8 and higher. - // Only extract 1st token since we need a valid algorithm name. - const aliasRegExp = /Alias name: (.+)/, - sigalgRegExp = /Signature algorithm name: (.[^\s]+)/; - out.split('\n\n').forEach(function (chunk) { - chunk = chunk.trim(); - const m = chunk.match(aliasRegExp); - if (m) { - const sigalg = chunk.match(sigalgRegExp); - _t.keystoreAliases.push({ - name: m[1], - sigalg: sigalg && sigalg[1] && sigalg[1].trim() - }); + if (_t.keystoreAliases.length === 0) { + cli.argv.keystore = undefined; + return callback(new Error('Keystore does not contain any certificates')); + } else if (!cli.argv.alias && _t.keystoreAliases.length === 1) { + cli.argv.alias = _t.keystoreAliases[0].name; } - }); - - if (_t.keystoreAliases.length === 0) { - cli.argv.keystore = undefined; - return callback(new Error(__('Keystore does not contain any certificates'))); - } else if (!cli.argv.alias && _t.keystoreAliases.length === 1) { - cli.argv.alias = _t.keystoreAliases[0].name; - } - // check if this keystore requires a key password - const keystoreFile = cli.argv.keystore, - alias = cli.argv.alias, - tmpKeystoreFile = temp.path({ suffix: '.jks' }); - - if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) { - // the only way to test the key password is to export the cert - appc.subprocess.run(_t.jdkInfo.executables.keytool, [ - '-J-Duser.language=en', - '-importkeystore', - '-v', - '-srckeystore', keystoreFile, - '-destkeystore', tmpKeystoreFile, - '-srcstorepass', storePassword, - '-deststorepass', storePassword, - '-srcalias', alias, - '-destalias', alias, - '-srckeypass', storePassword, - '-noprompt' - ], function (code, out) { - if (code) { - if (out.indexOf('Alias <' + alias + '> does not exist') !== -1) { - // bad alias, we'll let --alias find it again - _t.conf.options['alias'].required = true; + // check if this keystore requires a key password + const keystoreFile = cli.argv.keystore, + alias = cli.argv.alias, + tmpKeystoreFile = temp.path({ suffix: '.jks' }); + + if (keystoreFile && storePassword && alias && _t.jdkInfo && _t.jdkInfo.executables.keytool) { + // the only way to test the key password is to export the cert + appc.subprocess.run(_t.jdkInfo.executables.keytool, [ + '-J-Duser.language=en', + '-importkeystore', + '-v', + '-srckeystore', keystoreFile, + '-destkeystore', tmpKeystoreFile, + '-srcstorepass', storePassword, + '-deststorepass', storePassword, + '-srcalias', alias, + '-destalias', alias, + '-srckeypass', storePassword, + '-noprompt' + ], function (code, out) { + if (code) { + if (out.indexOf('Alias <' + alias + '> does not exist') !== -1) { + // bad alias, we'll let --alias find it again + _t.conf.options['alias'].required = true; + } + + // since we have an error, force the key password to be required + _t.conf.options['key-password'].required = true; + } else { + // remove the temp keystore + fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile); } - - // since we have an error, force the key password to be required - _t.conf.options['key-password'].required = true; - } else { - // remove the temp keystore - fs.existsSync(tmpKeystoreFile) && fs.unlinkSync(tmpKeystoreFile); - } + callback(null, storePassword); + }); + } else { callback(null, storePassword); - }); - } else { - callback(null, storePassword); - } - }); - } else { - callback(null, storePassword); - } - }); - } - }, - target: { - abbr: 'T', - callback: function (value) { - // as soon as we know the target, toggle required options for validation - if (value === 'dist-playstore') { - _t.conf.options['alias'].required = true; - _t.conf.options['deploy-type'].values = [ 'production' ]; - _t.conf.options['device-id'].required = false; - _t.conf.options['keystore'].required = true; - _t.conf.options['output-dir'].required = true; - _t.conf.options['store-password'].required = true; + } + }); + } else { + callback(null, storePassword); + } + }); } }, - default: 'emulator', - desc: __('the target to build for'), - order: 120, - required: true, - values: _t.targets - }, - sigalg: { - desc: __('the type of a digital signature algorithm. only used when overriding keystore signing algorithm'), - hint: __('signing'), - order: 170, - values: [ 'MD5withRSA', 'SHA1withRSA', 'SHA256withRSA' ] + target: { + abbr: 'T', + callback: function (value) { + // as soon as we know the target, toggle required options for validation + if (value === 'dist-playstore') { + _t.conf.options['alias'].required = true; + _t.conf.options['deploy-type'].values = [ 'production' ]; + _t.conf.options['device-id'].required = false; + _t.conf.options['keystore'].required = true; + _t.conf.options['output-dir'].required = true; + _t.conf.options['store-password'].required = true; + } + }, + default: 'emulator', + desc: 'the target to build for', + order: 120, + required: true, + values: _t.targets + }, + sigalg: { + desc: 'the type of a digital signature algorithm. only used when overriding keystore signing algorithm', + hint: 'signing', + order: 170, + values: [ 'MD5withRSA', 'SHA1withRSA', 'SHA256withRSA' ] + } } - } - }; - - callback(null, _t.conf = conf); - })(function (err, result) { - finished(result); - }); - }.bind(this); -}; + }; -AndroidBuilder.prototype.validate = function validate(logger, config, cli) { - Builder.prototype.validate.apply(this, arguments); + callback(null, _t.conf = conf); + })(function (err, result) { + finished(result); + }); + }.bind(this); + } - this.target = cli.argv.target; - this.deployType = !/^dist-/.test(this.target) && cli.argv['deploy-type'] ? cli.argv['deploy-type'] : this.deployTypes[this.target]; - this.buildType = cli.argv['build-type'] || ''; + validate(logger, config, cli) { + super.validate(logger, config, cli); - // ti.deploytype is deprecated and so we force the real deploy type - if (cli.tiapp.properties['ti.deploytype']) { - logger.warn(__('The %s tiapp.xml property has been deprecated, please use the %s option', 'ti.deploytype'.cyan, '--deploy-type'.cyan)); - } - cli.tiapp.properties['ti.deploytype'] = { type: 'string', value: this.deployType }; + this.target = cli.argv.target; + this.deployType = !/^dist-/.test(this.target) && cli.argv['deploy-type'] ? cli.argv['deploy-type'] : this.deployTypes[this.target]; + this.buildType = cli.argv['build-type'] || ''; - // Fetch Java max heap size setting. - this.javacMaxMemory = config.get('android.javac.maxMemory', '3072M'); + // ti.deploytype is deprecated and so we force the real deploy type + if (cli.tiapp.properties['ti.deploytype']) { + logger.warn(`The ${ + 'ti.deploytype'.cyan + } tiapp.xml property has been deprecated, please use the ${ + '--deploy-type'.cyan + } option`); + } + cli.tiapp.properties['ti.deploytype'] = { type: 'string', value: this.deployType }; - // TODO remove in the next SDK - if (cli.tiapp.properties['android.javac.maxmemory'] && cli.tiapp.properties['android.javac.maxmemory'].value) { - logger.error(__('android.javac.maxmemory is deprecated and will be removed in the next version. Please use android.javac.maxMemory') + '\n'); - this.javacMaxMemory = cli.tiapp.properties['android.javac.maxmemory'].value; - } + // Fetch Java max heap size setting. + this.javacMaxMemory = config.get('android.javac.maxMemory', '3072M'); - if (cli.tiapp.properties['android.javac.maxMemory'] && cli.tiapp.properties['android.javac.maxMemory'].value) { - this.javacMaxMemory = cli.tiapp.properties['android.javac.maxMemory'].value; - } + // TODO remove in the next SDK + if (cli.tiapp.properties['android.javac.maxmemory'] && cli.tiapp.properties['android.javac.maxmemory'].value) { + logger.error('android.javac.maxmemory is deprecated and will be removed in the next version. Please use android.javac.maxMemory\n'); + this.javacMaxMemory = cli.tiapp.properties['android.javac.maxmemory'].value; + } - // Transpilation details - this.transpile = cli.tiapp['transpile'] !== false; // Transpiling is an opt-out process now - // If they're passing flag to do source-mapping, that overrides everything, so turn it on - if (cli.argv['source-maps']) { - this.sourceMaps = true; - // if they haven't, respect the tiapp.xml value if set one way or the other - } else if (Object.prototype.hasOwnProperty.call(cli.tiapp, 'source-maps')) { // they've explicitly set a value in tiapp.xml - this.sourceMaps = cli.tiapp['source-maps'] === true; // respect the tiapp.xml value - } else { // otherwise turn on by default for non-production builds - this.sourceMaps = this.deployType !== 'production'; - } + if (cli.tiapp.properties['android.javac.maxMemory'] && cli.tiapp.properties['android.javac.maxMemory'].value) { + this.javacMaxMemory = cli.tiapp.properties['android.javac.maxMemory'].value; + } - // We get a string here like 6.2.414.36, we need to convert it to 62 (integer) - const v8Version = this.packageJson.v8.version; - const found = v8Version.match(V8_STRING_VERSION_REGEXP); - this.chromeVersion = parseInt(found[1] + found[2]); // concat the first two numbers as string, then turn to int - - // manually inject the build profile settings into the tiapp.xml - switch (this.deployType) { - case 'production': - this.minifyJS = true; - this.encryptJS = true; - this.minifyCSS = true; - this.allowDebugging = false; - this.allowProfiling = false; - this.proguard = false; - break; - - case 'test': - this.minifyJS = true; - this.encryptJS = true; - this.minifyCSS = true; - this.allowDebugging = true; - this.allowProfiling = true; - this.proguard = false; - break; - - case 'development': - default: - this.minifyJS = false; - this.encryptJS = false; - this.minifyCSS = false; - this.allowDebugging = true; - this.allowProfiling = true; - this.proguard = false; - } + // Transpilation details + this.transpile = cli.tiapp['transpile'] !== false; // Transpiling is an opt-out process now + // If they're passing flag to do source-mapping, that overrides everything, so turn it on + if (cli.argv['source-maps']) { + this.sourceMaps = true; + // if they haven't, respect the tiapp.xml value if set one way or the other + } else if (Object.prototype.hasOwnProperty.call(cli.tiapp, 'source-maps')) { // they've explicitly set a value in tiapp.xml + this.sourceMaps = cli.tiapp['source-maps'] === true; // respect the tiapp.xml value + } else { // otherwise turn on by default for non-production builds + this.sourceMaps = this.deployType !== 'production'; + } - if (cli.tiapp.properties['ti.android.compilejs']) { - logger.warn(__('The %s tiapp.xml property has been deprecated, please use the %s option to bypass JavaScript minification', 'ti.android.compilejs'.cyan, '--skip-js-minify'.cyan)); - } + // We get a string here like 6.2.414.36, we need to convert it to 62 (integer) + const v8Version = this.packageJson.v8.version; + const found = v8Version.match(V8_STRING_VERSION_REGEXP); + this.chromeVersion = parseInt(found[1] + found[2]); // concat the first two numbers as string, then turn to int - if (cli.argv['skip-js-minify']) { - this.minifyJS = false; - } + // manually inject the build profile settings into the tiapp.xml + switch (this.deployType) { + case 'production': + this.minifyJS = true; + this.encryptJS = true; + this.minifyCSS = true; + this.allowDebugging = false; + this.allowProfiling = false; + this.proguard = false; + break; - // Do we write out process.env into a file in the app to use? - this.writeEnvVars = this.deployType !== 'production'; + case 'test': + this.minifyJS = true; + this.encryptJS = true; + this.minifyCSS = true; + this.allowDebugging = true; + this.allowProfiling = true; + this.proguard = false; + break; - // check the app name - if (cli.tiapp.name.indexOf('&') !== -1) { - if (config.get('android.allowAppNameAmpersands', false)) { - logger.warn(__('The app name "%s" contains an ampersand (&) which will most likely cause problems.', cli.tiapp.name)); - logger.warn(__('It is recommended that you define the app name using i18n strings.')); - logger.warn(__('Refer to %s for more information.', 'https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/Cross-Platform_Mobile_Development_In_Titanium/Internationalization.html'.cyan)); - } else { - logger.error(__('The app name "%s" contains an ampersand (&) which will most likely cause problems.', cli.tiapp.name)); - logger.error(__('It is recommended that you define the app name using i18n strings.')); - logger.error(__('Refer to %s for more information.', 'https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/Cross-Platform_Mobile_Development_In_Titanium/Internationalization.html')); - logger.error(__('To allow ampersands in the app name, run:')); - logger.error(' %sti config android.allowAppNameAmpersands true\n', process.env.APPC_ENV ? 'appc ' : ''); - process.exit(1); + case 'development': + default: + this.minifyJS = false; + this.encryptJS = false; + this.minifyCSS = false; + this.allowDebugging = true; + this.allowProfiling = true; + this.proguard = false; } - } - // check the Android specific app id rules - if (!config.get('app.skipAppIdValidation') && !cli.tiapp.properties['ti.skipAppIdValidation']) { - if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_-]*(\.[a-zA-Z0-9_-]*)*)$/.test(cli.tiapp.id)) { - logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id)); - logger.error(__('The app id must consist only of letters, numbers, dashes, and underscores.')); - logger.error(__('Note: Android does not allow dashes.')); - logger.error(__('The first character must be a letter or underscore.')); - logger.error(__('Usually the app id is your company\'s reversed Internet domain name. (i.e. com.example.myapp)') + '\n'); - process.exit(1); + if (cli.tiapp.properties['ti.android.compilejs']) { + logger.warn(`The ${ + 'ti.android.compilejs'.cyan + } tiapp.xml property has been deprecated, please use the ${ + '--skip-js-minify'.cyan + } option to bypass JavaScript minification`); } - if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_]*(\.[a-zA-Z_]{1}[a-zA-Z0-9_]*)*)$/.test(cli.tiapp.id)) { - logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id)); - logger.error(__('The app id must consist of letters, numbers, and underscores.')); - logger.error(__('The first character must be a letter or underscore.')); - logger.error(__('The first character after a period must not be a number.')); - logger.error(__('Usually the app id is your company\'s reversed Internet domain name. (i.e. com.example.myapp)') + '\n'); - process.exit(1); + if (cli.argv['skip-js-minify']) { + this.minifyJS = false; } - if (!ti.validAppId(cli.tiapp.id)) { - logger.error(__('Invalid app id "%s"', cli.tiapp.id)); - logger.error(__('The app id must not contain Java reserved words.') + '\n'); - process.exit(1); + // Do we write out process.env into a file in the app to use? + this.writeEnvVars = this.deployType !== 'production'; + + // check the app name + if (cli.tiapp.name.indexOf('&') !== -1) { + if (config.get('android.allowAppNameAmpersands', false)) { + logger.warn(`The app name "${cli.tiapp.name}" contains an ampersand (&) which will most likely cause problems.`); + logger.warn('It is recommended that you define the app name using i18n strings.'); + logger.warn(`Refer to ${ + 'https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/Cross-Platform_Mobile_Development_In_Titanium/Internationalization.html'.cyan + } for more information.`); + } else { + logger.error(`The app name "${ + cli.tiapp.name + }" contains an ampersand (&) which will most likely cause problems.`); + logger.error('It is recommended that you define the app name using i18n strings.'); + logger.error(`Refer to ${ + 'https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/Cross-Platform_Mobile_Development_In_Titanium/Internationalization.html' + } for more information.`); + logger.error('To allow ampersands in the app name, run:'); + logger.error(' ti config android.allowAppNameAmpersands true\n'); + process.exit(1); + } } - } - // check the default unit - cli.tiapp.properties || (cli.tiapp.properties = {}); - cli.tiapp.properties['ti.ui.defaultunit'] || (cli.tiapp.properties['ti.ui.defaultunit'] = { type: 'string', value: 'system' }); - if (!/^system|px|dp|dip|mm|cm|in$/.test(cli.tiapp.properties['ti.ui.defaultunit'].value)) { - logger.error(__('Invalid "ti.ui.defaultunit" property value "%s"', cli.tiapp.properties['ti.ui.defaultunit'].value) + '\n'); - logger.log(__('Valid units:')); - 'system,px,dp,dip,mm,cm,in'.split(',').forEach(function (unit) { - logger.log(' ' + unit.cyan); - }); - logger.log(); - process.exit(1); - } + // check the Android specific app id rules + if (!config.get('app.skipAppIdValidation') && !cli.tiapp.properties['ti.skipAppIdValidation']) { + if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_-]*(\.[a-zA-Z0-9_-]*)*)$/.test(cli.tiapp.id)) { + logger.error(`tiapp.xml contains an invalid app id "${cli.tiapp.id}"`); + logger.error('The app id must consist only of letters, numbers, dashes, and underscores.'); + logger.error('Note: Android does not allow dashes.'); + logger.error('The first character must be a letter or underscore.'); + logger.error('Usually the app id is your company\'s reversed Internet domain name. (i.e. com.example.myapp)\n'); + process.exit(1); + } - // if we're building for the emulator, make sure we don't have any issues - if (cli.argv.target === 'emulator') { - this.androidInfo.issues.forEach(function (issue) { - if (/^ANDROID_MISSING_(LIBGL|I386_ARCH|IA32_LIBS|32BIT_GLIBC|32BIT_LIBSTDCPP)$/.test(issue.id)) { - issue.message.split('\n').forEach(function (line) { - logger.warn(line); - }); + if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_]*(\.[a-zA-Z_]{1}[a-zA-Z0-9_]*)*)$/.test(cli.tiapp.id)) { + logger.error(`tiapp.xml contains an invalid app id "${cli.tiapp.id}"`); + logger.error('The app id must consist of letters, numbers, and underscores.'); + logger.error('The first character must be a letter or underscore.'); + logger.error('The first character after a period must not be a number.'); + logger.error('Usually the app id is your company\'s reversed Internet domain name. (i.e. com.example.myapp)\n'); + process.exit(1); } - }); - } - // check that the proguard config exists - const proguardConfigFile = path.join(cli.argv['project-dir'], 'platform', 'android', 'proguard.cfg'); - if (this.proguard && !fs.existsSync(proguardConfigFile)) { - logger.error(__('Missing ProGuard configuration file')); - logger.error(__('ProGuard settings must go in the file "%s"', proguardConfigFile)); - logger.error(__('For example configurations, visit %s', 'http://proguard.sourceforge.net/index.html#manual/examples.html') + '\n'); - process.exit(1); - } + if (!ti.validAppId(cli.tiapp.id)) { + logger.error(`Invalid app id "${cli.tiapp.id}"`); + logger.error('The app id must not contain Java reserved words.\n'); + process.exit(1); + } + } - // map sdk versions to sdk targets instead of by id - const targetSDKMap = { + // check the default unit + cli.tiapp.properties || (cli.tiapp.properties = {}); + cli.tiapp.properties['ti.ui.defaultunit'] || (cli.tiapp.properties['ti.ui.defaultunit'] = { type: 'string', value: 'system' }); + if (!/^system|px|dp|dip|mm|cm|in$/.test(cli.tiapp.properties['ti.ui.defaultunit'].value)) { + logger.error(`Invalid "ti.ui.defaultunit" property value "${cli.tiapp.properties['ti.ui.defaultunit'].value}"\n`); + logger.log('Valid units:'); + 'system,px,dp,dip,mm,cm,in'.split(',').forEach(function (unit) { + logger.log(' ' + unit.cyan); + }); + logger.log(); + process.exit(1); + } - // placeholder for gradle to use - [this.compileSdkVersion]: { - sdk: this.compileSdkVersion + // if we're building for the emulator, make sure we don't have any issues + if (cli.argv.target === 'emulator') { + this.androidInfo.issues.forEach(function (issue) { + if (/^ANDROID_MISSING_(LIBGL|I386_ARCH|IA32_LIBS|32BIT_GLIBC|32BIT_LIBSTDCPP)$/.test(issue.id)) { + issue.message.split('\n').forEach(function (line) { + logger.warn(line); + }); + } + }); } - }; - Object.keys(this.androidInfo.targets).forEach(function (i) { - var t = this.androidInfo.targets[i]; - if (t.type === 'platform') { - targetSDKMap[t.id.replace('android-', '')] = t; + + // check that the proguard config exists + const proguardConfigFile = path.join(cli.argv['project-dir'], 'platform', 'android', 'proguard.cfg'); + if (this.proguard && !fs.existsSync(proguardConfigFile)) { + logger.error('Missing ProGuard configuration file'); + logger.error(`ProGuard settings must go in the file "${proguardConfigFile}"`); + logger.error('For example configurations, visit http://proguard.sourceforge.net/index.html#manual/examples.html\n'); + process.exit(1); } - }, this); - // check the Android SDK we require to build exists - this.androidCompileSDK = targetSDKMap[this.compileSdkVersion]; + // map sdk versions to sdk targets instead of by id + const targetSDKMap = { + // placeholder for gradle to use + [this.compileSdkVersion]: { + sdk: this.compileSdkVersion + } + }; + Object.keys(this.androidInfo.targets).forEach(function (i) { + var t = this.androidInfo.targets[i]; + if (t.type === 'platform') { + targetSDKMap[t.id.replace('android-', '')] = t; + } + }, this); + + // check the Android SDK we require to build exists + this.androidCompileSDK = targetSDKMap[this.compileSdkVersion]; - // If "tiapp.xml" contains "AndroidManifest.xml" info, then load/store it to "this.customAndroidManifest" field. - try { - if (cli.tiapp.android && cli.tiapp.android.manifest) { - this.customAndroidManifest = AndroidManifest.fromXmlString(cli.tiapp.android.manifest); + // If "tiapp.xml" contains "AndroidManifest.xml" info, then load/store it to "this.customAndroidManifest" field. + try { + if (cli.tiapp.android && cli.tiapp.android.manifest) { + this.customAndroidManifest = AndroidManifest.fromXmlString(cli.tiapp.android.manifest); + } + } catch (ex) { + logger.error('Malformed definition in the section of the tiapp.xml'); + process.exit(1); } - } catch (ex) { - logger.error(__n('Malformed definition in the section of the tiapp.xml')); - process.exit(1); - } - // If project has "./platform/android/AndroidManifest.xml" file, then load/store it to "this.customAndroidManifest" field. - const externalAndroidManifestFilePath = path.join(cli.argv['project-dir'], 'platform', 'android', 'AndroidManifest.xml'); - try { - if (fs.existsSync(externalAndroidManifestFilePath)) { - const externalAndroidManifest = AndroidManifest.fromFilePathSync(externalAndroidManifestFilePath); - if (externalAndroidManifest) { - if (this.customAndroidManifest) { - // External manifest file's settings will overwrite "tiapp.xml" manifest settings. - this.customAndroidManifest.copyFromAndroidManifest(externalAndroidManifest); - } else { - // The "tiapp.xml" did not contain any manifest settings. So, keep external manifest settings as-is. - this.customAndroidManifest = externalAndroidManifest; + // If project has "./platform/android/AndroidManifest.xml" file, then load/store it to "this.customAndroidManifest" field. + const externalAndroidManifestFilePath = path.join(cli.argv['project-dir'], 'platform', 'android', 'AndroidManifest.xml'); + try { + if (fs.existsSync(externalAndroidManifestFilePath)) { + const externalAndroidManifest = AndroidManifest.fromFilePathSync(externalAndroidManifestFilePath); + if (externalAndroidManifest) { + if (this.customAndroidManifest) { + // External manifest file's settings will overwrite "tiapp.xml" manifest settings. + this.customAndroidManifest.copyFromAndroidManifest(externalAndroidManifest); + } else { + // The "tiapp.xml" did not contain any manifest settings. So, keep external manifest settings as-is. + this.customAndroidManifest = externalAndroidManifest; + } } } + } catch (ex) { + logger.error(`Malformed custom AndroidManifest.xml file: ${externalAndroidManifestFilePath}`); + process.exit(1); } - } catch (ex) { - logger.error(__n('Malformed custom AndroidManifest.xml file: %s', externalAndroidManifestFilePath)); - process.exit(1); - } - // validate the sdk levels - const usesSDK = this.customAndroidManifest ? this.customAndroidManifest.getUsesSdk() : null; - - this.minSDK = this.minSupportedApiLevel; - this.targetSDK = cli.tiapp.android && ~~cli.tiapp.android['tool-api-level'] || null; - this.maxSDK = null; - - if (this.targetSDK) { - logger.log(); - logger.warn(__('%s has been deprecated, please specify the target SDK API using the %s tag:', ''.cyan, ''.cyan)); - logger.warn(); - logger.warn(''.grey); - logger.warn(' '.grey); - logger.warn(' '.grey); - logger.warn((' ').magenta); - logger.warn(' '.grey); - logger.warn(' '.grey); - logger.warn(''.grey); - logger.log(); - } + // validate the sdk levels + const usesSDK = this.customAndroidManifest ? this.customAndroidManifest.getUsesSdk() : null; - if (usesSDK) { - usesSDK.minSdkVersion && (this.minSDK = usesSDK.minSdkVersion); - usesSDK.targetSdkVersion && (this.targetSDK = usesSDK.targetSdkVersion); - usesSDK.maxSdkVersion && (this.maxSDK = usesSDK.maxSdkVersion); - } + this.minSDK = this.minSupportedApiLevel; + this.targetSDK = cli.tiapp.android && ~~cli.tiapp.android['tool-api-level'] || null; + this.maxSDK = null; - // we need to translate the sdk to a real api level (i.e. L => 20, MNC => 22) so that - // we can validate them - function getRealAPILevel(ver) { - return (ver && targetSDKMap[ver] && targetSDKMap[ver].sdk) || ver; - } - this.realMinSDK = getRealAPILevel(this.minSDK); - this.realTargetSDK = getRealAPILevel(this.targetSDK); - this.realMaxSDK = getRealAPILevel(this.maxSDK); - - // min sdk is too old - if (this.minSDK && this.realMinSDK < this.minSupportedApiLevel) { - logger.error(__('The minimum supported SDK API version must be %s or newer, but is currently set to %s', this.minSupportedApiLevel, this.minSDK + (this.minSDK !== this.realMinSDK ? ' (' + this.realMinSDK + ')' : '')) + '\n'); - logger.log( - appc.string.wrap( - __('Update the %s in the tiapp.xml or custom AndroidManifest to at least %s:', 'android:minSdkVersion'.cyan, String(this.minSupportedApiLevel).cyan), - config.get('cli.width', 100) - ) - ); - logger.log(); - logger.log(''.grey); - logger.log(' '.grey); - logger.log(' '.grey); - logger.log((' ').magenta); - logger.log(' '.grey); - logger.log(' '.grey); - logger.log(''.grey); - logger.log(); - process.exit(1); - } + if (this.targetSDK) { + logger.log(); + logger.warn(`${ + ''.cyan + } has been deprecated, please specify the target SDK API using the ${ + ''.cyan + } tag:`); + logger.warn(); + logger.warn(''.grey); + logger.warn(' '.grey); + logger.warn(' '.grey); + logger.warn((' ').magenta); + logger.warn(' '.grey); + logger.warn(' '.grey); + logger.warn(''.grey); + logger.log(); + } - if (this.targetSDK) { - // target sdk is too old - if (this.realTargetSDK < this.minTargetApiLevel) { - logger.error(__('The target SDK API %s is not supported by Titanium SDK %s', this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : ''), ti.manifest.version)); - logger.error(__('The target SDK API version must be %s or newer', this.minTargetApiLevel) + '\n'); + if (usesSDK) { + usesSDK.minSdkVersion && (this.minSDK = usesSDK.minSdkVersion); + usesSDK.targetSdkVersion && (this.targetSDK = usesSDK.targetSdkVersion); + usesSDK.maxSdkVersion && (this.maxSDK = usesSDK.maxSdkVersion); + } + + // we need to translate the sdk to a real api level (i.e. L => 20, MNC => 22) so that + // we can validate them + function getRealAPILevel(ver) { + return (ver && targetSDKMap[ver] && targetSDKMap[ver].sdk) || ver; + } + this.realMinSDK = getRealAPILevel(this.minSDK); + this.realTargetSDK = getRealAPILevel(this.targetSDK); + this.realMaxSDK = getRealAPILevel(this.maxSDK); + + // min sdk is too old + if (this.minSDK && this.realMinSDK < this.minSupportedApiLevel) { + logger.error(`The minimum supported SDK API version must be ${ + this.minSupportedApiLevel + } or newer, but is currently set to ${ + this.minSDK + }${ + this.minSDK !== this.realMinSDK ? ` (${this.realMinSDK})` : '' + }\n`); logger.log( appc.string.wrap( - __('Update the %s in the tiapp.xml or custom AndroidManifest to at least %s:', 'android:targetSdkVersion'.cyan, String(this.minTargetApiLevel).cyan), + `Update the ${ + 'android:minSdkVersion'.cyan + } in the tiapp.xml or custom AndroidManifest to at least ${ + String(this.minSupportedApiLevel).cyan + }:`, config.get('cli.width', 100) ) ); @@ -1150,8 +1139,8 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) { logger.log(' '.grey); logger.log(' '.grey); logger.log((' ').magenta); logger.log(' '.grey); @@ -1161,2657 +1150,2736 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) { process.exit(1); } - // target sdk < min sdk - if (this.realTargetSDK < this.realMinSDK) { - logger.error(__('The target SDK API must be greater than or equal to the minimum SDK %s, but is currently set to %s', - this.minSDK + (this.minSDK !== this.realMinSDK ? ' (' + this.realMinSDK + ')' : ''), - this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : '') - ) + '\n'); - process.exit(1); - } - - } else { - this.targetSDK = this.maxSupportedApiLevel; - this.realTargetSDK = this.targetSDK; - } - - // check that we have this target sdk installed - this.androidTargetSDK = targetSDKMap[this.targetSDK]; - - if (!this.androidTargetSDK) { - this.androidTargetSDK = { - sdk: this.targetSDK - }; - } - - if (this.realTargetSDK < this.realMinSDK) { - logger.error(__('Target Android SDK API version must be %s or newer', this.minSDK) + '\n'); - process.exit(1); - } - - if (this.realMaxSDK && this.realMaxSDK < this.realTargetSDK) { - logger.error(__('Maximum Android SDK API version must be greater than or equal to the target SDK API %s, but is currently set to %s', - this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : ''), - this.maxSDK + (this.maxSDK !== this.realMaxSDK ? ' (' + this.realMaxSDK + ')' : '') - ) + '\n'); - process.exit(1); - } - - if (this.maxSupportedApiLevel && this.realTargetSDK > this.maxSupportedApiLevel) { - // print warning that version this.targetSDK is not tested - logger.warn(__('Building with Android SDK API %s which hasn\'t been tested against Titanium SDK %s', - String(this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : '')).cyan, - this.titaniumSdkVersion - )); - } - - // determine the abis to support - this.abis = this.validABIs; - const customABIs = cli.tiapp.android && cli.tiapp.android.abi && cli.tiapp.android.abi.indexOf('all') === -1; - if (!customABIs && (this.deployType === 'production')) { - // If "tiapp.xml" does not have entry, then exclude "x86" and "x86_64" from production builds by default. - // These abis are mostly needed for testing in an emulator. Physical x86 devices are extremely rare. - this.abis = this.abis.filter(abi => { - return !abi.startsWith('x86'); - }); - } - if (customABIs) { - this.abis = cli.tiapp.android.abi; - this.abis.forEach(function (abi) { - if (this.validABIs.indexOf(abi) === -1) { - logger.error(__('Invalid ABI "%s"', abi) + '\n'); - logger.log(__('Valid ABIs:')); - this.validABIs.forEach(function (name) { - logger.log(' ' + name.cyan); - }); + if (this.targetSDK) { + // target sdk is too old + if (this.realTargetSDK < this.minTargetApiLevel) { + logger.error(`The target SDK API ${ + this.targetSDK + }${ + this.targetSDK !== this.realTargetSDK ? ` (${this.realTargetSDK})` : '' + } is not supported by Titanium SDK ${ + ti.manifest.version + }`); + logger.error(`The target SDK API version must be ${this.minTargetApiLevel} or newer\n`); + logger.log( + appc.string.wrap( + `Update the ${ + 'android:targetSdkVersion'.cyan + } in the tiapp.xml or custom AndroidManifest to at least ${ + String(this.minTargetApiLevel).cyan + }:`, + config.get('cli.width', 100) + ) + ); + logger.log(); + logger.log(''.grey); + logger.log(' '.grey); + logger.log(' '.grey); + logger.log((' ').magenta); + logger.log(' '.grey); + logger.log(' '.grey); + logger.log(''.grey); logger.log(); process.exit(1); } - }, this); - } - let deviceId = cli.argv['device-id']; + // target sdk < min sdk + if (this.realTargetSDK < this.realMinSDK) { + logger.error(`The target SDK API must be greater than or equal to the minimum SDK ${ + this.minSDK + }${ + this.minSDK !== this.realMinSDK ? ` (${this.realMinSDK})` : '' + }, but is currently set to ${ + this.targetSDK + }${ + this.targetSDK !== this.realTargetSDK ? ` (${this.realTargetSDK})` : '' + }\n`); + process.exit(1); + } - if (!this.buildOnly && /^device|emulator$/.test(this.target) && deviceId === undefined && config.get('android.autoSelectDevice', true)) { - // no --device-id, so intelligently auto select one - const apiLevel = this.androidTargetSDK.sdk, - devicesToAutoSelectFrom = this.devicesToAutoSelectFrom.sort((a, b) => b.api - a.api), - len = devicesToAutoSelectFrom.length; + } else { + this.targetSDK = this.maxSupportedApiLevel; + this.realTargetSDK = this.targetSDK; + } - // reset the device id - deviceId = null; + // check that we have this target sdk installed + this.androidTargetSDK = targetSDKMap[this.targetSDK]; - if (cli.argv.target === 'device') { - logger.info('Auto selecting device'); - } else { - logger.info('Auto selecting emulator'); + if (!this.androidTargetSDK) { + this.androidTargetSDK = { + sdk: this.targetSDK + }; } - function setDeviceId(device) { - deviceId = cli.argv['device-id'] = device.id; + if (this.realTargetSDK < this.realMinSDK) { + logger.error(`Target Android SDK API version must be ${this.minSDK} or newer\n`); + process.exit(1); + } - let gapi = ''; - if (device.googleApis) { - gapi = (' (' + __('Google APIs supported') + ')').grey; - } else if (device.googleApis === null) { - gapi = (' (' + __('Google APIs support unknown') + ')').grey; - } + if (this.realMaxSDK && this.realMaxSDK < this.realTargetSDK) { + logger.error(`Maximum Android SDK API version must be greater than or equal to the target SDK API ${ + this.targetSDK + }${ + this.targetSDK !== this.realTargetSDK ? ` (${this.realTargetSDK})` : '' + }, but is currently set to ${ + this.maxSDK + }${ + this.maxSDK !== this.realMaxSDK ? ` (${this.realMaxSDK})` : '' + }\n`); + process.exit(1); + } + + if (this.maxSupportedApiLevel && this.realTargetSDK > this.maxSupportedApiLevel) { + // print warning that version this.targetSDK is not tested + logger.warn(`Building with Android SDK API ${ + this.targetSDK + }${ + this.targetSDK !== this.realTargetSDK ? ` (${this.realTargetSDK})` : '' + } which hasn't been tested against Titanium SDK ${ + this.titaniumSdkVersion + }`); + } + + // determine the abis to support + this.abis = this.validABIs; + const customABIs = cli.tiapp.android && cli.tiapp.android.abi && cli.tiapp.android.abi.indexOf('all') === -1; + if (!customABIs && (this.deployType === 'production')) { + // If "tiapp.xml" does not have entry, then exclude "x86" and "x86_64" from production builds by default. + // These abis are mostly needed for testing in an emulator. Physical x86 devices are extremely rare. + this.abis = this.abis.filter(abi => { + return !abi.startsWith('x86'); + }); + } + if (customABIs) { + this.abis = cli.tiapp.android.abi; + this.abis.forEach(function (abi) { + if (this.validABIs.indexOf(abi) === -1) { + logger.error(`Invalid ABI "${abi}"\n`); + logger.log('Valid ABIs:'); + this.validABIs.forEach(function (name) { + logger.log(' ' + name.cyan); + }); + logger.log(); + process.exit(1); + } + }, this); + } + + let deviceId = cli.argv['device-id']; + + if (!this.buildOnly && /^device|emulator$/.test(this.target) && deviceId === undefined && config.get('android.autoSelectDevice', true)) { + // no --device-id, so intelligently auto select one + const apiLevel = this.androidTargetSDK.sdk, + devicesToAutoSelectFrom = this.devicesToAutoSelectFrom.sort((a, b) => b.api - a.api), + len = devicesToAutoSelectFrom.length; + + // reset the device id + deviceId = null; if (cli.argv.target === 'device') { - logger.info(__('Auto selected device %s %s', device.name.cyan, device.version) + gapi); + logger.info('Auto selecting device'); } else { - logger.info(__('Auto selected emulator %s %s', device.name.cyan, device.version) + gapi); + logger.info('Auto selecting emulator'); } - } - logger.debug(__('Searching for API >= %s and has Google APIs', apiLevel.cyan)); - for (let i = 0; i < len; i++) { - if (devicesToAutoSelectFrom[i].api >= apiLevel && devicesToAutoSelectFrom[i].googleApis) { - setDeviceId(devicesToAutoSelectFrom[i]); - break; + function setDeviceId(device) { + deviceId = cli.argv['device-id'] = device.id; + + let gapi = ''; + if (device.googleApis) { + gapi = (' (Google APIs supported)').grey; + } else if (device.googleApis === null) { + gapi = (' (Google APIs support unknown)').grey; + } + + logger.info(`Auto selected ${ + cli.argv.target === 'device' ? 'device' : 'emulator' + } ${device.name.cyan} ${device.version}${gapi}`); } - } - if (!deviceId) { - logger.debug(__('Searching for API >= %s', apiLevel.cyan)); + logger.debug(`Searching for API >= ${apiLevel.cyan} and has Google APIs`); for (let i = 0; i < len; i++) { - if (devicesToAutoSelectFrom[i].api >= apiLevel) { + if (devicesToAutoSelectFrom[i].api >= apiLevel && devicesToAutoSelectFrom[i].googleApis) { setDeviceId(devicesToAutoSelectFrom[i]); break; } } if (!deviceId) { - logger.debug(__('Searching for API < %s and has Google APIs', apiLevel.cyan)); + logger.debug(`Searching for API >= ${apiLevel.cyan}`); for (let i = 0; i < len; i++) { - if (devicesToAutoSelectFrom[i].api < apiLevel && devicesToAutoSelectFrom[i].googleApis) { // eslint-disable-line max-depth + if (devicesToAutoSelectFrom[i].api >= apiLevel) { setDeviceId(devicesToAutoSelectFrom[i]); break; } } if (!deviceId) { - logger.debug(__('Searching for API < %s', apiLevel.cyan)); - for (let i = 0; i < len; i++) { // eslint-disable-line max-depth - if (devicesToAutoSelectFrom[i].api < apiLevel) { // eslint-disable-line max-depth + logger.debug(`Searching for API < ${apiLevel.cyan} and has Google APIs`); + for (let i = 0; i < len; i++) { + if (devicesToAutoSelectFrom[i].api < apiLevel && devicesToAutoSelectFrom[i].googleApis) { // eslint-disable-line max-depth setDeviceId(devicesToAutoSelectFrom[i]); break; } } - if (!deviceId) { // eslint-disable-line max-depth - logger.debug(__('Selecting first device')); - setDeviceId(devicesToAutoSelectFrom[0]); + if (!deviceId) { + logger.debug(`Searching for API < ${apiLevel.cyan}`); + for (let i = 0; i < len; i++) { // eslint-disable-line max-depth + if (devicesToAutoSelectFrom[i].api < apiLevel) { // eslint-disable-line max-depth + setDeviceId(devicesToAutoSelectFrom[i]); + break; + } + } + + if (!deviceId) { // eslint-disable-line max-depth + logger.debug('Selecting first device'); + setDeviceId(devicesToAutoSelectFrom[0]); + } } } } + + const devices = deviceId === 'all' + ? this.devices + : this.devices.filter(function (d) { return d.id === deviceId; }); + devices.forEach(function (device) { + if (Array.isArray(device.abi) && !device.abi.some(function (a) { return this.abis.indexOf(a) !== -1; }.bind(this))) { // eslint-disable-line max-statements-per-line + if (this.target === 'emulator') { + logger.error(`The emulator "${ + device.name + }" does not support the desired ABI${ + this.abis.length === 1 ? '' : 's' + } ${`"${this.abis.join('", "')}"`}`); + } else { + logger.error(`The device "${ + device.model || device.manufacturer + }" does not support the desired ABI${ + this.abis.length === 1 ? '' : 's' + } ${ + `"${this.abis.join('", "')}"` + }`); + } + logger.error(`Supported ABIs: ${device.abi.join(', ')}\n`); + + logger.log('You need to add at least one of the device\'s supported ABIs to the tiapp.xml'); + logger.log(); + logger.log(''.grey); + logger.log(' '.grey); + logger.log(' '.grey); + logger.log((' ' + this.abis.concat(device.abi).join(',') + '').magenta); + logger.log(' '.grey); + logger.log(''.grey); + logger.log(); + + process.exit(1); + } + }, this); } - const devices = deviceId === 'all' - ? this.devices - : this.devices.filter(function (d) { return d.id === deviceId; }); - devices.forEach(function (device) { - if (Array.isArray(device.abi) && !device.abi.some(function (a) { return this.abis.indexOf(a) !== -1; }.bind(this))) { // eslint-disable-line max-statements-per-line - if (this.target === 'emulator') { - logger.error(__n('The emulator "%%s" does not support the desired ABI %%s', 'The emulator "%%s" does not support the desired ABIs %%s', this.abis.length, device.name, '"' + this.abis.join('", "') + '"')); - } else { - logger.error(__n('The device "%%s" does not support the desired ABI %%s', 'The device "%%s" does not support the desired ABIs %%s', this.abis.length, device.model || device.manufacturer, '"' + this.abis.join('", "') + '"')); + // validate debugger and profiler options + const tool = []; + this.allowDebugging && tool.push('debug'); + this.allowProfiling && tool.push('profiler'); + this.debugHost = null; + this.debugPort = null; + this.profilerHost = null; + this.profilerPort = null; + tool.forEach(function (type) { + if (cli.argv[type + '-host']) { + if (typeof cli.argv[type + '-host'] === 'number') { + logger.error(`Invalid ${type} host "${cli.argv[type + '-host']}"\n`); + logger.log(`The ${type} host must be in the format "host:port".\n`); + process.exit(1); } - logger.error(__('Supported ABIs: %s', device.abi.join(', ')) + '\n'); - logger.log(__('You need to add at least one of the device\'s supported ABIs to the tiapp.xml')); - logger.log(); - logger.log(''.grey); - logger.log(' '.grey); - logger.log(' '.grey); - logger.log((' ' + this.abis.concat(device.abi).join(',') + '').magenta); - logger.log(' '.grey); - logger.log(''.grey); - logger.log(); + const parts = cli.argv[type + '-host'].split(':'); + if (parts.length < 2) { + logger.error(`Invalid ${type} host "${cli.argv[type + '-host']}"\n`); + logger.log(`The ${type} host must be in the format "host:port".\n`); + process.exit(1); + } - process.exit(1); - } - }, this); - } + const port = parseInt(parts[1]); + if (isNaN(port) || port < 1 || port > 65535) { + logger.error(`Invalid ${type} host "${cli.argv[type + '-host']}"\n`); + logger.log('The port must be a valid integer between 1 and 65535.\n'); + process.exit(1); + } - // validate debugger and profiler options - const tool = []; - this.allowDebugging && tool.push('debug'); - this.allowProfiling && tool.push('profiler'); - this.debugHost = null; - this.debugPort = null; - this.profilerHost = null; - this.profilerPort = null; - tool.forEach(function (type) { - if (cli.argv[type + '-host']) { - if (typeof cli.argv[type + '-host'] === 'number') { - logger.error(__('Invalid %s host "%s"', type, cli.argv[type + '-host']) + '\n'); - logger.log(__('The %s host must be in the format "host:port".', type) + '\n'); - process.exit(1); + this[type + 'Host'] = parts[0]; + this[type + 'Port'] = port; } + }, this); - const parts = cli.argv[type + '-host'].split(':'); - if (parts.length < 2) { - logger.error(__('Invalid ' + type + ' host "%s"', cli.argv[type + '-host']) + '\n'); - logger.log(__('The %s host must be in the format "host:port".', type) + '\n'); + if (this.debugPort || this.profilerPort) { + // if debugging/profiling, make sure we only have one device and that it has an sd card + if (this.target === 'emulator') { + const emu = this.devices.filter(function (d) { return d.id === deviceId; }).shift(); // eslint-disable-line max-statements-per-line + if (!emu) { + logger.error(`Unable find emulator "${deviceId}"\n`); + process.exit(1); + } else if (!emu.sdcard) { + logger.error(`The selected emulator "${emu.name}" does not have an SD card.`); + if (this.profilerPort) { + logger.error('An SD card is required for profiling.\n'); + } else { + logger.error('An SD card is required for debugging.\n'); + } + process.exit(1); + } + } else if (this.target === 'device' && deviceId === 'all' && this.devices.length > 1) { + // fail, can't do 'all' for debug builds + logger.error('Cannot debug application when --device-id is set to "all" and more than one device is connected.'); + logger.error('Please specify a single device to debug on.\n'); process.exit(1); } + } - const port = parseInt(parts[1]); - if (isNaN(port) || port < 1 || port > 65535) { - logger.error(__('Invalid ' + type + ' host "%s"', cli.argv[type + '-host']) + '\n'); - logger.log(__('The port must be a valid integer between 1 and 65535.') + '\n'); + // check that the build directory is writeable + const buildDir = path.join(cli.argv['project-dir'], 'build'); + if (fs.existsSync(buildDir)) { + if (!afs.isDirWritable(buildDir)) { + logger.error(`The build directory is not writeable: ${buildDir}\n`); + logger.log('Make sure the build directory is writeable and that you have sufficient free disk space.\n'); process.exit(1); } - - this[type + 'Host'] = parts[0]; - this[type + 'Port'] = port; + } else if (!afs.isDirWritable(cli.argv['project-dir'])) { + logger.error(`The project directory is not writeable: ${cli.argv['project-dir']}\n`); + logger.log('Make sure the project directory is writeable and that you have sufficient free disk space.\n'); + process.exit(1); } - }, this); - if (this.debugPort || this.profilerPort) { - // if debugging/profiling, make sure we only have one device and that it has an sd card - if (this.target === 'emulator') { - const emu = this.devices.filter(function (d) { return d.id === deviceId; }).shift(); // eslint-disable-line max-statements-per-line - if (!emu) { - logger.error(__('Unable find emulator "%s"', deviceId) + '\n'); - process.exit(1); - } else if (!emu.sdcard && emu.type !== 'genymotion') { - logger.error(__('The selected emulator "%s" does not have an SD card.', emu.name)); - if (this.profilerPort) { - logger.error(__('An SD card is required for profiling.') + '\n'); - } else { - logger.error(__('An SD card is required for debugging.') + '\n'); + // make sure we have an icon + this.appIconManifestValue = null; + this.appRoundIconManifestValue = null; + if (this.customAndroidManifest) { + // Fetch the app "icon" and "roundIcon" attributes as-is from the "AndroidManfiest.xml". + this.appIconManifestValue = this.customAndroidManifest.getAppAttribute('android:icon'); + this.appRoundIconManifestValue = this.customAndroidManifest.getAppAttribute('android:roundIcon'); + if (this.appIconManifestValue) { + // Turn the "android:icon" value to an image file name. Remove the "@drawable/" or "@mipmap/" prefix. + let appIconName = this.appIconManifestValue; + const index = appIconName.lastIndexOf('/'); + if (index >= 0) { + appIconName = appIconName.substring(index + 1); } - process.exit(1); + cli.tiapp.icon = appIconName + '.png'; } - } else if (this.target === 'device' && deviceId === 'all' && this.devices.length > 1) { - // fail, can't do 'all' for debug builds - logger.error(__('Cannot debug application when --device-id is set to "all" and more than one device is connected.')); - logger.error(__('Please specify a single device to debug on.') + '\n'); - process.exit(1); } - } - - // check that the build directory is writeable - const buildDir = path.join(cli.argv['project-dir'], 'build'); - if (fs.existsSync(buildDir)) { - if (!afs.isDirWritable(buildDir)) { - logger.error(__('The build directory is not writeable: %s', buildDir) + '\n'); - logger.log(__('Make sure the build directory is writeable and that you have sufficient free disk space.') + '\n'); - process.exit(1); + if (!cli.tiapp.icon || ![ 'Resources', 'Resources/android' ].some(function (p) { + return fs.existsSync(cli.argv['project-dir'], p, cli.tiapp.icon); + })) { + cli.tiapp.icon = 'appicon.png'; } - } else if (!afs.isDirWritable(cli.argv['project-dir'])) { - logger.error(__('The project directory is not writeable: %s', cli.argv['project-dir']) + '\n'); - logger.log(__('Make sure the project directory is writeable and that you have sufficient free disk space.') + '\n'); - process.exit(1); - } - - // make sure we have an icon - this.appIconManifestValue = null; - this.appRoundIconManifestValue = null; - if (this.customAndroidManifest) { - // Fetch the app "icon" and "roundIcon" attributes as-is from the "AndroidManfiest.xml". - this.appIconManifestValue = this.customAndroidManifest.getAppAttribute('android:icon'); - this.appRoundIconManifestValue = this.customAndroidManifest.getAppAttribute('android:roundIcon'); - if (this.appIconManifestValue) { - // Turn the "android:icon" value to an image file name. Remove the "@drawable/" or "@mipmap/" prefix. - let appIconName = this.appIconManifestValue; - const index = appIconName.lastIndexOf('/'); + if (!this.appIconManifestValue) { + this.appIconManifestValue = '@drawable/' + cli.tiapp.icon; + const index = this.appIconManifestValue.indexOf('.'); if (index >= 0) { - appIconName = appIconName.substring(index + 1); + this.appIconManifestValue = this.appIconManifestValue.substring(0, index); } - cli.tiapp.icon = appIconName + '.png'; - } - } - if (!cli.tiapp.icon || ![ 'Resources', 'Resources/android' ].some(function (p) { - return fs.existsSync(cli.argv['project-dir'], p, cli.tiapp.icon); - })) { - cli.tiapp.icon = 'appicon.png'; - } - if (!this.appIconManifestValue) { - this.appIconManifestValue = '@drawable/' + cli.tiapp.icon; - const index = this.appIconManifestValue.indexOf('.'); - if (index >= 0) { - this.appIconManifestValue = this.appIconManifestValue.substring(0, index); } - } - return function (callback) { - this.validateTiModules('android', this.deployType, function validateTiModulesCallback(err, modules) { - // Create a copy of the given modules found in "tiapp.xml", excluding modules that we no longer support. - const blacklistedModuleNames = [ 'com.soasta.touchtest' ]; - this.modules = modules.found.filter((module) => { - const isBlackListed = blacklistedModuleNames.includes(module.id); - if (isBlackListed) { - this.logger.warn(__('Skipping unsupported module "%s"', module.id.cyan)); - } - return !isBlackListed; - }); + return function (callback) { + this.validateTiModules('android', this.deployType, function validateTiModulesCallback(err, modules) { + // Create a copy of the given modules found in "tiapp.xml", excluding modules that we no longer support. + const blacklistedModuleNames = [ 'com.soasta.touchtest' ]; + this.modules = modules.found.filter((module) => { + const isBlackListed = blacklistedModuleNames.includes(module.id); + if (isBlackListed) { + this.logger.warn(`Skipping unsupported module "${module.id.cyan}"`); + } + return !isBlackListed; + }); - for (const module of this.modules) { - // Flag object as either a native JAR/AAR module or a scripted CommonJS module for fast if-checks later. - module.native = (module.platform.indexOf('commonjs') < 0); + for (const module of this.modules) { + // Flag object as either a native JAR/AAR module or a scripted CommonJS module for fast if-checks later. + module.native = (module.platform.indexOf('commonjs') < 0); - // For native modules, verify they are built with API version 2.0 or higher. - if (module.native && (~~module.manifest.apiversion < 2)) { - this.logger.error(__('The "apiversion" for "%s" in the module manifest is less than version 2.', module.manifest.moduleid.cyan)); - this.logger.error(__('The module was likely built against a Titanium SDK 1.8.0.1 or older.')); - this.logger.error(__('Please use a version of the module that has "apiversion" 2 or greater')); - this.logger.log(); - process.exit(1); - } + // For native modules, verify they are built with API version 2.0 or higher. + if (module.native && (~~module.manifest.apiversion < 2)) { + this.logger.error(`The "apiversion" for "${module.manifest.moduleid.cyan}" in the module manifest is less than version 2.`); + this.logger.error('The module was likely built against a Titanium SDK 1.8.0.1 or older.'); + this.logger.error('Please use a version of the module that has "apiversion" 2 or greater'); + this.logger.log(); + process.exit(1); + } - // For CommonJS modules, verify we can find the main script to be loaded by require() method. - if (!module.native) { - // Look for legacy ".js" script file first. - let jsFilePath = path.join(module.modulePath, module.id + '.js'); - if (!fs.existsSync(jsFilePath)) { - // Check if require API can find the script. - jsFilePath = require.resolve(module.modulePath); + // For CommonJS modules, verify we can find the main script to be loaded by require() method. + if (!module.native) { + // Look for legacy ".js" script file first. + let jsFilePath = path.join(module.modulePath, module.id + '.js'); if (!fs.existsSync(jsFilePath)) { - this.logger.error(__( - 'Module "%s" v%s is missing main file: %s, package.json with "main" entry, index.js, or index.json', - module.id, module.manifest.version || 'latest', module.id + '.js') + '\n'); - process.exit(1); + // Check if require API can find the script. + jsFilePath = require.resolve(module.modulePath); + if (!fs.existsSync(jsFilePath)) { + this.logger.error( + `Module "${ + module.id + }" ${ + module.manifest.version ? `v${module.manifest.version}` : 'latest' + } is missing main file: ${ + module.id + }.js, package.json with "main" entry, index.js, or index.json\n` + ); + process.exit(1); + } } + } else { + // Limit application build ABI to that of provided native modules. + this.abis = this.abis.filter(abi => { + if (!module.manifest.architectures.includes(abi)) { + this.logger.warn(`Module ${ + module.id.cyan + } does not contain ${ + abi.cyan + } ABI. Application will build without ${ + abi.cyan + } ABI support!`); + return false; + } + return true; + }); } - } else { - // Limit application build ABI to that of provided native modules. - this.abis = this.abis.filter(abi => { - if (!module.manifest.architectures.includes(abi)) { - this.logger.warn(__('Module %s does not contain %s ABI. Application will build without %s ABI support!', module.id.cyan, abi.cyan, abi.cyan)); - return false; - } - return true; - }); - } - // scan the module for any CLI hooks - cli.scanHooks(path.join(module.modulePath, 'hooks')); - } - - // check for any missing module dependencies - let hasAddedModule = false; - for (const module of this.modules) { - if (!module.native) { - continue; + // scan the module for any CLI hooks + cli.scanHooks(path.join(module.modulePath, 'hooks')); } - const timoduleXmlFile = path.join(module.modulePath, 'timodule.xml'); - const timodule = fs.existsSync(timoduleXmlFile) ? new tiappxml(timoduleXmlFile) : undefined; + // check for any missing module dependencies + let hasAddedModule = false; + for (const module of this.modules) { + if (!module.native) { + continue; + } - if (timodule && Array.isArray(timodule.modules)) { - for (let dependency of timodule.modules) { - if (!dependency.platform || /^android$/.test(dependency.platform)) { - const isMissing = !this.modules.some(function (mod) { - return mod.native && (mod.id === dependency.id); - }); - if (isMissing) { - // attempt to include missing dependency - dependency.depended = module; - this.cli.tiapp.modules.push({ - id: dependency.id, - version: dependency.version, - platform: [ 'android' ], - deployType: [ this.deployType ] + const timoduleXmlFile = path.join(module.modulePath, 'timodule.xml'); + const timodule = fs.existsSync(timoduleXmlFile) ? new tiappxml(timoduleXmlFile) : undefined; + + if (timodule && Array.isArray(timodule.modules)) { + for (let dependency of timodule.modules) { + if (!dependency.platform || /^android$/.test(dependency.platform)) { + const isMissing = !this.modules.some(function (mod) { + return mod.native && (mod.id === dependency.id); }); - hasAddedModule = true; + if (isMissing) { + // attempt to include missing dependency + dependency.depended = module; + this.cli.tiapp.modules.push({ + id: dependency.id, + version: dependency.version, + platform: [ 'android' ], + deployType: [ this.deployType ] + }); + hasAddedModule = true; + } } } } } - } - // Re-validate if a module dependency was added to the modules array. - if (hasAddedModule) { - return this.validateTiModules('android', this.deployType, validateTiModulesCallback.bind(this)); - } + // Re-validate if a module dependency was added to the modules array. + if (hasAddedModule) { + return this.validateTiModules('android', this.deployType, validateTiModulesCallback.bind(this)); + } + + callback(); + }.bind(this)); + }.bind(this); + } - callback(); - }.bind(this)); - }.bind(this); -}; + async run(logger, config, cli, finished) { + try { + // Call the base builder's run() method. + super.run(logger, config, cli, finished); + + // Notify plugins that we're about to begin. + await new Promise(resolve => cli.emit('build.pre.construct', this, resolve)); + + // Initialize build system. Checks if we need to do a clean or incremental build. + await this.initialize(); + await this.loginfo(); + await this.computeHashes(); + await this.readBuildManifest(); + await this.checkIfNeedToRecompile(); + + // Notify plugins that we're prepping to compile. + await new Promise((resolve, reject) => { + cli.emit('build.pre.compile', this, e => (e ? reject(e) : resolve())); + }); -AndroidBuilder.prototype.run = async function run(logger, config, cli, finished) { - try { - // Call the base builder's run() method. - Builder.prototype.run.apply(this, arguments); + // Make sure we have an "app.js" script. Will exit with a build failure if not found. + // Note: This used to be validated by the validate() method, but Alloy plugin + // generates the "app.js" script via the "build.pre.compile" hook event above. + ti.validateAppJsExists(this.projectDir, logger, 'android'); - // Notify plugins that we're about to begin. - await new Promise(resolve => cli.emit('build.pre.construct', this, resolve)); + // Generate all gradle files, gradle app project, and gradle library projects (if needed). + await this.processLibraries(); + await this.generateRootProjectFiles(); + await this.generateAppProject(); - // Initialize build system. Checks if we need to do a clean or incremental build. - await this.initialize(); - await this.loginfo(); - await this.computeHashes(); - await this.readBuildManifest(); - await this.checkIfNeedToRecompile(); + // Build the app. + await new Promise(resolve => cli.emit('build.pre.build', this, resolve)); + await this.buildAppProject(); + await new Promise(resolve => cli.emit('build.post.build', this, resolve)); - // Notify plugins that we're prepping to compile. - await new Promise((resolve, reject) => { - cli.emit('build.pre.compile', this, e => (e ? reject(e) : resolve())); - }); + // Write Titanium build settings to file. Used to determine if next build can be incremental or not. + await this.writeBuildManifest(); - // Make sure we have an "app.js" script. Will exit with a build failure if not found. - // Note: This used to be validated by the validate() method, but Alloy plugin - // generates the "app.js" script via the "build.pre.compile" hook event above. - ti.validateAppJsExists(this.projectDir, logger, 'android'); - - // Generate all gradle files, gradle app project, and gradle library projects (if needed). - await this.processLibraries(); - await this.generateRootProjectFiles(); - await this.generateAppProject(); - - // Build the app. - await new Promise(resolve => cli.emit('build.pre.build', this, resolve)); - await this.buildAppProject(); - await new Promise(resolve => cli.emit('build.post.build', this, resolve)); - - // Write Titanium build settings to file. Used to determine if next build can be incremental or not. - await this.writeBuildManifest(); - - // Log how long the build took. - if (!this.buildOnly && this.target === 'simulator') { - const delta = appc.time.prettyDiff(this.cli.startTime, Date.now()); - logger.info(__('Finished building the application in %s', delta.cyan)); - } - - // Notify plugins that the build is done. - await new Promise(resolve => cli.emit('build.post.compile', this, resolve)); - await new Promise(resolve => cli.emit('build.finalize', this, resolve)); - } catch (err) { - // Failed to build app. Print the error message and stack trace (if possible), then exit out. - // Note: "err" can be whatever type (including undefined) that was passed into Promise.reject(). - if (err instanceof Error) { - this.logger.error(err.stack || err.message); - } else if ((typeof err === 'string') && (err.length > 0)) { - this.logger.error(err); - } else { - this.logger.error('Build failed. Reason: Unknown'); + // Log how long the build took. + if (!this.buildOnly && this.target === 'simulator') { + const delta = appc.time.prettyDiff(this.cli.startTime, Date.now()); + logger.info(`Finished building the application in ${delta.cyan}`); + } + + // Notify plugins that the build is done. + await new Promise(resolve => cli.emit('build.post.compile', this, resolve)); + await new Promise(resolve => cli.emit('build.finalize', this, resolve)); + } catch (err) { + // Failed to build app. Print the error message and stack trace (if possible), then exit out. + // Note: "err" can be whatever type (including undefined) that was passed into Promise.reject(). + if (err instanceof Error) { + this.logger.error(err.stack || err.message); + } else if ((typeof err === 'string') && (err.length > 0)) { + this.logger.error(err); + } else { + this.logger.error('Build failed. Reason: Unknown'); + } + process.exit(1); } - process.exit(1); - } - // We're done. Invoke optional callback if provided. - if (finished) { - finished(); + // We're done. Invoke optional callback if provided. + if (finished) { + finished(); + } } -}; -AndroidBuilder.prototype.initialize = async function initialize() { - const argv = this.cli.argv; + async initialize() { + const argv = this.cli.argv; - this.appid = this.tiapp.id; - this.appid.indexOf('.') === -1 && (this.appid = 'com.' + this.appid); + this.appid = this.tiapp.id; + this.appid.indexOf('.') === -1 && (this.appid = 'com.' + this.appid); - this.classname = this.tiapp.name.split(/[^A-Za-z0-9_]/).map(function (word) { - return appc.string.capitalize(word.toLowerCase()); - }).join(''); - /^[0-9]/.test(this.classname) && (this.classname = '_' + this.classname); + this.classname = this.tiapp.name.split(/[^A-Za-z0-9_]/).map(function (word) { + return appc.string.capitalize(word.toLowerCase()); + }).join(''); + /^[0-9]/.test(this.classname) && (this.classname = '_' + this.classname); - const deviceId = this.deviceId = argv['device-id']; - if (!this.buildOnly && this.target === 'emulator') { - const emu = this.devices.filter(function (e) { return e.id === deviceId; }).shift(); // eslint-disable-line max-statements-per-line - if (!emu) { - // sanity check - this.logger.error(__('Unable to find Android emulator "%s"', deviceId) + '\n'); - process.exit(0); + const deviceId = this.deviceId = argv['device-id']; + if (!this.buildOnly && this.target === 'emulator') { + const emu = this.devices.filter(function (e) { return e.id === deviceId; }).shift(); // eslint-disable-line max-statements-per-line + if (!emu) { + // sanity check + this.logger.error(`Unable to find Android emulator "${deviceId}"\n`); + process.exit(0); + } + this.emulator = emu; + } + + this.outputDir = argv['output-dir'] ? afs.resolvePath(argv['output-dir']) : null; + + // set the keystore to the dev keystore, if not already set + this.keystore = argv.keystore; + this.keystoreStorePassword = argv['store-password']; + this.keystoreKeyPassword = argv['key-password']; + this.sigalg = argv['sigalg']; + if (!this.keystore) { + this.keystore = path.join(this.platformPath, 'dev_keystore'); + this.keystoreStorePassword = 'tirocks'; + this.keystoreAlias = { + name: 'tidev', + sigalg: 'MD5withRSA' + }; } - this.emulator = emu; - } - this.outputDir = argv['output-dir'] ? afs.resolvePath(argv['output-dir']) : null; - - // set the keystore to the dev keystore, if not already set - this.keystore = argv.keystore; - this.keystoreStorePassword = argv['store-password']; - this.keystoreKeyPassword = argv['key-password']; - this.sigalg = argv['sigalg']; - if (!this.keystore) { - this.keystore = path.join(this.platformPath, 'dev_keystore'); - this.keystoreStorePassword = 'tirocks'; - this.keystoreAlias = { - name: 'tidev', - sigalg: 'MD5withRSA' - }; - } + const loadFromSDCardProp = this.tiapp.properties['ti.android.loadfromsdcard']; + this.loadFromSDCard = loadFromSDCardProp && loadFromSDCardProp.value === true; + + // Array of gradle/maven compatible library reference names the app project depends on. + // Formatted as: "::" + // Example: "com.google.android.gms:play-services-base:11.0.4" + this.libDependencyStrings = []; + + // Array of JAR/AAR library file paths the app project depends on. + this.libFilePaths = []; + + // Array of gradle library project names the app depends on. + this.libProjectNames = []; + + // Array of maven repository URLs the app project will need to search for dependencies. + // Typically set to local "file://" URLs referencing installed Titanium module. + this.mavenRepositoryUrls = []; + + // Set up directory paths. + this.buildAssetsDir = path.join(this.buildDir, 'assets'); + this.buildTiIncrementalDir = path.join(this.buildDir, 'ti-incremental'); + this.buildAppDir = path.join(this.buildDir, 'app'); + this.buildAppMainDir = path.join(this.buildAppDir, 'src', 'main'); + this.buildAppMainAssetsDir = path.join(this.buildAppMainDir, 'assets'); + this.buildAppMainAssetsResourcesDir = path.join(this.buildAppMainAssetsDir, 'Resources'); + this.buildGenAppIdDir = path.join(this.buildAppMainDir, 'java', this.appid.split('.').join(path.sep)); + this.buildAppMainResDir = path.join(this.buildAppMainDir, 'res'); + this.buildAppMainResDrawableDir = path.join(this.buildAppMainResDir, 'drawable'); + this.templatesDir = path.join(this.platformPath, 'templates', 'build'); + + // The "appc-cli-titanium" module reads this builder's "buildBinAssetsDir" variable when "tiapp.xml" + // property "appc-sourcecode-encryption-policy" is set to "remote" or "embed". + this.buildBinAssetsDir = this.buildAppMainAssetsDir; + + // Path to file storing some Titanium build settings. + // Used to determine if next build can be an incremental build or must be a clean/rebuild. + this.buildManifestFile = path.join(this.buildDir, 'build-manifest.json'); + + const buildTypeName = (this.allowDebugging) ? 'debug' : 'release'; + this.apkFile = path.join(this.buildDir, 'app', 'build', 'outputs', 'apk', buildTypeName, `app-${buildTypeName}.apk`); + + // Assign base builder file list for backwards compatibility with existing + // hooks that may use lastBuildFiles. + // TODO: remove in 9.0 + this.lastBuildFiles = this.buildDirFiles; + } + + loginfo() { + this.logger.debug(`Titanium SDK Android directory: ${this.platformPath.cyan}`); + this.logger.info(`Deploy type: ${this.deployType.cyan}`); + this.logger.info(`Building for target: ${this.target.cyan}`); + + if (this.buildOnly) { + this.logger.info('Performing build only'); + } else if (this.target === 'emulator') { + this.logger.info(`Building for emulator: ${this.deviceId.cyan}`); + } else if (this.target === 'device') { + this.logger.info(`Building for device: ${this.deviceId.cyan}`); + } + + this.logger.info(`Targeting Android SDK API: ${ + String(this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : '')).cyan + }`); + this.logger.info(`Building for the following architectures: ${ + this.abis.join(', ').cyan + }`); + this.logger.info(`Signing with keystore: ${ + (this.keystore + ' (' + this.keystoreAlias.name + ')').cyan + }`); + + this.logger.debug(`App ID: ${this.appid.cyan}`); + this.logger.debug(`Classname: ${this.classname.cyan}`); + + if (this.allowDebugging && this.debugPort) { + this.logger.info(`Debugging enabled via debug port: ${String(this.debugPort).cyan}`); + } else { + this.logger.info('Debugging disabled'); + } - const loadFromSDCardProp = this.tiapp.properties['ti.android.loadfromsdcard']; - this.loadFromSDCard = loadFromSDCardProp && loadFromSDCardProp.value === true; - - // Array of gradle/maven compatible library reference names the app project depends on. - // Formatted as: "::" - // Example: "com.google.android.gms:play-services-base:11.0.4" - this.libDependencyStrings = []; - - // Array of JAR/AAR library file paths the app project depends on. - this.libFilePaths = []; - - // Array of gradle library project names the app depends on. - this.libProjectNames = []; - - // Array of maven repository URLs the app project will need to search for dependencies. - // Typically set to local "file://" URLs referencing installed Titanium module. - this.mavenRepositoryUrls = []; - - // Set up directory paths. - this.buildAssetsDir = path.join(this.buildDir, 'assets'); - this.buildTiIncrementalDir = path.join(this.buildDir, 'ti-incremental'); - this.buildAppDir = path.join(this.buildDir, 'app'); - this.buildAppMainDir = path.join(this.buildAppDir, 'src', 'main'); - this.buildAppMainAssetsDir = path.join(this.buildAppMainDir, 'assets'); - this.buildAppMainAssetsResourcesDir = path.join(this.buildAppMainAssetsDir, 'Resources'); - this.buildGenAppIdDir = path.join(this.buildAppMainDir, 'java', this.appid.split('.').join(path.sep)); - this.buildAppMainResDir = path.join(this.buildAppMainDir, 'res'); - this.buildAppMainResDrawableDir = path.join(this.buildAppMainResDir, 'drawable'); - this.templatesDir = path.join(this.platformPath, 'templates', 'build'); - - // The "appc-cli-titanium" module reads this builder's "buildBinAssetsDir" variable when "tiapp.xml" - // property "appc-sourcecode-encryption-policy" is set to "remote" or "embed". - this.buildBinAssetsDir = this.buildAppMainAssetsDir; - - // Path to file storing some Titanium build settings. - // Used to determine if next build can be an incremental build or must be a clean/rebuild. - this.buildManifestFile = path.join(this.buildDir, 'build-manifest.json'); - - const buildTypeName = (this.allowDebugging) ? 'debug' : 'release'; - this.apkFile = path.join(this.buildDir, 'app', 'build', 'outputs', 'apk', buildTypeName, `app-${buildTypeName}.apk`); - - // Assign base builder file list for backwards compatibility with existing - // hooks that may use lastBuildFiles. - // TODO: remove in 9.0 - this.lastBuildFiles = this.buildDirFiles; -}; - -AndroidBuilder.prototype.loginfo = async function loginfo() { - this.logger.debug(__('Titanium SDK Android directory: %s', this.platformPath.cyan)); - this.logger.info(__('Deploy type: %s', this.deployType.cyan)); - this.logger.info(__('Building for target: %s', this.target.cyan)); - - if (this.buildOnly) { - this.logger.info(__('Performing build only')); - } else if (this.target === 'emulator') { - this.logger.info(__('Building for emulator: %s', this.deviceId.cyan)); - } else if (this.target === 'device') { - this.logger.info(__('Building for device: %s', this.deviceId.cyan)); - } + if (this.allowProfiling && this.profilerPort) { + this.logger.info(`Profiler enabled via profiler port: ${String(this.profilerPort).cyan}`); + } else { + this.logger.info('Profiler disabled'); + } - this.logger.info(__('Targeting Android SDK API: %s', String(this.targetSDK + (this.targetSDK !== this.realTargetSDK ? ' (' + this.realTargetSDK + ')' : '')).cyan)); - this.logger.info(__('Building for the following architectures: %s', this.abis.join(', ').cyan)); - this.logger.info(__('Signing with keystore: %s', (this.keystore + ' (' + this.keystoreAlias.name + ')').cyan)); + this.logger.info(`Transpile javascript: ${(this.transpile ? 'true' : 'false').cyan}`); + this.logger.info(`Generate source maps: ${(this.sourceMaps ? 'true' : 'false').cyan}`); + } - this.logger.debug(__('App ID: %s', this.appid.cyan)); - this.logger.debug(__('Classname: %s', this.classname.cyan)); + computeHashes() { + // modules + this.modulesHash = !Array.isArray(this.tiapp.modules) ? '' : this.hash(this.tiapp.modules.filter(function (m) { + return !m.platform || /^android|commonjs$/.test(m.platform); + }).map(function (m) { + return m.id + ',' + m.platform + ',' + m.version; + }).join('|')); - if (this.allowDebugging && this.debugPort) { - this.logger.info(__('Debugging enabled via debug port: %s', String(this.debugPort).cyan)); - } else { - this.logger.info(__('Debugging disabled')); + // tiapp.xml properties, activities, and services + this.propertiesHash = this.hash(this.tiapp.properties ? JSON.stringify(this.tiapp.properties) : ''); + const android = this.tiapp.android; + this.activitiesHash = this.hash(android && android.application && android.application ? JSON.stringify(android.application.activities) : ''); + this.servicesHash = this.hash(android && android.services ? JSON.stringify(android.services) : ''); } - if (this.allowProfiling && this.profilerPort) { - this.logger.info(__('Profiler enabled via profiler port: %s', String(this.profilerPort).cyan)); - } else { - this.logger.info(__('Profiler disabled')); - } + async readBuildManifest() { + // read the build manifest from the last build, if exists, so we + // can determine if we need to do a full rebuild + this.buildManifest = {}; - this.logger.info(__('Transpile javascript: %s', (this.transpile ? 'true' : 'false').cyan)); - this.logger.info(__('Generate source maps: %s', (this.sourceMaps ? 'true' : 'false').cyan)); -}; - -AndroidBuilder.prototype.computeHashes = async function computeHashes() { - // modules - this.modulesHash = !Array.isArray(this.tiapp.modules) ? '' : this.hash(this.tiapp.modules.filter(function (m) { - return !m.platform || /^android|commonjs$/.test(m.platform); - }).map(function (m) { - return m.id + ',' + m.platform + ',' + m.version; - }).join('|')); - - // tiapp.xml properties, activities, and services - this.propertiesHash = this.hash(this.tiapp.properties ? JSON.stringify(this.tiapp.properties) : ''); - const android = this.tiapp.android; - this.activitiesHash = this.hash(android && android.application && android.application ? JSON.stringify(android.application.activities) : ''); - this.servicesHash = this.hash(android && android.services ? JSON.stringify(android.services) : ''); -}; - -AndroidBuilder.prototype.readBuildManifest = async function readBuildManifest() { - // read the build manifest from the last build, if exists, so we - // can determine if we need to do a full rebuild - this.buildManifest = {}; - - if (await fs.exists(this.buildManifestFile)) { - try { - this.buildManifest = JSON.parse(await fs.readFile(this.buildManifestFile)) || {}; - } catch (e) { - // ignore + if (await fs.exists(this.buildManifestFile)) { + try { + this.buildManifest = JSON.parse(await fs.readFile(this.buildManifestFile)) || {}; + } catch (e) { + // ignore + } } } -}; -AndroidBuilder.prototype.checkIfShouldForceRebuild = function checkIfShouldForceRebuild() { - var manifest = this.buildManifest; + checkIfShouldForceRebuild() { + var manifest = this.buildManifest; - if (this.cli.argv.force) { - this.logger.info(__('Forcing rebuild: %s flag was set', '--force'.cyan)); - return true; - } + if (this.cli.argv.force) { + this.logger.info(`Forcing rebuild: ${'--force'.cyan} flag was set`); + return true; + } - if (!fs.existsSync(this.buildManifestFile)) { - this.logger.info(__('Forcing rebuild: %s does not exist', this.buildManifestFile.cyan)); - return true; - } + if (!fs.existsSync(this.buildManifestFile)) { + this.logger.info(`Forcing rebuild: ${this.buildManifestFile.cyan} does not exist`); + return true; + } - // check if the target changed - if (this.target !== manifest.target) { - this.logger.info(__('Forcing rebuild: target changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.target)); - this.logger.info(' ' + __('Now: %s', this.target)); - return true; - } + // check if the target changed + if (this.target !== manifest.target) { + this.logger.info('Forcing rebuild: target changed since last build'); + this.logger.info(` Was: ${manifest.target}`); + this.logger.info(` Now: ${this.target}`); + return true; + } - // check if the deploy type changed - if (this.deployType !== manifest.deployType) { - this.logger.info(__('Forcing rebuild: deploy type changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.deployType)); - this.logger.info(' ' + __('Now: %s', this.deployType)); - return true; - } + // check if the deploy type changed + if (this.deployType !== manifest.deployType) { + this.logger.info('Forcing rebuild: deploy type changed since last build'); + this.logger.info(` Was: ${manifest.deployType}`); + this.logger.info(` Now: ${this.deployType}`); + return true; + } - // check if the classname changed - if (this.classname !== manifest.classname) { - this.logger.info(__('Forcing rebuild: classname changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.classname)); - this.logger.info(' ' + __('Now: %s', this.classname)); - return true; - } + // check if the classname changed + if (this.classname !== manifest.classname) { + this.logger.info('Forcing rebuild: classname changed since last build'); + this.logger.info(` Was: ${manifest.classname}`); + this.logger.info(` Now: ${this.classname}`); + return true; + } - // if encryptJS changed, then we need to recompile the java files - if (this.encryptJS !== manifest.encryptJS) { - this.logger.info(__('Forcing rebuild: JavaScript encryption flag changed')); - this.logger.info(' ' + __('Was: %s', manifest.encryptJS)); - this.logger.info(' ' + __('Now: %s', this.encryptJS)); - return true; - } + // if encryptJS changed, then we need to recompile the java files + if (this.encryptJS !== manifest.encryptJS) { + this.logger.info('Forcing rebuild: JavaScript encryption flag changed'); + this.logger.info(` Was: ${manifest.encryptJS}`); + this.logger.info(` Now: ${this.encryptJS}`); + return true; + } - // check if the titanium sdk paths are different - if (this.platformPath !== manifest.platformPath) { - this.logger.info(__('Forcing rebuild: Titanium SDK path changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.platformPath)); - this.logger.info(' ' + __('Now: %s', this.platformPath)); - return true; - } + // check if the titanium sdk paths are different + if (this.platformPath !== manifest.platformPath) { + this.logger.info('Forcing rebuild: Titanium SDK path changed since last build'); + this.logger.info(` Was: ${manifest.platformPath}`); + this.logger.info(` Now: ${this.platformPath}`); + return true; + } - // check the git hashes are different - if (!manifest.gitHash || manifest.gitHash !== ti.manifest.githash) { - this.logger.info(__('Forcing rebuild: githash changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.gitHash)); - this.logger.info(' ' + __('Now: %s', ti.manifest.githash)); - return true; - } + // check the git hashes are different + if (!manifest.gitHash || manifest.gitHash !== ti.manifest.githash) { + this.logger.info('Forcing rebuild: githash changed since last build'); + this.logger.info(` Was: ${manifest.gitHash}`); + this.logger.info(` Now: ${ti.manifest.githash}`); + return true; + } - // check if the modules hashes are different - if (this.modulesHash !== manifest.modulesHash) { - this.logger.info(__('Forcing rebuild: modules hash changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.modulesHash)); - this.logger.info(' ' + __('Now: %s', this.modulesHash)); - return true; - } + // check if the modules hashes are different + if (this.modulesHash !== manifest.modulesHash) { + this.logger.info('Forcing rebuild: modules hash changed since last build'); + this.logger.info(` Was: ${manifest.modulesHash}`); + this.logger.info(` Now: ${this.modulesHash}`); + return true; + } - // next we check if any tiapp.xml values changed so we know if we need to reconstruct the main.m - if (this.tiapp.name !== manifest.name) { - this.logger.info(__('Forcing rebuild: tiapp.xml project name changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.name)); - this.logger.info(' ' + __('Now: %s', this.tiapp.name)); - return true; - } + // next we check if any tiapp.xml values changed so we know if we need to reconstruct the main.m + if (this.tiapp.name !== manifest.name) { + this.logger.info('Forcing rebuild: tiapp.xml project name changed since last build'); + this.logger.info(` Was: ${manifest.name}`); + this.logger.info(` Now: ${this.tiapp.name}`); + return true; + } - if (this.tiapp.id !== manifest.id) { - this.logger.info(__('Forcing rebuild: tiapp.xml app id changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.id)); - this.logger.info(' ' + __('Now: %s', this.tiapp.id)); - return true; - } + if (this.tiapp.id !== manifest.id) { + this.logger.info('Forcing rebuild: tiapp.xml app id changed since last build'); + this.logger.info(` Was: ${manifest.id}`); + this.logger.info(` Now: ${this.tiapp.id}`); + return true; + } - if (this.tiapp.publisher !== manifest.publisher) { - this.logger.info(__('Forcing rebuild: tiapp.xml publisher changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.publisher)); - this.logger.info(' ' + __('Now: %s', this.tiapp.publisher)); - return true; - } + if (this.tiapp.publisher !== manifest.publisher) { + this.logger.info('Forcing rebuild: tiapp.xml publisher changed since last build'); + this.logger.info(` Was: ${manifest.publisher}`); + this.logger.info(` Now: ${this.tiapp.publisher}`); + return true; + } - if (this.tiapp.url !== manifest.url) { - this.logger.info(__('Forcing rebuild: tiapp.xml url changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.url)); - this.logger.info(' ' + __('Now: %s', this.tiapp.url)); - return true; - } + if (this.tiapp.url !== manifest.url) { + this.logger.info('Forcing rebuild: tiapp.xml url changed since last build'); + this.logger.info(` Was: ${manifest.url}`); + this.logger.info(` Now: ${this.tiapp.url}`); + return true; + } - if (this.tiapp.version !== manifest.version) { - this.logger.info(__('Forcing rebuild: tiapp.xml version changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.version)); - this.logger.info(' ' + __('Now: %s', this.tiapp.version)); - return true; - } + if (this.tiapp.version !== manifest.version) { + this.logger.info('Forcing rebuild: tiapp.xml version changed since last build'); + this.logger.info(` Was: ${manifest.version}`); + this.logger.info(` Now: ${this.tiapp.version}`); + return true; + } - if (this.tiapp.description !== manifest.description) { - this.logger.info(__('Forcing rebuild: tiapp.xml description changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.description)); - this.logger.info(' ' + __('Now: %s', this.tiapp.description)); - return true; - } + if (this.tiapp.description !== manifest.description) { + this.logger.info('Forcing rebuild: tiapp.xml description changed since last build'); + this.logger.info(` Was: ${manifest.description}`); + this.logger.info(` Now: ${this.tiapp.description}`); + return true; + } - if (this.tiapp.copyright !== manifest.copyright) { - this.logger.info(__('Forcing rebuild: tiapp.xml copyright changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.copyright)); - this.logger.info(' ' + __('Now: %s', this.tiapp.copyright)); - return true; - } + if (this.tiapp.copyright !== manifest.copyright) { + this.logger.info('Forcing rebuild: tiapp.xml copyright changed since last build'); + this.logger.info(` Was: ${manifest.copyright}`); + this.logger.info(` Now: ${this.tiapp.copyright}`); + return true; + } - if (this.tiapp.guid !== manifest.guid) { - this.logger.info(__('Forcing rebuild: tiapp.xml guid changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.guid)); - this.logger.info(' ' + __('Now: %s', this.tiapp.guid)); - return true; - } + if (this.tiapp.guid !== manifest.guid) { + this.logger.info('Forcing rebuild: tiapp.xml guid changed since last build'); + this.logger.info(` Was: ${manifest.guid}`); + this.logger.info(` Now: ${this.tiapp.guid}`); + return true; + } - if (this.tiapp.icon !== manifest.icon) { - this.logger.info(__('Forcing rebuild: tiapp.xml icon changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.icon)); - this.logger.info(' ' + __('Now: %s', this.tiapp.icon)); - return true; - } + if (this.tiapp.icon !== manifest.icon) { + this.logger.info('Forcing rebuild: tiapp.xml icon changed since last build'); + this.logger.info(` Was: ${manifest.icon}`); + this.logger.info(` Now: ${this.tiapp.icon}`); + return true; + } - if (this.tiapp.fullscreen !== manifest.fullscreen) { - this.logger.info(__('Forcing rebuild: tiapp.xml fullscreen changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.fullscreen)); - this.logger.info(' ' + __('Now: %s', this.tiapp.fullscreen)); - return true; - } + if (this.tiapp.fullscreen !== manifest.fullscreen) { + this.logger.info('Forcing rebuild: tiapp.xml fullscreen changed since last build'); + this.logger.info(` Was: ${manifest.fullscreen}`); + this.logger.info(` Now: ${this.tiapp.fullscreen}`); + return true; + } - if (this.tiapp['navbar-hidden'] !== manifest.navbarHidden) { - this.logger.info(__('Forcing rebuild: tiapp.xml navbar-hidden changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.navbarHidden)); - this.logger.info(' ' + __('Now: %s', this.tiapp['navbar-hidden'])); - return true; - } + if (this.tiapp['navbar-hidden'] !== manifest.navbarHidden) { + this.logger.info('Forcing rebuild: tiapp.xml navbar-hidden changed since last build'); + this.logger.info(` Was: ${manifest.navbarHidden}`); + this.logger.info(` Now: ${this.tiapp['navbar-hidden']}`); + return true; + } - if (this.minSDK !== manifest.minSDK) { - this.logger.info(__('Forcing rebuild: Android minimum SDK changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.minSDK)); - this.logger.info(' ' + __('Now: %s', this.minSDK)); - return true; - } + if (this.minSDK !== manifest.minSDK) { + this.logger.info('Forcing rebuild: Android minimum SDK changed since last build'); + this.logger.info(` Was: ${manifest.minSDK}`); + this.logger.info(` Now: ${this.minSDK}`); + return true; + } - if (this.targetSDK !== manifest.targetSDK) { - this.logger.info(__('Forcing rebuild: Android target SDK changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.targetSDK)); - this.logger.info(' ' + __('Now: %s', this.targetSDK)); - return true; - } + if (this.targetSDK !== manifest.targetSDK) { + this.logger.info('Forcing rebuild: Android target SDK changed since last build'); + this.logger.info(` Was: ${manifest.targetSDK}`); + this.logger.info(` Now: ${this.targetSDK}`); + return true; + } - if (this.propertiesHash !== manifest.propertiesHash) { - this.logger.info(__('Forcing rebuild: tiapp.xml properties changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.propertiesHash)); - this.logger.info(' ' + __('Now: %s', this.propertiesHash)); - return true; - } + if (this.propertiesHash !== manifest.propertiesHash) { + this.logger.info('Forcing rebuild: tiapp.xml properties changed since last build'); + this.logger.info(` Was: ${manifest.propertiesHash}`); + this.logger.info(` Now: ${this.propertiesHash}`); + return true; + } - if (this.activitiesHash !== manifest.activitiesHash) { - this.logger.info(__('Forcing rebuild: Android activities in tiapp.xml changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.activitiesHash)); - this.logger.info(' ' + __('Now: %s', this.activitiesHash)); - return true; - } + if (this.activitiesHash !== manifest.activitiesHash) { + this.logger.info('Forcing rebuild: Android activities in tiapp.xml changed since last build'); + this.logger.info(` Was: ${manifest.activitiesHash}`); + this.logger.info(` Now: ${this.activitiesHash}`); + return true; + } - if (this.servicesHash !== manifest.servicesHash) { - this.logger.info(__('Forcing rebuild: Android services in tiapp.xml SDK changed since last build')); - this.logger.info(' ' + __('Was: %s', manifest.servicesHash)); - this.logger.info(' ' + __('Now: %s', this.servicesHash)); - return true; - } + if (this.servicesHash !== manifest.servicesHash) { + this.logger.info('Forcing rebuild: Android services in tiapp.xml SDK changed since last build'); + this.logger.info(` Was: ${manifest.servicesHash}`); + this.logger.info(` Now: ${this.servicesHash}`); + return true; + } - return false; -}; + return false; + } -AndroidBuilder.prototype.checkIfNeedToRecompile = async function checkIfNeedToRecompile() { - // Determine if we should do a "clean" build. - this.forceRebuild = this.checkIfShouldForceRebuild(); - if (this.forceRebuild) { - // On Windows, stop gradle daemon to make it release its file locks so that they can be deleted. - if (process.platform === 'win32') { - try { - const gradlew = new GradleWrapper(this.buildDir); - gradlew.logger = this.logger; - if (await gradlew.hasWrapperFiles()) { - await gradlew.stopDaemon(); + async checkIfNeedToRecompile() { + // Determine if we should do a "clean" build. + this.forceRebuild = this.checkIfShouldForceRebuild(); + if (this.forceRebuild) { + // On Windows, stop gradle daemon to make it release its file locks so that they can be deleted. + if (process.platform === 'win32') { + try { + const gradlew = new GradleWrapper(this.buildDir); + gradlew.logger = this.logger; + if (await gradlew.hasWrapperFiles()) { + await gradlew.stopDaemon(); + } + } catch (err) { + this.logger.error(`Failed to stop gradle daemon. Reason:\n${err}`); } - } catch (err) { - this.logger.error(`Failed to stop gradle daemon. Reason:\n${err}`); } + + // Delete all files under the "./build/android" directory. + await fs.emptyDir(this.buildDir); + this.unmarkBuildDirFiles(this.buildDir); } - // Delete all files under the "./build/android" directory. - await fs.emptyDir(this.buildDir); - this.unmarkBuildDirFiles(this.buildDir); + // Delete the "build-manifest.json" in case the build fails and errors out. + // If the build succeeds, then we'll re-create this file which will later allow an incremental build. + // But if the file is missing, then the next build will attempt a clean build. + if (await fs.exists(this.buildManifestFile)) { + await fs.unlink(this.buildManifestFile); + } } - // Delete the "build-manifest.json" in case the build fails and errors out. - // If the build succeeds, then we'll re-create this file which will later allow an incremental build. - // But if the file is missing, then the next build will attempt a clean build. - if (await fs.exists(this.buildManifestFile)) { - await fs.unlink(this.buildManifestFile); - } -}; + async generateLibProjectForModule(moduleInfo) { + // Validate arguments. + if (!moduleInfo || !moduleInfo.native) { + return; + } -AndroidBuilder.prototype.generateLibProjectForModule = async function generateLibProjectForModule(moduleInfo) { - // Validate arguments. - if (!moduleInfo || !moduleInfo.native) { - return; - } + // Create the library project subdirectory, if it doesn't already exist. + const projectDirName = 'lib.' + moduleInfo.manifest.moduleid; + const projectDirPath = path.join(this.buildDir, projectDirName); + this.logger.info(`Generating gradle project: ${projectDirName.cyan}`); + await fs.ensureDir(projectDirPath); - // Create the library project subdirectory, if it doesn't already exist. - const projectDirName = 'lib.' + moduleInfo.manifest.moduleid; - const projectDirPath = path.join(this.buildDir, projectDirName); - this.logger.info(__('Generating gradle project: %s', projectDirName.cyan)); - await fs.ensureDir(projectDirPath); + // Add the library project's name to our array. + // This array of names will later be added to the app project's "build.gradle" file as library dependencies. + if (this.libProjectNames.includes(projectDirName) === false) { + this.libProjectNames.push(projectDirName); + } - // Add the library project's name to our array. - // This array of names will later be added to the app project's "build.gradle" file as library dependencies. - if (this.libProjectNames.includes(projectDirName) === false) { - this.libProjectNames.push(projectDirName); - } + // Create the library project's "libs" directory where JAR/AAR libraries go. + // Delete the directory's files if it already exists. + const projectLibsDirPath = path.join(projectDirPath, 'libs'); + await fs.emptyDir(projectLibsDirPath); - // Create the library project's "libs" directory where JAR/AAR libraries go. - // Delete the directory's files if it already exists. - const projectLibsDirPath = path.join(projectDirPath, 'libs'); - await fs.emptyDir(projectLibsDirPath); - - // Copy module's main JAR to project's "libs" directory. - const sourceJarFileName = moduleInfo.manifest.name + '.jar'; - const sourceJarFilePath = path.join(moduleInfo.modulePath, sourceJarFileName); - afs.copyFileSync(sourceJarFilePath, path.join(projectLibsDirPath, sourceJarFileName), { - logger: this.logger.debug - }); - - // Copy module's dependency JAR/AAR files to project's "libs" directory. - const sourceLibDirPath = path.join(moduleInfo.modulePath, 'lib'); - if (await fs.exists(sourceLibDirPath)) { - afs.copyDirSyncRecursive(sourceLibDirPath, projectLibsDirPath, { - logger: this.logger.debug, - preserve: true + // Copy module's main JAR to project's "libs" directory. + const sourceJarFileName = moduleInfo.manifest.name + '.jar'; + const sourceJarFilePath = path.join(moduleInfo.modulePath, sourceJarFileName); + afs.copyFileSync(sourceJarFilePath, path.join(projectLibsDirPath, sourceJarFileName), { + logger: this.logger.debug }); - } - // Delete the library project's "./src/main" subdirectory. - // Note: Do not delete project's "./build" directory. It contains incremental build info. - const projectSrcMainDirPath = path.join(projectDirPath, 'src', 'main'); - await fs.emptyDir(projectSrcMainDirPath); - - // Copy module's APK "assets" files, "res" files, and other native Android specific files. - // Do this by copying its "platform/android" directory tree to library project's "src/main" directory. - const sourcePlaformAndroidDirPath = path.join(moduleInfo.modulePath, 'platform', 'android'); - if (await fs.exists(sourcePlaformAndroidDirPath)) { - afs.copyDirSyncRecursive(sourcePlaformAndroidDirPath, projectSrcMainDirPath, { - logger: this.logger.debug, - preserve: false - }); - } + // Copy module's dependency JAR/AAR files to project's "libs" directory. + const sourceLibDirPath = path.join(moduleInfo.modulePath, 'lib'); + if (await fs.exists(sourceLibDirPath)) { + afs.copyDirSyncRecursive(sourceLibDirPath, projectLibsDirPath, { + logger: this.logger.debug, + preserve: true + }); + } + + // Delete the library project's "./src/main" subdirectory. + // Note: Do not delete project's "./build" directory. It contains incremental build info. + const projectSrcMainDirPath = path.join(projectDirPath, 'src', 'main'); + await fs.emptyDir(projectSrcMainDirPath); + + // Copy module's APK "assets" files, "res" files, and other native Android specific files. + // Do this by copying its "platform/android" directory tree to library project's "src/main" directory. + const sourcePlaformAndroidDirPath = path.join(moduleInfo.modulePath, 'platform', 'android'); + if (await fs.exists(sourcePlaformAndroidDirPath)) { + afs.copyDirSyncRecursive(sourcePlaformAndroidDirPath, projectSrcMainDirPath, { + logger: this.logger.debug, + preserve: false + }); + } + + // Copy module's C/C++ "*.so" libraries to project's "jniLibs" directory. + // Note: Must be done last since above code deletes the "src/main" directory. + const sourceJniLibsDirPath = path.join(moduleInfo.modulePath, 'libs'); + if (await fs.exists(sourceJniLibsDirPath)) { + const projectJniLibsDirPath = path.join(projectDirPath, 'src', 'main', 'jniLibs'); + afs.copyDirSyncRecursive(sourceJniLibsDirPath, projectJniLibsDirPath, { + logger: this.logger.debug, + preserve: false + }); + } + + // If module has "AndroidManifest.xml" file under its "./platform/android" directory, + // then copy it to library project's "debug" and "release" subdirectories. + // This makes them extend main "AndroidManifest.xml" under "./src/main" which is taken from "timodule.xml". + const sourceManifestFilePath = path.join(sourcePlaformAndroidDirPath, 'AndroidManifest.xml'); + if (await fs.exists(sourceManifestFilePath)) { + // Create the "debug" and "release" subdirectories. + const debugDirPath = path.join(projectDirPath, 'src', 'debug'); + const releaseDirPath = path.join(projectDirPath, 'src', 'release'); + await fs.ensureDir(debugDirPath); + await fs.ensureDir(releaseDirPath); + + // Load "AndroidManifest.xml", replace ${tiapp.properties['key']} variables, and save to above directories. + const manifest = await AndroidManifest.fromFilePath(sourceManifestFilePath); + manifest.setPackageName(moduleInfo.manifest.moduleid); + manifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); + await manifest.writeToFilePath(path.join(debugDirPath, 'AndroidManifest.xml')); + await manifest.writeToFilePath(path.join(releaseDirPath, 'AndroidManifest.xml')); + } + + // Create main "AndroidManifest.xml" file under library project's "./src/main". + // If manifest settings exist in "timodule.xml", then merge it into main manifest. + const mainManifest = AndroidManifest.fromXmlString(''); + const tiModuleXmlFilePath = path.join(moduleInfo.modulePath, 'timodule.xml'); + try { + if (await fs.exists(tiModuleXmlFilePath)) { + const tiModuleInfo = new tiappxml(tiModuleXmlFilePath); + if (tiModuleInfo && tiModuleInfo.android && tiModuleInfo.android.manifest) { + const tiModuleManifest = AndroidManifest.fromXmlString(tiModuleInfo.android.manifest); + tiModuleManifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); + mainManifest.copyFromAndroidManifest(tiModuleManifest); + } + } + } catch (ex) { + this.logger.error(`Unable to load Android content from: ${tiModuleXmlFilePath}`); + throw ex; + } + mainManifest.setPackageName(moduleInfo.manifest.moduleid); + await mainManifest.writeToFilePath(path.join(projectSrcMainDirPath, 'AndroidManifest.xml')); - // Copy module's C/C++ "*.so" libraries to project's "jniLibs" directory. - // Note: Must be done last since above code deletes the "src/main" directory. - const sourceJniLibsDirPath = path.join(moduleInfo.modulePath, 'libs'); - if (await fs.exists(sourceJniLibsDirPath)) { - const projectJniLibsDirPath = path.join(projectDirPath, 'src', 'main', 'jniLibs'); - afs.copyDirSyncRecursive(sourceJniLibsDirPath, projectJniLibsDirPath, { - logger: this.logger.debug, - preserve: false + // Generate a "build.gradle" file for this project from the SDK's "lib.build.gradle" EJS template. + // Note: Google does not support setting "maxSdkVersion" via gradle script. + let buildGradleContent = await fs.readFile(path.join(this.templatesDir, 'lib.build.gradle')); + buildGradleContent = ejs.render(buildGradleContent.toString(), { + compileSdkVersion: this.compileSdkVersion, + minSdkVersion: this.minSDK, + targetSdkVersion: this.targetSDK }); + await fs.writeFile(path.join(projectDirPath, 'build.gradle'), buildGradleContent); } - // If module has "AndroidManifest.xml" file under its "./platform/android" directory, - // then copy it to library project's "debug" and "release" subdirectories. - // This makes them extend main "AndroidManifest.xml" under "./src/main" which is taken from "timodule.xml". - const sourceManifestFilePath = path.join(sourcePlaformAndroidDirPath, 'AndroidManifest.xml'); - if (await fs.exists(sourceManifestFilePath)) { - // Create the "debug" and "release" subdirectories. - const debugDirPath = path.join(projectDirPath, 'src', 'debug'); - const releaseDirPath = path.join(projectDirPath, 'src', 'release'); - await fs.ensureDir(debugDirPath); - await fs.ensureDir(releaseDirPath); - - // Load "AndroidManifest.xml", replace ${tiapp.properties['key']} variables, and save to above directories. - const manifest = await AndroidManifest.fromFilePath(sourceManifestFilePath); - manifest.setPackageName(moduleInfo.manifest.moduleid); - manifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); - await manifest.writeToFilePath(path.join(debugDirPath, 'AndroidManifest.xml')); - await manifest.writeToFilePath(path.join(releaseDirPath, 'AndroidManifest.xml')); - } + async processLibraries() { + this.logger.info('Processing libraries'); - // Create main "AndroidManifest.xml" file under library project's "./src/main". - // If manifest settings exist in "timodule.xml", then merge it into main manifest. - const mainManifest = AndroidManifest.fromXmlString(''); - const tiModuleXmlFilePath = path.join(moduleInfo.modulePath, 'timodule.xml'); - try { - if (await fs.exists(tiModuleXmlFilePath)) { - const tiModuleInfo = new tiappxml(tiModuleXmlFilePath); - if (tiModuleInfo && tiModuleInfo.android && tiModuleInfo.android.manifest) { - const tiModuleManifest = AndroidManifest.fromXmlString(tiModuleInfo.android.manifest); - tiModuleManifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); - mainManifest.copyFromAndroidManifest(tiModuleManifest); - } - } - } catch (ex) { - this.logger.error(`Unable to load Android content from: ${tiModuleXmlFilePath}`); - throw ex; - } - mainManifest.setPackageName(moduleInfo.manifest.moduleid); - await mainManifest.writeToFilePath(path.join(projectSrcMainDirPath, 'AndroidManifest.xml')); - - // Generate a "build.gradle" file for this project from the SDK's "lib.build.gradle" EJS template. - // Note: Google does not support setting "maxSdkVersion" via gradle script. - let buildGradleContent = await fs.readFile(path.join(this.templatesDir, 'lib.build.gradle')); - buildGradleContent = ejs.render(buildGradleContent.toString(), { - compileSdkVersion: this.compileSdkVersion, - minSdkVersion: this.minSDK, - targetSdkVersion: this.targetSDK - }); - await fs.writeFile(path.join(projectDirPath, 'build.gradle'), buildGradleContent); -}; - -AndroidBuilder.prototype.processLibraries = async function processLibraries() { - this.logger.info(__('Processing libraries')); - - // Clear last fetched library information. - this.libProjectNames = []; - this.libDependencyStrings = []; - this.mavenRepositoryUrls = []; - - // Make sure "modules" property is set to a valid value. - if (!this.modules) { - this.modules = []; - } + // Clear last fetched library information. + this.libProjectNames = []; + this.libDependencyStrings = []; + this.mavenRepositoryUrls = []; - // Add a reference to the core Titanium library. - const tiMavenRepoUrl = 'file://' + path.join(this.platformPath, 'm2repository').replace(/\\/g, '/'); - this.mavenRepositoryUrls.push(encodeURI(tiMavenRepoUrl)); - this.libDependencyStrings.push(`org.appcelerator:titanium:${this.titaniumSdkVersion}`); - - // Process all Titanium modules referenced by the Titanium project. - for (const nextModule of this.modules) { - // Skip non-native modules. - if (!nextModule.native) { - continue; - } - - // Check if the module has a maven repository directory. - // If it does, then we can leverage gradle/maven's dependency management system. - let dependencyString = null; - const repositoryDirPath = path.join(nextModule.modulePath, 'm2repository'); - if (await fs.exists(repositoryDirPath)) { - const moduleId = nextModule.manifest.moduleid; - let index = moduleId.lastIndexOf('.'); - if ((index !== 0) && ((index + 1) < moduleId.length)) { - if (index > 0) { - dependencyString = moduleId.substring(0, index); - dependencyString += ':'; - dependencyString += moduleId.substring(index + 1); - } else { - dependencyString = moduleId; + // Make sure "modules" property is set to a valid value. + if (!this.modules) { + this.modules = []; + } + + // Add a reference to the core Titanium library. + const tiMavenRepoUrl = 'file://' + path.join(this.platformPath, 'm2repository').replace(/\\/g, '/'); + this.mavenRepositoryUrls.push(encodeURI(tiMavenRepoUrl)); + this.libDependencyStrings.push(`org.appcelerator:titanium:${this.titaniumSdkVersion}`); + + // Process all Titanium modules referenced by the Titanium project. + for (const nextModule of this.modules) { + // Skip non-native modules. + if (!nextModule.native) { + continue; + } + + // Check if the module has a maven repository directory. + // If it does, then we can leverage gradle/maven's dependency management system. + let dependencyString = null; + const repositoryDirPath = path.join(nextModule.modulePath, 'm2repository'); + if (await fs.exists(repositoryDirPath)) { + const moduleId = nextModule.manifest.moduleid; + let index = moduleId.lastIndexOf('.'); + if ((index !== 0) && ((index + 1) < moduleId.length)) { + if (index > 0) { + dependencyString = moduleId.substring(0, index); + dependencyString += ':'; + dependencyString += moduleId.substring(index + 1); + } else { + dependencyString = moduleId; + dependencyString += ':'; + dependencyString += moduleId; + } dependencyString += ':'; - dependencyString += moduleId; + dependencyString += nextModule.manifest.version; } - dependencyString += ':'; - dependencyString += nextModule.manifest.version; } - } - // Determine how to reference the module in gradle. - if (repositoryDirPath && dependencyString) { - // Referenced module has a maven repository. - // This supports dependency management to avoid library version conflicts. - const url = 'file://' + repositoryDirPath.replace(/\\/g, '/'); - this.mavenRepositoryUrls.push(encodeURI(url)); - this.libDependencyStrings.push(dependencyString); - } else { - // Module directory only contains JARs/AARs. (This is our legacy module distribution.) - // We must create a gradle library project and copy the module's files to it. - await this.generateLibProjectForModule(nextModule); + // Determine how to reference the module in gradle. + if (repositoryDirPath && dependencyString) { + // Referenced module has a maven repository. + // This supports dependency management to avoid library version conflicts. + const url = 'file://' + repositoryDirPath.replace(/\\/g, '/'); + this.mavenRepositoryUrls.push(encodeURI(url)); + this.libDependencyStrings.push(dependencyString); + } else { + // Module directory only contains JARs/AARs. (This is our legacy module distribution.) + // We must create a gradle library project and copy the module's files to it. + await this.generateLibProjectForModule(nextModule); + } } } -}; - -AndroidBuilder.prototype.generateRootProjectFiles = async function generateRootProjectFiles() { - this.logger.info(__('Generating root project files')); - - // Copy our SDK's gradle files to the build directory. (Includes "gradlew" scripts and "gradle" directory tree.) - // The below install method will also generate a "gradle.properties" file. - const gradlew = new GradleWrapper(this.buildDir); - gradlew.logger = this.logger; - await gradlew.installTemplate(path.join(this.platformPath, 'templates', 'gradle')); - - // Create a "gradle.properties" file. Will add network proxy settings if needed. - // Note: Enable Jetifier to replace all Google Support library references with AndroidX in all pre-built JARs. - // This is needed because using both libraries will cause class name collisions, causing a build failure. - const gradleProperties = await gradlew.fetchDefaultGradleProperties(); - gradleProperties.push({ key: 'android.useAndroidX', value: 'true' }); - gradleProperties.push({ key: 'android.enableJetifier', value: 'true' }); - gradleProperties.push({ key: 'android.suppressUnsupportedCompileSdk', value: '33' }); - gradleProperties.push({ key: 'org.gradle.jvmargs', value: `-Xmx${this.javacMaxMemory}` }); - await gradlew.writeGradlePropertiesFile(gradleProperties); - - // Copy optional "gradle.properties" file contents from Titanium project to the above generated file. - // These properties must be copied to the end of the file so that they can override Titanium's default properties. - const customGradlePropertiesFilePath = path.join(this.projectDir, 'platform', 'android', 'gradle.properties'); - if (await fs.exists(customGradlePropertiesFilePath)) { - const targetGradlePropertiesFilePath = path.join(this.buildDir, 'gradle.properties'); - const fileContent = await fs.readFile(customGradlePropertiesFilePath); - await fs.appendFile(targetGradlePropertiesFilePath, - '\n\n' - + '# The below was copied from project file: ./platform/android/gradle.properties\n' - + fileContent.toString() + '\n'); - } - // Create a "local.properties" file providing a path to the Android SDK directory. - await gradlew.writeLocalPropertiesFile(this.androidInfo.sdk.path); - - // Copy our root "build.gradle" template script to the root build directory. - await fs.copyFile( - path.join(this.templatesDir, 'root.build.gradle'), - path.join(this.buildDir, 'build.gradle')); - - // Copy our Titanium template's gradle constants file. - // This provides the Google library versions we use and defines our custom "AndroidManifest.xml" placeholders. - const tiConstantsGradleFileName = 'ti.constants.gradle'; - await fs.copyFile( - path.join(this.templatesDir, tiConstantsGradleFileName), - path.join(this.buildDir, tiConstantsGradleFileName)); - - // Create a "settings.gradle" file providing all of the gradle projects configured. - // By default, these project names must match the subdirectory names. - const fileLines = [ - `rootProject.name = '${this.tiapp.name.replace(/'/g, "\\'")}'`, // eslint-disable-line quotes - "include ':app'" // eslint-disable-line quotes - ]; - if (this.libProjectNames) { - for (const projectName of this.libProjectNames) { - fileLines.push(`include ':${projectName}'`); + async generateRootProjectFiles() { + this.logger.info('Generating root project files'); + + // Copy our SDK's gradle files to the build directory. (Includes "gradlew" scripts and "gradle" directory tree.) + // The below install method will also generate a "gradle.properties" file. + const gradlew = new GradleWrapper(this.buildDir); + gradlew.logger = this.logger; + await gradlew.installTemplate(path.join(this.platformPath, 'templates', 'gradle')); + + // Create a "gradle.properties" file. Will add network proxy settings if needed. + // Note: Enable Jetifier to replace all Google Support library references with AndroidX in all pre-built JARs. + // This is needed because using both libraries will cause class name collisions, causing a build failure. + const gradleProperties = await gradlew.fetchDefaultGradleProperties(); + gradleProperties.push({ key: 'android.useAndroidX', value: 'true' }); + gradleProperties.push({ key: 'android.enableJetifier', value: 'true' }); + gradleProperties.push({ key: 'android.suppressUnsupportedCompileSdk', value: '33' }); + gradleProperties.push({ key: 'org.gradle.jvmargs', value: `-Xmx${this.javacMaxMemory}` }); + await gradlew.writeGradlePropertiesFile(gradleProperties); + + // Copy optional "gradle.properties" file contents from Titanium project to the above generated file. + // These properties must be copied to the end of the file so that they can override Titanium's default properties. + const customGradlePropertiesFilePath = path.join(this.projectDir, 'platform', 'android', 'gradle.properties'); + if (await fs.exists(customGradlePropertiesFilePath)) { + const targetGradlePropertiesFilePath = path.join(this.buildDir, 'gradle.properties'); + const fileContent = await fs.readFile(customGradlePropertiesFilePath); + await fs.appendFile(targetGradlePropertiesFilePath, + '\n\n' + + '# The below was copied from project file: ./platform/android/gradle.properties\n' + + fileContent.toString() + '\n'); + } + + // Create a "local.properties" file providing a path to the Android SDK directory. + await gradlew.writeLocalPropertiesFile(this.androidInfo.sdk.path); + + // Copy our root "build.gradle" template script to the root build directory. + await fs.copyFile( + path.join(this.templatesDir, 'root.build.gradle'), + path.join(this.buildDir, 'build.gradle')); + + // Copy our Titanium template's gradle constants file. + // This provides the Google library versions we use and defines our custom "AndroidManifest.xml" placeholders. + const tiConstantsGradleFileName = 'ti.constants.gradle'; + await fs.copyFile( + path.join(this.templatesDir, tiConstantsGradleFileName), + path.join(this.buildDir, tiConstantsGradleFileName)); + + // Create a "settings.gradle" file providing all of the gradle projects configured. + // By default, these project names must match the subdirectory names. + const fileLines = [ + `rootProject.name = '${this.tiapp.name.replace(/'/g, "\\'")}'`, // eslint-disable-line quotes + "include ':app'" // eslint-disable-line quotes + ]; + if (this.libProjectNames) { + for (const projectName of this.libProjectNames) { + fileLines.push(`include ':${projectName}'`); + } + } + await fs.writeFile(path.join(this.buildDir, 'settings.gradle'), fileLines.join('\n') + '\n'); + } + + async generateAppProject() { + this.logger.info(`Generating gradle project: ${'app'.cyan}`); + + // Create the "app" project directory and its "./src/main" subdirectory tree. + // Delete all files under its "./src/main" subdirectory if it already exists. + // Note: Do not delete the "./build" subdirectory. It contains incremental build info. + await fs.emptyDir(this.buildAppMainDir); + await fs.ensureDir(this.buildAppMainAssetsResourcesDir); + + // Make sure Titanium's "assets" directory exists. (This is not an APK "assets" directory.) + // We output transpiled/polyfilled JS files here via copyResources() method. + // This is a temporary output path for transpiled JS files + // If we encrypt, we expect the input files to be here and encrypted copies placed in this.buildAppMainAssetsResourcesDir + // IF we do not encrypt we copy from this path to this.buildAppMainAssetsResourcesDir + // Note: Do NOT delete this folder. We do our own incremental build handling on it. + await fs.ensureDir(this.buildAssetsDir); + + // Create a "libs" folder under root build folder. + // NOTE: This is NOT a standard location to put JAR/AAR files, but node module "appc-cli-titanium" + // will put libraries here depending on "tiapp.xml" property settings. + const rootLibsDirPath = path.join(this.buildDir, 'libs'); + await fs.emptyDir(rootLibsDirPath); + await fs.ensureDir(rootLibsDirPath); + + // Create a "deploy.json" file if debugging/profiling is enabled. + const deployJsonFile = path.join(this.buildAppMainAssetsDir, 'deploy.json'); + const deployData = { + debuggerEnabled: !!this.debugPort, + debuggerPort: this.debugPort || -1, + profilerEnabled: !!this.profilerPort, + profilerPort: this.profilerPort || -1 + }; + if (await fs.exists(deployJsonFile)) { + await fs.unlink(deployJsonFile); + } + if (deployData.debuggerEnabled || deployData.profilerEnabled) { + await fs.writeFile(deployJsonFile, JSON.stringify(deployData)); } - } - await fs.writeFile(path.join(this.buildDir, 'settings.gradle'), fileLines.join('\n') + '\n'); -}; - -AndroidBuilder.prototype.generateAppProject = async function generateAppProject() { - this.logger.info(__('Generating gradle project: %s', 'app'.cyan)); - - // Create the "app" project directory and its "./src/main" subdirectory tree. - // Delete all files under its "./src/main" subdirectory if it already exists. - // Note: Do not delete the "./build" subdirectory. It contains incremental build info. - await fs.emptyDir(this.buildAppMainDir); - await fs.ensureDir(this.buildAppMainAssetsResourcesDir); - - // Make sure Titanium's "assets" directory exists. (This is not an APK "assets" directory.) - // We output transpiled/polyfilled JS files here via copyResources() method. - // This is a temporary output path for transpiled JS files - // If we encrypt, we expect the input files to be here and encrypted copies placed in this.buildAppMainAssetsResourcesDir - // IF we do not encrypt we copy from this path to this.buildAppMainAssetsResourcesDir - // Note: Do NOT delete this folder. We do our own incremental build handling on it. - await fs.ensureDir(this.buildAssetsDir); - - // Create a "libs" folder under root build folder. - // NOTE: This is NOT a standard location to put JAR/AAR files, but node module "appc-cli-titanium" - // will put libraries here depending on "tiapp.xml" property settings. - const rootLibsDirPath = path.join(this.buildDir, 'libs'); - await fs.emptyDir(rootLibsDirPath); - await fs.ensureDir(rootLibsDirPath); - - // Create a "deploy.json" file if debugging/profiling is enabled. - const deployJsonFile = path.join(this.buildAppMainAssetsDir, 'deploy.json'); - const deployData = { - debuggerEnabled: !!this.debugPort, - debuggerPort: this.debugPort || -1, - profilerEnabled: !!this.profilerPort, - profilerPort: this.profilerPort || -1 - }; - if (await fs.exists(deployJsonFile)) { - await fs.unlink(deployJsonFile); - } - if (deployData.debuggerEnabled || deployData.profilerEnabled) { - await fs.writeFile(deployJsonFile, JSON.stringify(deployData)); - } - // Copy files from Titanium project's "Resources" directory to the build directory. - await fs.ensureDir(this.buildAppMainResDrawableDir); - await this.copyResources(); + // Copy files from Titanium project's "Resources" directory to the build directory. + await fs.ensureDir(this.buildAppMainResDrawableDir); + await this.copyResources(); - // We can do the following in parallel. - await Promise.all([ - // Generate an "index.json" file referencing every JavaScript file bundled into the app. - // This is used by the require() function to find the required-in JS files. - this.generateRequireIndex(), + // We can do the following in parallel. + await Promise.all([ + // Generate an "index.json" file referencing every JavaScript file bundled into the app. + // This is used by the require() function to find the required-in JS files. + this.generateRequireIndex(), - // Generate "*.java" source files for application. - this.generateJavaFiles(), + // Generate "*.java" source files for application. + this.generateJavaFiles(), - // Generate a "res/values" XML file from a Titanium i18n file, if it exists. - this.generateI18N(), + // Generate a "res/values" XML file from a Titanium i18n file, if it exists. + this.generateI18N(), - // Generate a "res/values" styles XML file if a custom theme was assigned in app's "AndroidManifest.xml". - this.generateTheme(), + // Generate a "res/values" styles XML file if a custom theme was assigned in app's "AndroidManifest.xml". + this.generateTheme(), - // Generate "semantic.colors.xml" in "res/values" and "res/values-night" - this.generateSemanticColors() - ]); + // Generate "semantic.colors.xml" in "res/values" and "res/values-night" + this.generateSemanticColors() + ]); - // Generate an "AndroidManifest.xml" for the app and copy in any custom manifest settings from "tiapp.xml". - await this.generateAndroidManifest(); + // Generate an "AndroidManifest.xml" for the app and copy in any custom manifest settings from "tiapp.xml". + await this.generateAndroidManifest(); - // Generate a "java-sources.txt" file containing paths to all Java source files under our "app" project. - // Note: Our "appc-cli-titanium" node module uses this to make source code adjustments based on build settings. - const javaFilePaths = []; - function fetchJavaFilePathsFrom(directoryPath) { - for (const fileName of fs.readdirSync(directoryPath)) { - const filePath = path.join(directoryPath, fileName); - if (fs.statSync(filePath).isDirectory()) { - fetchJavaFilePathsFrom(filePath); - } else if (fileName.toLowerCase().endsWith('.java')) { - javaFilePaths.push(filePath); + // Generate a "java-sources.txt" file containing paths to all Java source files under our "app" project. + // Note: Our "appc-cli-titanium" node module uses this to make source code adjustments based on build settings. + const javaFilePaths = []; + function fetchJavaFilePathsFrom(directoryPath) { + for (const fileName of fs.readdirSync(directoryPath)) { + const filePath = path.join(directoryPath, fileName); + if (fs.statSync(filePath).isDirectory()) { + fetchJavaFilePathsFrom(filePath); + } else if (fileName.toLowerCase().endsWith('.java')) { + javaFilePaths.push(filePath); + } } } - } - fetchJavaFilePathsFrom(path.join(this.buildAppMainDir, 'java')); - await fs.writeFile( - path.join(this.buildDir, 'java-sources.txt'), - '"' + javaFilePaths.join('"\n"').replace(/\\/g, '/') + '"'); // Paths must be double quoted and escaped. - - // Emit our legacy "aapt" hook event so that old plugins can copy in additional resources to the project. - // Note: Our "appc-cli-titanium" node module needs this when property "appc-sourcecode-encryption-policy" is set. - await new Promise((resolve) => { - const aaptHook = this.cli.createHook('build.android.aapt', this, function (exe, args, opts, done) { - done(); + fetchJavaFilePathsFrom(path.join(this.buildAppMainDir, 'java')); + await fs.writeFile( + path.join(this.buildDir, 'java-sources.txt'), + '"' + javaFilePaths.join('"\n"').replace(/\\/g, '/') + '"'); // Paths must be double quoted and escaped. + + // Emit our legacy "aapt" hook event so that old plugins can copy in additional resources to the project. + // Note: Our "appc-cli-titanium" node module needs this when property "appc-sourcecode-encryption-policy" is set. + await new Promise((resolve) => { + const aaptHook = this.cli.createHook('build.android.aapt', this, function (exe, args, opts, done) { + done(); + }); + aaptHook('', [], {}, resolve); }); - aaptHook('', [], {}, resolve); - }); - - // Emit a "javac" hook event so that plugins can copy in addition Java files or source code changes. - // Note: Our "appc-cli-titanium" node module requires this event and the "-bootclasspath" argument too. - await new Promise((resolve) => { - const javacHook = this.cli.createHook('build.android.javac', this, (exe, args, opts, done) => { - done(); + + // Emit a "javac" hook event so that plugins can copy in addition Java files or source code changes. + // Note: Our "appc-cli-titanium" node module requires this event and the "-bootclasspath" argument too. + await new Promise((resolve) => { + const javacHook = this.cli.createHook('build.android.javac', this, (exe, args, opts, done) => { + done(); + }); + javacHook('', [ '-bootclasspath', '' ], {}, resolve); }); - javacHook('', [ '-bootclasspath', '' ], {}, resolve); - }); - - // Emit a "dexer" hook event to acquire additional JAR/AAR files from plugins. - // Note: Our "appc-cli-titanium" node module needs "args" array to have a 2nd element. - await new Promise((resolve) => { - const dexerHook = this.cli.createHook('build.android.dexer', this, (exe, args, opts, done) => { - for (const nextArg of args) { - try { - if (fs.existsSync(nextArg) && fs.statSync(nextArg).isFile()) { - this.libFilePaths.push(nextArg); + + // Emit a "dexer" hook event to acquire additional JAR/AAR files from plugins. + // Note: Our "appc-cli-titanium" node module needs "args" array to have a 2nd element. + await new Promise((resolve) => { + const dexerHook = this.cli.createHook('build.android.dexer', this, (exe, args, opts, done) => { + for (const nextArg of args) { + try { + if (fs.existsSync(nextArg) && fs.statSync(nextArg).isFile()) { + this.libFilePaths.push(nextArg); + } + } catch (err) { + // Ignore. } - } catch (err) { - // Ignore. } - } - done(); + done(); + }); + dexerHook('', [ '', '' ], {}, resolve); }); - dexerHook('', [ '', '' ], {}, resolve); - }); - // Acquire the app's version integer code and name to be written to "build.gradle" later. - let versionCode = '1'; - let versionName = this.tiapp.version ? this.tiapp.version : '1'; - if (this.customAndroidManifest) { - const versionInfo = this.customAndroidManifest.getAppVersionInfo(); - if (versionInfo) { - if (versionInfo.versionCode) { - versionCode = versionInfo.versionCode; - } - if (versionInfo.versionName) { - versionName = versionInfo.versionName; + // Acquire the app's version integer code and name to be written to "build.gradle" later. + let versionCode = '1'; + let versionName = this.tiapp.version ? this.tiapp.version : '1'; + if (this.customAndroidManifest) { + const versionInfo = this.customAndroidManifest.getAppVersionInfo(); + if (versionInfo) { + if (versionInfo.versionCode) { + versionCode = versionInfo.versionCode; + } + if (versionInfo.versionName) { + versionName = versionInfo.versionName; + } } } - } - // Generate a "build.gradle" file for this project from the SDK's "app.build.gradle" EJS template. - // Note: Google does not support setting "maxSdkVersion" via gradle script. - let buildGradleContent = await fs.readFile(path.join(this.templatesDir, 'app.build.gradle')); - buildGradleContent = ejs.render(buildGradleContent.toString(), { - applicationId: this.appid, - compileSdkVersion: this.compileSdkVersion, - minSdkVersion: this.minSDK, - targetSdkVersion: this.targetSDK, - versionCode: versionCode, - versionName: versionName, - libFilePaths: this.libFilePaths, - libProjectNames: this.libProjectNames, - libDependencyStrings: this.libDependencyStrings, - mavenRepositoryUrls: this.mavenRepositoryUrls, - ndkAbiArray: this.abis, - proguardFilePaths: this.proguardConfigFile ? [ this.proguardConfigFile ] : null, - tiSdkAndroidDir: this.platformPath - }); - await fs.writeFile(path.join(this.buildAppDir, 'build.gradle'), buildGradleContent); -}; + // Generate a "build.gradle" file for this project from the SDK's "app.build.gradle" EJS template. + // Note: Google does not support setting "maxSdkVersion" via gradle script. + let buildGradleContent = await fs.readFile(path.join(this.templatesDir, 'app.build.gradle')); + buildGradleContent = ejs.render(buildGradleContent.toString(), { + applicationId: this.appid, + compileSdkVersion: this.compileSdkVersion, + minSdkVersion: this.minSDK, + targetSdkVersion: this.targetSDK, + versionCode: versionCode, + versionName: versionName, + libFilePaths: this.libFilePaths, + libProjectNames: this.libProjectNames, + libDependencyStrings: this.libDependencyStrings, + mavenRepositoryUrls: this.mavenRepositoryUrls, + ndkAbiArray: this.abis, + proguardFilePaths: this.proguardConfigFile ? [ this.proguardConfigFile ] : null, + tiSdkAndroidDir: this.platformPath + }); + await fs.writeFile(path.join(this.buildAppDir, 'build.gradle'), buildGradleContent); + } + + /** + * Walks the project resources/assets, module resources/assets and gathers up a categorized listing + * of files to process. (css, html, js, images, etc) + * @returns {Promise} + */ + async gatherResources() { + const gather = await import('../../../cli/lib/gather.js'); + const walker = new gather.Walker({ + ignoreDirs: this.ignoreDirs, + ignoreFiles: this.ignoreFiles, + }); -/** - * Walks the project resources/assets, module resources/assets and gathers up a categorized listing - * of files to process. (css, html, js, images, etc) - * @returns {Promise} - */ -AndroidBuilder.prototype.gatherResources = async function gatherResources() { - const gather = require('../../../cli/lib/gather'); - const walker = new gather.Walker({ - ignoreDirs: this.ignoreDirs, - ignoreFiles: this.ignoreFiles, - }); - - this.logger.info(__('Analyzing Resources directory')); - const firstWave = await Promise.all([ - walker.walk(path.join(this.titaniumSdkPath, 'common', 'Resources', 'android'), this.buildAppMainAssetsResourcesDir), - // NOTE: we copy over platform/android as-is without any transform/walk. Should iOS do the same? - // walker.walk(path.join(this.projectDir, 'platform', 'android'), this.buildAppMainDir), - walker.walk(path.join(this.projectDir, 'Resources'), this.buildAppMainAssetsResourcesDir, platformsRegExp), - walker.walk(path.join(this.projectDir, 'Resources', 'android'), this.buildAppMainAssetsResourcesDir), - ]); - let combined = gather.mergeMaps(firstWave); - - // node_modules - this.logger.info(__('Analyzing NPM package files')); - const moduleCopier = require('../../../cli/lib/module-copier'); - const dirSet = await moduleCopier.gather(this.projectDir); - const nodeModuleDirs = Array.from(dirSet); - const secondWave = await Promise.all(nodeModuleDirs.map(async dir => { - // here dir is the absolute path to the directory - // That means we need to construct the relative path to append to this.projectDir and this.xcodeAppDir - const relativePath = dir.substring(this.projectDir.length + 1); - return walker.walk(dir, path.join(this.buildAppMainAssetsResourcesDir, relativePath), null, null, relativePath); - })); - // merge the node_modules results on top of the project results... (shouldn't be any conflicts!) - secondWave.unshift(combined); - combined = gather.mergeMaps(secondWave); - - // Fire an event requesting additional "Resources" paths from plugins. (used by hyperloop) - this.logger.info(__('Analyzing plugin-contributed files')); - this.htmlJsFiles = {}; // for hyperloop to mark files it doesn't want processed - const hook = this.cli.createHook('build.android.requestResourcesDirPaths', this, async (paths, done) => { - try { - const newTasks = []; - if (Array.isArray(paths)) { - for (const nextPath of paths) { - if (typeof nextPath !== 'string') { - continue; - } - if (!await fs.exists(nextPath) || !(await fs.stat(nextPath)).isDirectory()) { - continue; + this.logger.info('Analyzing Resources directory'); + const firstWave = await Promise.all([ + walker.walk(path.join(this.titaniumSdkPath, 'common', 'Resources', 'android'), this.buildAppMainAssetsResourcesDir), + // NOTE: we copy over platform/android as-is without any transform/walk. Should iOS do the same? + // walker.walk(path.join(this.projectDir, 'platform', 'android'), this.buildAppMainDir), + walker.walk(path.join(this.projectDir, 'Resources'), this.buildAppMainAssetsResourcesDir, platformsRegExp), + walker.walk(path.join(this.projectDir, 'Resources', 'android'), this.buildAppMainAssetsResourcesDir), + ]); + let combined = gather.mergeMaps(firstWave); + + // node_modules + this.logger.info('Analyzing NPM package files'); + const moduleCopier = await import('../../../cli/lib/module-copier.js'); + const dirSet = await moduleCopier.gather(this.projectDir); + const nodeModuleDirs = Array.from(dirSet); + const secondWave = await Promise.all(nodeModuleDirs.map(async dir => { + // here dir is the absolute path to the directory + // That means we need to construct the relative path to append to this.projectDir and this.xcodeAppDir + const relativePath = dir.substring(this.projectDir.length + 1); + return walker.walk(dir, path.join(this.buildAppMainAssetsResourcesDir, relativePath), null, null, relativePath); + })); + // merge the node_modules results on top of the project results... (shouldn't be any conflicts!) + secondWave.unshift(combined); + combined = gather.mergeMaps(secondWave); + + // Fire an event requesting additional "Resources" paths from plugins. (used by hyperloop) + this.logger.info('Analyzing plugin-contributed files'); + this.htmlJsFiles = {}; // for hyperloop to mark files it doesn't want processed + const hook = this.cli.createHook('build.android.requestResourcesDirPaths', this, async (paths, done) => { + try { + const newTasks = []; + if (Array.isArray(paths)) { + for (const nextPath of paths) { + if (typeof nextPath !== 'string') { + continue; + } + if (!await fs.exists(nextPath) || !(await fs.stat(nextPath)).isDirectory()) { + continue; + } + newTasks.push( + walker.walk(nextPath, this.buildAppMainAssetsResourcesDir) + ); } - newTasks.push( - walker.walk(nextPath, this.buildAppMainAssetsResourcesDir) - ); } + const results = await Promise.all(newTasks); + done(null, results); + } catch (err) { + return done(err); } - const results = await Promise.all(newTasks); - done(null, results); - } catch (err) { - return done(err); - } - }); - const hookResults = await util.promisify(hook)([]); - // merge the hook results on top of the project/node_modules results... (shouldn't be any conflicts!) - hookResults.unshift(combined); - combined = gather.mergeMaps(hookResults); - - this.logger.info(__('Analyzing module files')); - // detect ambiguous modules - // this.modules.forEach(module => { - // const filename = `${module.id}.js`; - // if (combined.has(filename)) { - // this.logger.error(__('There is a project resource "%s" that conflicts with a native Android module', filename)); - // this.logger.error(__('Please rename the file, then rebuild') + '\n'); - // process.exit(1); - // } - // }); - // do modules in parallel - and for each we need to merge the results together! - const allModulesResults = await Promise.all(this.modules.map(async module => { - const tasks = []; - let assetDest = this.buildAppMainAssetsResourcesDir; - if (!module.native) { - // Copy CommonJS non-asset files - const dest = path.join(this.buildAppMainAssetsResourcesDir, path.basename(module.id)); - // Pass in the relative path prefix we should give because we aren't copying direct to the root here. - // Otherwise index.js in one module "overwrites" index.js in another (because they're at same relative path inside module) - tasks.push(walker.walk(module.modulePath, dest, /^(apidoc|docs|documentation|example|assets)$/, null, module.id)); // TODO Consult some .moduleignore file in the module or something? .npmignore? - // CommonJS assets go to special location - assetDest = path.join(assetDest, 'modules', module.id.toLowerCase()); - } - // Create a task which copies "assets" file tree from all modules. - // Note: Android native module asset handling is inconsistent with commonjs modules and iOS native modules where - // we're not copying assets to "modules/moduleId" directory. Continue doing this for backward compatibility. - tasks.push(walker.walk(path.join(module.modulePath, 'assets'), assetDest)); - - // NOTE: Android just copies without any special processing for platform/android. Should iOS? - // walker.walk(path.join(module.modulePath, 'platform', 'android'), this.buildAppMainDir), - - // Resources - tasks.push(walker.walk(path.join(module.modulePath, 'Resources'), this.buildAppMainAssetsResourcesDir, platformsRegExp)); - tasks.push(walker.walk(path.join(module.modulePath, 'Resources', 'android'), this.buildAppMainAssetsResourcesDir)); - const moduleResults = await Promise.all(tasks); - return gather.mergeMaps(moduleResults); - })); - // merge the allModulesResults over top our current combined! - allModulesResults.unshift(combined); - combined = gather.mergeMaps(allModulesResults); - - // Ok, so we have a Map for the full set of unique relative paths - // now categorize (i.e. lump into buckets of js/css/html/assets/generic resources) - const categorizer = new gather.Categorizer({ - tiappIcon: this.tiapp.icon, - jsFilesNotToProcess: Object.keys(this.htmlJsFiles), - platform: 'android', - }); - return await categorizer.run(combined); -}; - -/** - * Optionally mifies the input css files and copies them to the app - * @param {Map} files map from filename to file info - * @returns {Promise} - */ -AndroidBuilder.prototype.copyCSSFiles = async function copyCSSFiles(files) { - this.logger.debug(__('Copying CSS files')); - const task = new ProcessCSSTask({ - files, - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-css'), - logger: this.logger, - builder: this, - }); - return task.run(); -}; - -/** - * Copies drawable resources into the app - * @param {Map} files map from filename to file info - * @returns {Promise} - */ -AndroidBuilder.prototype.processDrawableFiles = async function processDrawableFiles(files) { - this.logger.debug(__('Copying Drawables')); - const task = new ProcessDrawablesTask({ - files, - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-drawables'), - logger: this.logger, - builder: this, - }); - return task.run(); -}; - -/** - * Copies splash screen resources into the app - * @param {Map} files map from filename to file info - * @returns {Promise} - */ -AndroidBuilder.prototype.processSplashesFiles = async function processSplashesFiles(files) { - this.logger.debug(__('Copying Splash Screens')); - const task = new ProcessSplashesTask({ - files, - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-splashes'), - logger: this.logger, - builder: this, - }); - return task.run(); -}; - -/** - * Used to de4termine the destination path for special assets (_app_props_.json, bootstrap.json) based on encyption or not. - * @returns {string} destination directory to place file - */ -AndroidBuilder.prototype.buildAssetsPath = function buildAssetsPath() { - return this.encryptJS ? this.buildAssetsDir : this.buildAppMainAssetsResourcesDir; -}; - -/** - * Write out file used by Ti.Properties to access properties at runtime - * This may modify this.jsFilesToEncrypt - * @returns {Promise} - */ -AndroidBuilder.prototype.writeAppProps = async function writeAppProps() { - const appPropsFile = path.join(this.buildAssetsPath(), '_app_props_.json'); - const props = {}; - Object.keys(this.tiapp.properties).forEach(prop => { - props[prop] = this.tiapp.properties[prop].value; - }); - await fs.writeFile(appPropsFile, JSON.stringify(props)); - this.encryptJS && this.jsFilesToEncrypt.push('_app_props_.json'); - this.unmarkBuildDirFile(appPropsFile); -}; - -/** - * Write the env variables file - used by node shim for process.env - * This may modify this.jsFilesToEncrypt - * @returns {Promise} - */ -AndroidBuilder.prototype.writeEnvironmentVariables = async function writeEnvironmentVariables() { - const envVarsFile = path.join(this.buildAssetsPath(), '_env_.json'); - await fs.writeFile( - envVarsFile, - // for non-development builds, DO NOT WRITE OUT ENV VARIABLES TO APP - this.writeEnvVars ? JSON.stringify(process.env) : '{}' - ); - this.encryptJS && this.jsFilesToEncrypt.push('_env_.json'); - this.unmarkBuildDirFile(envVarsFile); -}; - -/** - * This may modify this.jsFilesToEncrypt - * @param {Map} jsFilesMap map from filename to file info - * @returns {Promise} - */ -AndroidBuilder.prototype.processJSFiles = async function processJSFiles(jsFilesMap) { - // do the processing - this.logger.info(__('Processing JavaScript files')); - const sdkCommonFolder = path.join(this.titaniumSdkPath, 'common', 'Resources', 'android'); - // For now, need to adapt our Map results to String[] and Object for ProcessJsTask - // Note that because we wipe build/android/app/src/main every build, we have to use a middleman - // directory to hold processed files for incremental builds! - // FIXME: Can we avoid emptying the directory each time?! see generateAppProject() - const jsFiles = {}; - const inputFiles = []; - for (let [ key, value ] of jsFilesMap) { - jsFiles[key] = { - src: value.src, - dest: path.join(this.buildAssetsDir, key) // hijack destination to point to build/assets - }; - inputFiles.push(value.src); - } - - const jsBootstrapFiles = []; // modified by the task and then used after the fact to write our bootstrap.json file - const task = new ProcessJsTask({ - inputFiles, - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-js'), - logger: this.logger, - builder: this, - jsFiles, - jsBootstrapFiles, - sdkCommonFolder, - defaultAnalyzeOptions: { - minify: this.minifyJS, - transpile: this.transpile, - sourceMap: this.sourceMaps, - resourcesDir: this.buildAssetsDir, + }); + const hookResults = await util.promisify(hook)([]); + // merge the hook results on top of the project/node_modules results... (shouldn't be any conflicts!) + hookResults.unshift(combined); + combined = gather.mergeMaps(hookResults); + + this.logger.info('Analyzing module files'); + // detect ambiguous modules + // this.modules.forEach(module => { + // const filename = `${module.id}.js`; + // if (combined.has(filename)) { + // this.logger.error(`There is a project resource "${filename}" that conflicts with a native Android module`); + // this.logger.error('Please rename the file, then rebuild\n'); + // process.exit(1); + // } + // }); + // do modules in parallel - and for each we need to merge the results together! + const allModulesResults = await Promise.all(this.modules.map(async module => { + const tasks = []; + let assetDest = this.buildAppMainAssetsResourcesDir; + if (!module.native) { + // Copy CommonJS non-asset files + const dest = path.join(this.buildAppMainAssetsResourcesDir, path.basename(module.id)); + // Pass in the relative path prefix we should give because we aren't copying direct to the root here. + // Otherwise index.js in one module "overwrites" index.js in another (because they're at same relative path inside module) + tasks.push(walker.walk(module.modulePath, dest, /^(apidoc|docs|documentation|example|assets)$/, null, module.id)); // TODO Consult some .moduleignore file in the module or something? .npmignore? + // CommonJS assets go to special location + assetDest = path.join(assetDest, 'modules', module.id.toLowerCase()); + } + // Create a task which copies "assets" file tree from all modules. + // Note: Android native module asset handling is inconsistent with commonjs modules and iOS native modules where + // we're not copying assets to "modules/moduleId" directory. Continue doing this for backward compatibility. + tasks.push(walker.walk(path.join(module.modulePath, 'assets'), assetDest)); + + // NOTE: Android just copies without any special processing for platform/android. Should iOS? + // walker.walk(path.join(module.modulePath, 'platform', 'android'), this.buildAppMainDir), + + // Resources + tasks.push(walker.walk(path.join(module.modulePath, 'Resources'), this.buildAppMainAssetsResourcesDir, platformsRegExp)); + tasks.push(walker.walk(path.join(module.modulePath, 'Resources', 'android'), this.buildAppMainAssetsResourcesDir)); + const moduleResults = await Promise.all(tasks); + return gather.mergeMaps(moduleResults); + })); + // merge the allModulesResults over top our current combined! + allModulesResults.unshift(combined); + combined = gather.mergeMaps(allModulesResults); + + // Ok, so we have a Map for the full set of unique relative paths + // now categorize (i.e. lump into buckets of js/css/html/assets/generic resources) + const categorizer = new gather.Categorizer({ + tiappIcon: this.tiapp.icon, + jsFilesNotToProcess: Object.keys(this.htmlJsFiles), + platform: 'android', + }); + return await categorizer.run(combined); + } + + /** + * Optionally mifies the input css files and copies them to the app + * @param {Map} files map from filename to file info + * @returns {Promise} + */ + async copyCSSFiles(files) { + this.logger.debug('Copying CSS files'); + const task = new ProcessCSSTask({ + files, + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-css'), logger: this.logger, - targets: { - chrome: this.chromeVersion - } - } - }); - await task.run(); - if (this.useWebpack) { - // Merge Ti symbols from Webpack with the ones from legacy js processing - Object.keys(task.data.tiSymbols).forEach(file => { - const existingSymbols = this.tiSymbols[file] || []; - const additionalSymbols = task.data.tiSymbols[file]; - this.tiSymbols[file] = Array.from(new Set(existingSymbols.concat(additionalSymbols))); + builder: this, }); - } else { - this.tiSymbols = task.data.tiSymbols; // record API usage for analytics - } - - // Copy all unencrypted files processed by ProcessJsTask to "app" project's APK "assets" directory. - // Note: For encrypted builds, our encryptJSFiles() method will write encrypted JS files to the app project. - if (!this.encryptJS) { - // Now we need to copy the processed JS files from build/assets to build/app/src/main/assets/Resources - const resourcesToCopy = new Map(); + return task.run(); + } + + /** + * Copies drawable resources into the app + * @param {Map} files map from filename to file info + * @returns {Promise} + */ + async processDrawableFiles(files) { + this.logger.debug('Copying Drawables'); + const task = new ProcessDrawablesTask({ + files, + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-drawables'), + logger: this.logger, + builder: this, + }); + return task.run(); + } + + /** + * Copies splash screen resources into the app + * @param {Map} files map from filename to file info + * @returns {Promise} + */ + async processSplashesFiles(files) { + this.logger.debug('Copying Splash Screens'); + const task = new ProcessSplashesTask({ + files, + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-splashes'), + logger: this.logger, + builder: this, + }); + return task.run(); + } + + /** + * Used to de4termine the destination path for special assets (_app_props_.json, bootstrap.json) based on encyption or not. + * @returns {string} destination directory to place file + */ + buildAssetsPath() { + return this.encryptJS ? this.buildAssetsDir : this.buildAppMainAssetsResourcesDir; + } + + /** + * Write out file used by Ti.Properties to access properties at runtime + * This may modify this.jsFilesToEncrypt + * @returns {Promise} + */ + async writeAppProps() { + const appPropsFile = path.join(this.buildAssetsPath(), '_app_props_.json'); + const props = {}; + Object.keys(this.tiapp.properties).forEach(prop => { + props[prop] = this.tiapp.properties[prop].value; + }); + await fs.writeFile(appPropsFile, JSON.stringify(props)); + this.encryptJS && this.jsFilesToEncrypt.push('_app_props_.json'); + this.unmarkBuildDirFile(appPropsFile); + } + + /** + * Write the env variables file - used by node shim for process.env + * This may modify this.jsFilesToEncrypt + * @returns {Promise} + */ + async writeEnvironmentVariables() { + const envVarsFile = path.join(this.buildAssetsPath(), '_env_.json'); + await fs.writeFile( + envVarsFile, + // for non-development builds, DO NOT WRITE OUT ENV VARIABLES TO APP + this.writeEnvVars ? JSON.stringify(process.env) : '{}' + ); + this.encryptJS && this.jsFilesToEncrypt.push('_env_.json'); + this.unmarkBuildDirFile(envVarsFile); + } + + /** + * This may modify this.jsFilesToEncrypt + * @param {Map} jsFilesMap map from filename to file info + * @returns {Promise} + */ + async processJSFiles(jsFilesMap) { + // do the processing + this.logger.info('Processing JavaScript files'); + const sdkCommonFolder = path.join(this.titaniumSdkPath, 'common', 'Resources', 'android'); + // For now, need to adapt our Map results to String[] and Object for ProcessJsTask + // Note that because we wipe build/android/app/src/main every build, we have to use a middleman + // directory to hold processed files for incremental builds! + // FIXME: Can we avoid emptying the directory each time?! see generateAppProject() + const jsFiles = {}; + const inputFiles = []; for (let [ key, value ] of jsFilesMap) { - resourcesToCopy.set(key, { - src: path.join(this.buildAssetsDir, key), - dest: value.dest - }); - this.unmarkBuildDirFile(value.dest); + jsFiles[key] = { + src: value.src, + dest: path.join(this.buildAssetsDir, key) // hijack destination to point to build/assets + }; + inputFiles.push(value.src); } - const copyTask = new CopyResourcesTask({ - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'copy-processed-js'), - name: 'copy-processed-js', + + const jsBootstrapFiles = []; // modified by the task and then used after the fact to write our bootstrap.json file + const task = new ProcessJsTask({ + inputFiles, + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-js'), logger: this.logger, builder: this, - files: resourcesToCopy + jsFiles, + jsBootstrapFiles, + sdkCommonFolder, + defaultAnalyzeOptions: { + minify: this.minifyJS, + transpile: this.transpile, + sourceMap: this.sourceMaps, + resourcesDir: this.buildAssetsDir, + logger: this.logger, + targets: { + chrome: this.chromeVersion + } + } }); - await copyTask.run(); - } - - // then write the bootstrap json - return this.writeBootstrapJson(jsBootstrapFiles); -}; - -/** - * @param {string[]} jsBootstrapFiles list of bootstrap js files to add to listing we generate - * @returns {Promise} - */ -AndroidBuilder.prototype.writeBootstrapJson = async function writeBootstrapJson(jsBootstrapFiles) { - this.logger.info(__('Writing bootstrap json')); - // Write the "bootstrap.json" file, even if the bootstrap array is empty. - // Note: An empty array indicates the app has no bootstrap files. - const bootstrapJsonRelativePath = path.join('ti.internal', 'bootstrap.json'); - const bootstrapJsonAbsolutePath = path.join(this.buildAssetsPath(), bootstrapJsonRelativePath); - await fs.ensureDir(path.dirname(bootstrapJsonAbsolutePath)); - await fs.writeFile(bootstrapJsonAbsolutePath, JSON.stringify({ scripts: jsBootstrapFiles })); - this.encryptJS && this.jsFilesToEncrypt.push(bootstrapJsonRelativePath); - this.unmarkBuildDirFile(bootstrapJsonAbsolutePath); -}; - -/** - * Copy "./platform/android" directory tree from all modules and main project to "app" project's "./src/main". - * Android build tools auto-grabs folders named "assets", "res", "aidl", etc. from this folder. - * Note 1: Our "build.gradle" is configured to look for JAR/AAR files here too. (Needed by hyperloop.) - * Note 2: Main Titanium project's folder must be copied last, allowing it to replace asset or res files. - * @returns {Promise} - */ -AndroidBuilder.prototype.copyPlatformDirs = async function copyPlatformDirs() { - const platformDirPaths = []; - for (const module of this.modules) { - if (!module.native) { - platformDirPaths.push(path.join(module.modulePath, 'platform', 'android')); + await task.run(); + if (this.useWebpack) { + // Merge Ti symbols from Webpack with the ones from legacy js processing + Object.keys(task.data.tiSymbols).forEach(file => { + const existingSymbols = this.tiSymbols[file] || []; + const additionalSymbols = task.data.tiSymbols[file]; + this.tiSymbols[file] = Array.from(new Set(existingSymbols.concat(additionalSymbols))); + }); + } else { + this.tiSymbols = task.data.tiSymbols; // record API usage for analytics + } + + // Copy all unencrypted files processed by ProcessJsTask to "app" project's APK "assets" directory. + // Note: For encrypted builds, our encryptJSFiles() method will write encrypted JS files to the app project. + if (!this.encryptJS) { + // Now we need to copy the processed JS files from build/assets to build/app/src/main/assets/Resources + const resourcesToCopy = new Map(); + for (let [ key, value ] of jsFilesMap) { + resourcesToCopy.set(key, { + src: path.join(this.buildAssetsDir, key), + dest: value.dest + }); + this.unmarkBuildDirFile(value.dest); + } + const copyTask = new CopyResourcesTask({ + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'copy-processed-js'), + name: 'copy-processed-js', + logger: this.logger, + builder: this, + files: resourcesToCopy + }); + await copyTask.run(); + } + + // then write the bootstrap json + return this.writeBootstrapJson(jsBootstrapFiles); + } + + /** + * @param {string[]} jsBootstrapFiles list of bootstrap js files to add to listing we generate + * @returns {Promise} + */ + async writeBootstrapJson(jsBootstrapFiles) { + this.logger.info('Writing bootstrap json'); + // Write the "bootstrap.json" file, even if the bootstrap array is empty. + // Note: An empty array indicates the app has no bootstrap files. + const bootstrapJsonRelativePath = path.join('ti.internal', 'bootstrap.json'); + const bootstrapJsonAbsolutePath = path.join(this.buildAssetsPath(), bootstrapJsonRelativePath); + await fs.ensureDir(path.dirname(bootstrapJsonAbsolutePath)); + await fs.writeFile(bootstrapJsonAbsolutePath, JSON.stringify({ scripts: jsBootstrapFiles })); + this.encryptJS && this.jsFilesToEncrypt.push(bootstrapJsonRelativePath); + this.unmarkBuildDirFile(bootstrapJsonAbsolutePath); + } + + /** + * Copy "./platform/android" directory tree from all modules and main project to "app" project's "./src/main". + * Android build tools auto-grabs folders named "assets", "res", "aidl", etc. from this folder. + * Note 1: Our "build.gradle" is configured to look for JAR/AAR files here too. (Needed by hyperloop.) + * Note 2: Main Titanium project's folder must be copied last, allowing it to replace asset or res files. + * @returns {Promise} + */ + async copyPlatformDirs() { + const platformDirPaths = []; + for (const module of this.modules) { + if (!module.native) { + platformDirPaths.push(path.join(module.modulePath, 'platform', 'android')); + } } - } - const googleServicesFile = path.join(this.projectDir, 'platform', 'android', 'google-services.json'); - if (await fs.exists(googleServicesFile)) { - afs.copyFileSync(googleServicesFile, path.join(this.buildAppDir, 'google-services.json'), { - logger: this.logger.debug, - preserve: true - }); - } - platformDirPaths.push(path.join(this.projectDir, 'platform', 'android')); - for (const nextPath of platformDirPaths) { - if (await fs.exists(nextPath)) { - afs.copyDirSyncRecursive(nextPath, this.buildAppMainDir, { + const googleServicesFile = path.join(this.projectDir, 'platform', 'android', 'google-services.json'); + if (await fs.exists(googleServicesFile)) { + afs.copyFileSync(googleServicesFile, path.join(this.buildAppDir, 'google-services.json'), { logger: this.logger.debug, preserve: true }); } + platformDirPaths.push(path.join(this.projectDir, 'platform', 'android')); + for (const nextPath of platformDirPaths) { + if (await fs.exists(nextPath)) { + afs.copyDirSyncRecursive(nextPath, this.buildAppMainDir, { + logger: this.logger.debug, + preserve: true + }); + } + } } -}; - -AndroidBuilder.prototype.copyResources = async function copyResources() { - // First walk all the input dirs and gather/categorize the files into buckets - const gatheredResults = await this.gatherResources(); - this.jsFilesToEncrypt = []; // set listing of files to encrypt to empty array (may be modified by tasks below) - - // ok we now have them organized into broad categories - // we can schedule tasks to happen in parallel: - await Promise.all([ - this.copyCSSFiles(gatheredResults.cssFiles), - this.processJSFiles(gatheredResults.jsFiles), - this.processDrawableFiles(gatheredResults.imageAssets), - this.processSplashesFiles(gatheredResults.launchImages), - this.writeAppProps(), // writes _app_props_.json for Ti.Properties - this.writeEnvironmentVariables(), // writes _env_.json for process.env - this.copyPlatformDirs(), // copies platform/android dirs from project/modules - this.copyUnmodifiedResources(gatheredResults.resourcesToCopy), // copies any other files that don't require special handling (like JS/CSS do) - ]); - - // Finish doing the following after the above tasks have copied files to the build folder. - const templateDir = path.join(this.platformPath, 'templates', 'app', 'default', 'template', 'Resources', 'android'); - return Promise.all([ - this.encryptJSFiles(), - this.ensureAppIcon(templateDir), - this.detectLegacySplashImage(), - ]); -}; - -/** - * Copies all the rest of the files that need no extra processing. - * @param {Map} resourcesToCopy filepaths to file info - * @returns {Promise} - */ -AndroidBuilder.prototype.copyUnmodifiedResources = async function copyUnmodifiedResources(resourcesToCopy) { - this.logger.debug(__('Copying resources')); - const task = new CopyResourcesTask({ - incrementalDirectory: path.join(this.buildTiIncrementalDir, 'copy-resources'), - logger: this.logger, - builder: this, - files: resourcesToCopy - }); - return task.run(); -}; -/** - * Checks if a legacy splash screen "background.png" exists in generated build folder. - * Note: As of Titanium 10.1.0, this image is optional and will use the app icon instead if not found. - */ -AndroidBuilder.prototype.detectLegacySplashImage = async function detectLegacySplashImage() { - // Check if a "background" splash image exists under one of the "res/drawable" folders. - this.hasSplashBackgroundImage = false; - const backgroundRegExp = /^background(\.9)?\.(png|jpg)$/; - for (const dirName of await fs.readdir(this.buildAppMainResDir)) { - if (dirName.startsWith('drawable')) { - const drawableDirPath = path.join(this.buildAppMainResDir, dirName); - for (const fileName of await fs.readdir(drawableDirPath)) { - if (backgroundRegExp.test(fileName)) { - this.hasSplashBackgroundImage = true; - this.unmarkBuildDirFile(path.join(drawableDirPath, fileName)); + async copyResources() { + // First walk all the input dirs and gather/categorize the files into buckets + const gatheredResults = await this.gatherResources(); + this.jsFilesToEncrypt = []; // set listing of files to encrypt to empty array (may be modified by tasks below) + + // ok we now have them organized into broad categories + // we can schedule tasks to happen in parallel: + await Promise.all([ + this.copyCSSFiles(gatheredResults.cssFiles), + this.processJSFiles(gatheredResults.jsFiles), + this.processDrawableFiles(gatheredResults.imageAssets), + this.processSplashesFiles(gatheredResults.launchImages), + this.writeAppProps(), // writes _app_props_.json for Ti.Properties + this.writeEnvironmentVariables(), // writes _env_.json for process.env + this.copyPlatformDirs(), // copies platform/android dirs from project/modules + this.copyUnmodifiedResources(gatheredResults.resourcesToCopy), // copies any other files that don't require special handling (like JS/CSS do) + ]); + + // Finish doing the following after the above tasks have copied files to the build folder. + const templateDir = path.join(this.platformPath, 'templates', 'app', 'default', 'template', 'Resources', 'android'); + return Promise.all([ + this.encryptJSFiles(), + this.ensureAppIcon(templateDir), + this.detectLegacySplashImage(), + ]); + } + + /** + * Copies all the rest of the files that need no extra processing. + * @param {Map} resourcesToCopy filepaths to file info + * @returns {Promise} + */ + copyUnmodifiedResources(resourcesToCopy) { + this.logger.debug('Copying resources'); + const task = new CopyResourcesTask({ + incrementalDirectory: path.join(this.buildTiIncrementalDir, 'copy-resources'), + logger: this.logger, + builder: this, + files: resourcesToCopy + }); + return task.run(); + } + + /** + * Checks if a legacy splash screen "background.png" exists in generated build folder. + * Note: As of Titanium 10.1.0, this image is optional and will use the app icon instead if not found. + */ + async detectLegacySplashImage() { + // Check if a "background" splash image exists under one of the "res/drawable" folders. + this.hasSplashBackgroundImage = false; + const backgroundRegExp = /^background(\.9)?\.(png|jpg)$/; + for (const dirName of await fs.readdir(this.buildAppMainResDir)) { + if (dirName.startsWith('drawable')) { + const drawableDirPath = path.join(this.buildAppMainResDir, dirName); + for (const fileName of await fs.readdir(drawableDirPath)) { + if (backgroundRegExp.test(fileName)) { + this.hasSplashBackgroundImage = true; + this.unmarkBuildDirFile(path.join(drawableDirPath, fileName)); + break; + } + } + if (this.hasSplashBackgroundImage) { break; } } - if (this.hasSplashBackgroundImage) { - break; - } } } -}; -/** - * Ensures the generated app has an app icon - * @param {string} templateDir the filepath to the Titanium SDK's app template for Android apps - */ -AndroidBuilder.prototype.ensureAppIcon = async function ensureAppIcon(templateDir) { - const srcIcon = path.join(templateDir, 'appicon.png'); - const destIcon = path.join(this.buildAppMainAssetsResourcesDir, this.tiapp.icon); + /** + * Ensures the generated app has an app icon + * @param {string} templateDir the filepath to the Titanium SDK's app template for Android apps + */ + async ensureAppIcon(templateDir) { + const srcIcon = path.join(templateDir, 'appicon.png'); + const destIcon = path.join(this.buildAppMainAssetsResourcesDir, this.tiapp.icon); - // if an app icon hasn't been copied, copy the default one from our app template - if (!(await fs.exists(destIcon))) { - this.copyFileSync(srcIcon, destIcon); // TODO: Use async call! - } - this.unmarkBuildDirFile(destIcon); + // if an app icon hasn't been copied, copy the default one from our app template + if (!(await fs.exists(destIcon))) { + this.copyFileSync(srcIcon, destIcon); // TODO: Use async call! + } + this.unmarkBuildDirFile(destIcon); - const destIcon2 = path.join(this.buildAppMainResDrawableDir, this.tiapp.icon); - if (!(await fs.exists(destIcon2))) { - // Note, we are explicitly copying destIcon here as we want to ensure that we're - // copying the user specified icon, srcIcon is the default Titanium icon - this.copyFileSync(destIcon, destIcon2); // TODO: Use async call! + const destIcon2 = path.join(this.buildAppMainResDrawableDir, this.tiapp.icon); + if (!(await fs.exists(destIcon2))) { + // Note, we are explicitly copying destIcon here as we want to ensure that we're + // copying the user specified icon, srcIcon is the default Titanium icon + this.copyFileSync(destIcon, destIcon2); // TODO: Use async call! + } + this.unmarkBuildDirFile(destIcon2); } - this.unmarkBuildDirFile(destIcon2); -}; -/** - * @returns {Promise} - */ -AndroidBuilder.prototype.encryptJSFiles = async function encryptJSFiles() { - if (!this.jsFilesToEncrypt.length) { - // nothing to encrypt, continue - return; - } + /** + * @returns {Promise} + */ + async encryptJSFiles() { + if (!this.jsFilesToEncrypt.length) { + // nothing to encrypt, continue + return; + } - const Cloak = require('ti.cloak').default; - const cloak = this.encryptJS ? new Cloak() : null; - if (!cloak) { - throw new Error('Could not load encryption library!'); - } + const { default: Cloak } = await import('ti.cloak'); + const cloak = this.encryptJS ? new Cloak() : null; + if (!cloak) { + throw new Error('Could not load encryption library!'); + } - this.logger.info('Encrypting javascript assets...'); + this.logger.info('Encrypting javascript assets...'); - // NOTE: maintain 'build.android.titaniumprep' hook for remote encryption policy. - const hook = this.cli.createHook('build.android.titaniumprep', this, async function (exe, args, opts, next) { - try { - await Promise.all( - this.jsFilesToEncrypt.map(async file => { - const from = path.join(this.buildAssetsDir, file); - const to = path.join(this.buildAppMainAssetsResourcesDir, file + '.bin'); - - this.logger.debug(__('Encrypting: %s', from.cyan)); - await fs.ensureDir(path.dirname(to)); - this.unmarkBuildDirFile(to); - return await cloak.encryptFile(from, to); - }) - ); + // NOTE: maintain 'build.android.titaniumprep' hook for remote encryption policy. + const hook = this.cli.createHook('build.android.titaniumprep', this, async function (exe, args, opts, next) { + try { + await Promise.all( + this.jsFilesToEncrypt.map(async file => { + const from = path.join(this.buildAssetsDir, file); + const to = path.join(this.buildAppMainAssetsResourcesDir, file + '.bin'); + + this.logger.debug(`Encrypting: ${from.cyan}`); + await fs.ensureDir(path.dirname(to)); + this.unmarkBuildDirFile(to); + return await cloak.encryptFile(from, to); + }) + ); + + this.logger.info('Writing encryption key...'); + await cloak.setKey('android', this.abis, path.join(this.buildAppMainDir, 'jniLibs')); + + // Generate 'AssetCryptImpl.java' from template. + const assetCryptDest = path.join(this.buildGenAppIdDir, 'AssetCryptImpl.java'); + this.unmarkBuildDirFile(assetCryptDest); + await fs.ensureDir(this.buildGenAppIdDir); + await fs.writeFile( + assetCryptDest, + ejs.render( + await fs.readFile(path.join(this.templatesDir, 'AssetCryptImpl.java'), 'utf8'), + { + appid: this.appid, + assets: this.jsFilesToEncrypt.map(f => f.replace(/\\/g, '/')), + salt: cloak.salt + } + ) + ); - this.logger.info('Writing encryption key...'); - await cloak.setKey('android', this.abis, path.join(this.buildAppMainDir, 'jniLibs')); - - // Generate 'AssetCryptImpl.java' from template. - const assetCryptDest = path.join(this.buildGenAppIdDir, 'AssetCryptImpl.java'); - this.unmarkBuildDirFile(assetCryptDest); - await fs.ensureDir(this.buildGenAppIdDir); - await fs.writeFile( - assetCryptDest, - ejs.render( - await fs.readFile(path.join(this.templatesDir, 'AssetCryptImpl.java'), 'utf8'), - { - appid: this.appid, - assets: this.jsFilesToEncrypt.map(f => f.replace(/\\/g, '/')), - salt: cloak.salt + next(); + } catch (e) { + next(new Error('Could not encrypt assets!\n' + e)); + } + }); + return util.promisify(hook)(null, [ this.tiapp.guid, '' ], {}); + } + + async generateRequireIndex() { + this.logger.info('Generating import/require index file'); + + // Fetch relative paths to all of the app's *.js and *.json files. + const filePathDictionary = {}; + const normalizedAssetsDir = this.buildAppMainAssetsDir.replace(/\\/g, '/'); + const walkDir = async (directoryPath) => { + const fileNameArray = await fs.readdir(directoryPath); + for (const fileName of fileNameArray) { + const filePath = path.join(directoryPath, fileName); + const stat = await fs.stat(filePath); + if (stat.isDirectory()) { + await walkDir(filePath); + } else if (stat.isFile()) { + const lowerCaseFileName = fileName.toLowerCase(); + // TODO: Support mjs files! + if (lowerCaseFileName.endsWith('.js') || lowerCaseFileName.endsWith('.json') || lowerCaseFileName.endsWith('.cjs')) { + let normalizedFilePath = filePath.replace(/\\/g, '/'); + normalizedFilePath = normalizedFilePath.replace(normalizedAssetsDir + '/', ''); + filePathDictionary[normalizedFilePath] = 1; } - ) - ); - - next(); - } catch (e) { - next(new Error('Could not encrypt assets!\n' + e)); - } - }); - return util.promisify(hook)(null, [ this.tiapp.guid, '' ], {}); -}; - -AndroidBuilder.prototype.generateRequireIndex = async function generateRequireIndex() { - this.logger.info('Generating import/require index file'); - - // Fetch relative paths to all of the app's *.js and *.json files. - const filePathDictionary = {}; - const normalizedAssetsDir = this.buildAppMainAssetsDir.replace(/\\/g, '/'); - const walkDir = async (directoryPath) => { - const fileNameArray = await fs.readdir(directoryPath); - for (const fileName of fileNameArray) { - const filePath = path.join(directoryPath, fileName); - const stat = await fs.stat(filePath); - if (stat.isDirectory()) { - await walkDir(filePath); - } else if (stat.isFile()) { - const lowerCaseFileName = fileName.toLowerCase(); - // TODO: Support mjs files! - if (lowerCaseFileName.endsWith('.js') || lowerCaseFileName.endsWith('.json') || lowerCaseFileName.endsWith('.cjs')) { - let normalizedFilePath = filePath.replace(/\\/g, '/'); - normalizedFilePath = normalizedFilePath.replace(normalizedAssetsDir + '/', ''); - filePathDictionary[normalizedFilePath] = 1; } } - } - }; - await walkDir(this.buildAppMainAssetsResourcesDir); - for (const filePath of this.jsFilesToEncrypt) { - filePathDictionary['Resources/' + filePath.replace(/\\/g, '/')] = 1; - } - delete filePathDictionary['Resources/_app_props_.json']; - - // Create the "index.json" file. This is used by our require/import function to load these files. - const indexJsonFilePath = path.join(normalizedAssetsDir, 'index.json'); - if (await fs.exists(indexJsonFilePath)) { - await fs.unlink(indexJsonFilePath); - } - await fs.writeFile(indexJsonFilePath, JSON.stringify(filePathDictionary)); - - // Fetch JavaScript files that should be pre-loaded by the app before required/imported in. - // Always pre-load "app.js" and Alloy generated *.js files. Allows for faster app startup time. - const cacheAssets = [ 'Resources/app.js' ]; - const assets = Object.keys(filePathDictionary); - if (assets.includes('Resources/alloy.js')) { - for (let asset of assets) { - if (asset.startsWith('Resources/alloy')) { - cacheAssets.push(asset); + }; + await walkDir(this.buildAppMainAssetsResourcesDir); + for (const filePath of this.jsFilesToEncrypt) { + filePathDictionary['Resources/' + filePath.replace(/\\/g, '/')] = 1; + } + delete filePathDictionary['Resources/_app_props_.json']; + + // Create the "index.json" file. This is used by our require/import function to load these files. + const indexJsonFilePath = path.join(normalizedAssetsDir, 'index.json'); + if (await fs.exists(indexJsonFilePath)) { + await fs.unlink(indexJsonFilePath); + } + await fs.writeFile(indexJsonFilePath, JSON.stringify(filePathDictionary)); + + // Fetch JavaScript files that should be pre-loaded by the app before required/imported in. + // Always pre-load "app.js" and Alloy generated *.js files. Allows for faster app startup time. + const cacheAssets = [ 'Resources/app.js' ]; + const assets = Object.keys(filePathDictionary); + if (assets.includes('Resources/alloy.js')) { + for (let asset of assets) { + if (asset.startsWith('Resources/alloy')) { + cacheAssets.push(asset); + } } } - } - // Create the "cache.json" file. - const cacheJsonFilePath = path.join(this.buildAppMainAssetsDir, 'cache.json'); - await fs.writeFile(cacheJsonFilePath, JSON.stringify(cacheAssets)); -}; + // Create the "cache.json" file. + const cacheJsonFilePath = path.join(this.buildAppMainAssetsDir, 'cache.json'); + await fs.writeFile(cacheJsonFilePath, JSON.stringify(cacheAssets)); + } -/** - * @param {string} jarFile filepath to JAR - * @returns {Promise} parsed JSON of the module's bindings - */ -AndroidBuilder.prototype.getNativeModuleBindings = async function getNativeModuleBindings(jarFile) { - return new Promise((resolve, reject) => { - const yauzl = require('yauzl'); - yauzl.open(jarFile, { lazyEntries: true }, (err, zipfile) => { - if (err) { - return reject(err); - } - - zipfile.once('error', reject); - zipfile.on('entry', entry => { - if (!entry.fileName.startsWith('org/appcelerator/titanium/bindings/')) { - zipfile.readEntry(); // move on - return; + /** + * @param {string} jarFile filepath to JAR + * @returns {Promise} parsed JSON of the module's bindings + */ + async getNativeModuleBindings(jarFile) { + const yauzl = await import('yauzl'); + return new Promise((resolve, reject) => { + yauzl.open(jarFile, { lazyEntries: true }, (err, zipfile) => { + if (err) { + return reject(err); } - // read the entry - zipfile.openReadStream(entry, function (err, readStream) { - if (err) { - return reject(err); - } - // read file contents and when done, parse as JSON - const chunks = []; - readStream.once('error', reject); - readStream.on('data', chunk => chunks.push(chunk)); - readStream.on('end', () => { - try { - zipfile.close(); - const str = Buffer.concat(chunks).toString('utf8'); - return resolve(JSON.parse(str)); - } catch (error) { - reject(error); + zipfile.once('error', reject); + zipfile.on('entry', entry => { + if (!entry.fileName.startsWith('org/appcelerator/titanium/bindings/')) { + zipfile.readEntry(); // move on + return; + } + // read the entry + zipfile.openReadStream(entry, function (err, readStream) { + if (err) { + return reject(err); } + + // read file contents and when done, parse as JSON + const chunks = []; + readStream.once('error', reject); + readStream.on('data', chunk => chunks.push(chunk)); + readStream.on('end', () => { + try { + zipfile.close(); + const str = Buffer.concat(chunks).toString('utf8'); + return resolve(JSON.parse(str)); + } catch (error) { + reject(error); + } + }); }); }); + zipfile.readEntry(); }); - zipfile.readEntry(); }); - }); -}; - -AndroidBuilder.prototype.generateJavaFiles = async function generateJavaFiles() { - this.logger.info('Generating Java files'); - - const copyTemplate = async (src, dest, ejsParams) => { - this.logger.debug(__('Copying template %s => %s', src.cyan, dest.cyan)); - let fileContent = await fs.readFile(src); - fileContent = ejs.render(fileContent.toString(), ejsParams); - await fs.writeFile(dest, fileContent); - }; - - // Fetch Java proxy class information from all modules. - // Needed so they can be required-in via JavaScript and to enable onAppCreate() method support on app startup. - const moduleProxyArray = []; - for (const module of this.modules) { - // Skip commonjs modules. - if (!module.native) { - continue; - } - - // Attempt to read the module's Java bindings JSON file. - let javaBindings = null; - const moduleName = module.manifest.name; - { - // Check if a ".json" file exists in the module's root directory. - const jsonFilePath = path.join(module.modulePath, moduleName + '.json'); - try { - if (await fs.exists(jsonFilePath)) { - const fileContent = await fs.readFile(jsonFilePath); - if (fileContent) { - javaBindings = JSON.parse(fileContent); - } else { - this.logger.error(__n('Failed to read module "%s" file "%s"', module.id, jsonFilePath)); + } + + async generateJavaFiles() { + this.logger.info('Generating Java files'); + + const copyTemplate = async (src, dest, ejsParams) => { + this.logger.debug(`Copying template ${src.cyan} => ${dest.cyan}`); + let fileContent = await fs.readFile(src); + fileContent = ejs.render(fileContent.toString(), ejsParams); + await fs.writeFile(dest, fileContent); + }; + + // Fetch Java proxy class information from all modules. + // Needed so they can be required-in via JavaScript and to enable onAppCreate() method support on app startup. + const moduleProxyArray = []; + for (const module of this.modules) { + // Skip commonjs modules. + if (!module.native) { + continue; + } + + // Attempt to read the module's Java bindings JSON file. + let javaBindings = null; + const moduleName = module.manifest.name; + { + // Check if a ".json" file exists in the module's root directory. + const jsonFilePath = path.join(module.modulePath, moduleName + '.json'); + try { + if (await fs.exists(jsonFilePath)) { + const fileContent = await fs.readFile(jsonFilePath); + if (fileContent) { + javaBindings = JSON.parse(fileContent); + } else { + this.logger.error(`Failed to read module "${module.id}" file "${jsonFilePath}"`); + } } + } catch (ex) { + this.logger.error(`Error accessing module "${ + module.id + }" file "${ + jsonFilePath + }". Reason: ${ + ex.message + }`); } - } catch (ex) { - this.logger.error(__n( - 'Error accessing module "%s" file "%s". Reason: %s', module.id, jsonFilePath, ex.message)); } - } - if (!javaBindings) { - // Check if a JSON file is embedded within the module's main JAR file. - const jarFilePath = path.join(module.modulePath, moduleName + '.jar'); - try { - if (await fs.exists(jarFilePath)) { - javaBindings = await this.getNativeModuleBindings(jarFilePath); + if (!javaBindings) { + // Check if a JSON file is embedded within the module's main JAR file. + const jarFilePath = path.join(module.modulePath, moduleName + '.jar'); + try { + if (await fs.exists(jarFilePath)) { + javaBindings = await this.getNativeModuleBindings(jarFilePath); + } + } catch (ex) { + this.logger.error(`The module "${module.id}" has an invalid jar file: ${jarFilePath}`); } - } catch (ex) { - this.logger.error(__n('The module "%s" has an invalid jar file: %s', module.id, jarFilePath)); } - } - if (!javaBindings || !javaBindings.modules || !javaBindings.proxies) { - continue; - } - - // Add the module's main Java proxy class info to our "moduleProxyArray" object. - for (const moduleClass in javaBindings.modules) { - // Skip proxy classes not named after the module. - const proxy = javaBindings.proxies[moduleClass]; - if (!proxy || !proxy.proxyAttrs || (proxy.proxyAttrs.id !== module.manifest.moduleid)) { + if (!javaBindings || !javaBindings.modules || !javaBindings.proxies) { continue; } - // Add the module's proxy info to array. - moduleProxyArray.push({ - apiName: javaBindings.modules[moduleClass].apiName, - proxyName: proxy.proxyClassName, - className: moduleClass, - manifest: module.manifest, - onAppCreate: proxy.onAppCreate || proxy['on_app_create'] || null, - isNativeJsModule: !!module.manifest.commonjs - }); - } - } + // Add the module's main Java proxy class info to our "moduleProxyArray" object. + for (const moduleClass in javaBindings.modules) { + // Skip proxy classes not named after the module. + const proxy = javaBindings.proxies[moduleClass]; + if (!proxy || !proxy.proxyAttrs || (proxy.proxyAttrs.id !== module.manifest.moduleid)) { + continue; + } - // Copy main application Java classes. - await fs.ensureDir(this.buildGenAppIdDir); - await copyTemplate( - path.join(this.templatesDir, 'AppInfo.java'), - path.join(this.buildGenAppIdDir, `${this.classname}AppInfo.java`), - { - appid: this.appid, - buildType: this.buildType, - classname: this.classname, - deployType: this.deployType, - tiapp: this.tiapp - }); - await copyTemplate( - path.join(this.templatesDir, 'App.java'), - path.join(this.buildGenAppIdDir, `${this.classname}Application.java`), - { - appid: this.appid, - classname: this.classname, - customModules: moduleProxyArray, - deployType: this.deployType, - encryptJS: this.encryptJS - }); - await copyTemplate( - path.join(this.templatesDir, 'Activity.java'), - path.join(this.buildGenAppIdDir, `${this.classname}Activity.java`), - { - appid: this.appid, - classname: this.classname - }); + // Add the module's proxy info to array. + moduleProxyArray.push({ + apiName: javaBindings.modules[moduleClass].apiName, + proxyName: proxy.proxyClassName, + className: moduleClass, + manifest: module.manifest, + onAppCreate: proxy.onAppCreate || proxy['on_app_create'] || null, + isNativeJsModule: !!module.manifest.commonjs + }); + } + } - // Copy git-ignore file. - afs.copyFileSync( - path.join(this.templatesDir, 'gitignore'), - path.join(this.buildDir, '.gitignore'), - { logger: this.logger.debug }); - - // Generate the JavaScript-based activity classes. - const android = this.tiapp.android; - if (android && android.activities) { - const activityTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSActivity.java'))).toString(); - for (const activityName in android.activities) { - const activity = android.activities[activityName]; - this.logger.debug(__('Generating activity class: %s', activity.classname.cyan)); - const fileContent = ejs.render(activityTemplate, { + // Copy main application Java classes. + await fs.ensureDir(this.buildGenAppIdDir); + await copyTemplate( + path.join(this.templatesDir, 'AppInfo.java'), + path.join(this.buildGenAppIdDir, `${this.classname}AppInfo.java`), + { appid: this.appid, - activity: activity + buildType: this.buildType, + classname: this.classname, + deployType: this.deployType, + tiapp: this.tiapp }); - await fs.writeFile(path.join(this.buildGenAppIdDir, `${activity.classname}.java`), fileContent); - } - } - - // Generate the JavaScript-based Service classes. - if (android && android.services) { - const serviceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSService.java'))).toString(); - const intervalServiceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSIntervalService.java'))).toString(); - const quickSettingsServiceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSQuickSettingsService.java'))).toString(); - for (const serviceName in android.services) { - const service = android.services[serviceName]; - let tpl = serviceTemplate; - if (service.type === 'interval') { - tpl = intervalServiceTemplate; - this.logger.debug(__('Generating interval service class: %s', service.classname.cyan)); - } else if (service.type === 'quicksettings') { - tpl = quickSettingsServiceTemplate; - this.logger.debug(__('Generating quick settings service class: %s', service.classname.cyan)); - } else { - this.logger.debug(__('Generating service class: %s', service.classname.cyan)); - } - const fileContent = ejs.render(tpl, { + await copyTemplate( + path.join(this.templatesDir, 'App.java'), + path.join(this.buildGenAppIdDir, `${this.classname}Application.java`), + { appid: this.appid, - service: service + classname: this.classname, + customModules: moduleProxyArray, + deployType: this.deployType, + encryptJS: this.encryptJS }); - await fs.writeFile(path.join(this.buildGenAppIdDir, `${service.classname}.java`), fileContent); + await copyTemplate( + path.join(this.templatesDir, 'Activity.java'), + path.join(this.buildGenAppIdDir, `${this.classname}Activity.java`), + { + appid: this.appid, + classname: this.classname + }); + + // Copy git-ignore file. + afs.copyFileSync( + path.join(this.templatesDir, 'gitignore'), + path.join(this.buildDir, '.gitignore'), + { logger: this.logger.debug }); + + // Generate the JavaScript-based activity classes. + const android = this.tiapp.android; + if (android && android.activities) { + const activityTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSActivity.java'))).toString(); + for (const activityName in android.activities) { + const activity = android.activities[activityName]; + this.logger.debug(`Generating activity class: ${activity.classname.cyan}`); + const fileContent = ejs.render(activityTemplate, { + appid: this.appid, + activity: activity + }); + await fs.writeFile(path.join(this.buildGenAppIdDir, `${activity.classname}.java`), fileContent); + } } - } -}; - -AndroidBuilder.prototype.generateI18N = async function generateI18N() { - this.logger.info(__('Generating i18n files')); - - const badStringNames = {}; - const data = i18n.load(this.projectDir, this.logger, { - ignoreDirs: this.ignoreDirs, - ignoreFiles: this.ignoreFiles - }); - if (!data.en) { - data.en = data['en-US'] || {}; - } - if (!data.en.app) { - data.en.app = {}; - } - if (!data.en.app.appname) { - data.en.app.appname = this.tiapp.name; - } - function replaceSpaces(s) { - return s.replace(/./g, '\\u0020'); + // Generate the JavaScript-based Service classes. + if (android && android.services) { + const serviceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSService.java'))).toString(); + const intervalServiceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSIntervalService.java'))).toString(); + const quickSettingsServiceTemplate = (await fs.readFile(path.join(this.templatesDir, 'JSQuickSettingsService.java'))).toString(); + for (const serviceName in android.services) { + const service = android.services[serviceName]; + let tpl = serviceTemplate; + if (service.type === 'interval') { + tpl = intervalServiceTemplate; + this.logger.debug(`Generating interval service class: ${service.classname.cyan}`); + } else if (service.type === 'quicksettings') { + tpl = quickSettingsServiceTemplate; + this.logger.debug(`Generating quick settings service class: ${service.classname.cyan}`); + } else { + this.logger.debug(`Generating service class: ${service.classname.cyan}`); + } + const fileContent = ejs.render(tpl, { + appid: this.appid, + service: service + }); + await fs.writeFile(path.join(this.buildGenAppIdDir, `${service.classname}.java`), fileContent); + } + } } - function resolveRegionName(locale) { - if (locale.match(/\w{2}(-|_)r?\w{2}/)) { - const parts = locale.split(/-|_/), - lang = parts[0], - region = parts[1]; - let separator = '-'; + async generateI18N() { + this.logger.info('Generating i18n files'); - if (region.length === 2) { - separator = '-r'; - } + const badStringNames = {}; + const data = i18n.load(this.projectDir, this.logger, { + ignoreDirs: this.ignoreDirs, + ignoreFiles: this.ignoreFiles + }); + if (!data.en) { + data.en = data['en-US'] || {}; + } + if (!data.en.app) { + data.en.app = {}; + } + if (!data.en.app.appname) { + data.en.app.appname = this.tiapp.name; + } - return lang + separator + region; + function replaceSpaces(s) { + return s.replace(/./g, '\\u0020'); } - return locale; - } - // Traverse all loaded i18n locales and write them to XML files under the Android "res" folder. - for (const locale in data) { - // Create a localized strings dictionary if no i18n "strings.xml" file was found. - const localeData = data[locale]; - if (!localeData.strings) { - localeData.strings = {}; - } - - // Add localized app name to strings dictionary under the "app_name" key: - // 1) If not already defined in i18n "strings.xml" file. (This is undocumented, but some devs do this.) - // 2) If defined in i18n "app.xml". (The preferred cross-platform way to localize it.) - // 3) Default to "tiapp.xml" file's if not defined under i18n. (Not localized.) - let appName = localeData.strings.app_name; - if (!appName) { - appName = localeData.app && localeData.app.appname; - if (!appName) { - appName = this.tiapp.name; + function resolveRegionName(locale) { + if (locale.match(/\w{2}(-|_)r?\w{2}/)) { + const parts = locale.split(/-|_/), + lang = parts[0], + region = parts[1]; + let separator = '-'; + + if (region.length === 2) { + separator = '-r'; + } + + return lang + separator + region; } - localeData.strings.app_name = appName; + return locale; } - // Create the XML content for all localized strings. - const dom = new DOMParser().parseFromString('', 'text/xml'); - const root = dom.documentElement; - for (const name in localeData.strings) { - if (name.indexOf(' ') !== -1) { - badStringNames[locale] || (badStringNames[locale] = []); - badStringNames[locale].push(name); - } else { - const node = dom.createElement('string'); - node.setAttribute('name', name); - node.setAttribute('formatted', 'false'); - node.appendChild(dom.createTextNode(localeData.strings[name].replace(/\\?'/g, '\\\'').replace(/^\s+/g, replaceSpaces).replace(/\s+$/g, replaceSpaces))); - root.appendChild(dom.createTextNode('\n\t')); - root.appendChild(node); - } - } - root.appendChild(dom.createTextNode('\n')); - - // Create the XML file under the Android "res/values-" folder. - const defaultLang = this.tiapp.defaultLang || 'en'; - const localeSuffixName = (locale === defaultLang ? '' : '-' + resolveRegionName(locale)); - const dirPath = path.join(this.buildAppMainResDir, `values${localeSuffixName}`); - const filePath = path.join(dirPath, 'ti_i18n_strings.xml'); - this.logger.debug(__('Writing %s strings => %s', locale.cyan, filePath.cyan)); - await fs.ensureDir(dirPath); - await fs.writeFile(filePath, '\n' + dom.documentElement.toString()); - } + // Traverse all loaded i18n locales and write them to XML files under the Android "res" folder. + for (const locale in data) { + // Create a localized strings dictionary if no i18n "strings.xml" file was found. + const localeData = data[locale]; + if (!localeData.strings) { + localeData.strings = {}; + } + + // Add localized app name to strings dictionary under the "app_name" key: + // 1) If not already defined in i18n "strings.xml" file. (This is undocumented, but some devs do this.) + // 2) If defined in i18n "app.xml". (The preferred cross-platform way to localize it.) + // 3) Default to "tiapp.xml" file's if not defined under i18n. (Not localized.) + let appName = localeData.strings.app_name; + if (!appName) { + appName = localeData.app && localeData.app.appname; + if (!appName) { + appName = this.tiapp.name; + } + localeData.strings.app_name = appName; + } - if (Object.keys(badStringNames).length) { - this.logger.error(__('Found invalid i18n string names:')); - Object.keys(badStringNames).forEach(function (locale) { - badStringNames[locale].forEach(function (s) { - this.logger.error(' "' + s + '" (' + locale + ')'); + // Create the XML content for all localized strings. + const dom = new DOMParser().parseFromString('', 'text/xml'); + const root = dom.documentElement; + for (const name in localeData.strings) { + if (name.indexOf(' ') !== -1) { + badStringNames[locale] || (badStringNames[locale] = []); + badStringNames[locale].push(name); + } else { + const node = dom.createElement('string'); + node.setAttribute('name', name); + node.setAttribute('formatted', 'false'); + node.appendChild(dom.createTextNode(localeData.strings[name].replace(/\\?'/g, '\\\'').replace(/^\s+/g, replaceSpaces).replace(/\s+$/g, replaceSpaces))); + root.appendChild(dom.createTextNode('\n\t')); + root.appendChild(node); + } + } + root.appendChild(dom.createTextNode('\n')); + + // Create the XML file under the Android "res/values-" folder. + const defaultLang = this.tiapp.defaultLang || 'en'; + const localeSuffixName = (locale === defaultLang ? '' : '-' + resolveRegionName(locale)); + const dirPath = path.join(this.buildAppMainResDir, `values${localeSuffixName}`); + const filePath = path.join(dirPath, 'ti_i18n_strings.xml'); + this.logger.debug(`Writing ${locale.cyan} strings => ${filePath.cyan}`); + await fs.ensureDir(dirPath); + await fs.writeFile(filePath, '\n' + dom.documentElement.toString()); + } + + if (Object.keys(badStringNames).length) { + this.logger.error('Found invalid i18n string names:'); + Object.keys(badStringNames).forEach(function (locale) { + badStringNames[locale].forEach(function (s) { + this.logger.error(' "' + s + '" (' + locale + ')'); + }, this); }, this); - }, this); - this.logger.error(__('Android does not allow i18n string names with spaces.')); - if (!this.config.get('android.excludeInvalidI18nStrings', false)) { - this.logger.error(__('To exclude invalid i18n strings from the build, run:')); - this.logger.error(' ' + this.cli.argv.$ + ' config android.excludeInvalidI18nStrings true'); - this.logger.log(); - process.exit(1); + this.logger.error('Android does not allow i18n string names with spaces.'); + if (!this.config.get('android.excludeInvalidI18nStrings', false)) { + this.logger.error('To exclude invalid i18n strings from the build, run:'); + this.logger.error(' ' + this.cli.argv.$ + ' config android.excludeInvalidI18nStrings true'); + this.logger.log(); + process.exit(1); + } } } -}; - -AndroidBuilder.prototype.generateSemanticColors = async function generateSemanticColors() { - this.logger.info(__('Generating semantic colors resources')); - const _t = this; - const xmlFileName = 'ti.semantic.colors.xml'; - const valuesDirPath = path.join(this.buildAppMainResDir, 'values'); - const valuesNightDirPath = path.join(this.buildAppMainResDir, 'values-night'); - await fs.ensureDir(valuesDirPath); - await fs.ensureDir(valuesNightDirPath); - const destLight = path.join(valuesDirPath, xmlFileName); - const destNight = path.join(valuesNightDirPath, xmlFileName); - - let colorsFile = path.join(this.projectDir, 'Resources', 'android', 'semantic.colors.json'); - - if (!fs.existsSync(colorsFile)) { - // Fallback to root of Resources folder for Classic applications - colorsFile = path.join(this.projectDir, 'Resources', 'semantic.colors.json'); - } - if (!fs.existsSync(colorsFile)) { - this.logger.debug(__('Skipping colorset generation as "semantic.colors.json" file does not exist')); - return; - } + async generateSemanticColors() { + this.logger.info('Generating semantic colors resources'); + const _t = this; + const xmlFileName = 'ti.semantic.colors.xml'; + const valuesDirPath = path.join(this.buildAppMainResDir, 'values'); + const valuesNightDirPath = path.join(this.buildAppMainResDir, 'values-night'); + await fs.ensureDir(valuesDirPath); + await fs.ensureDir(valuesNightDirPath); + const destLight = path.join(valuesDirPath, xmlFileName); + const destNight = path.join(valuesNightDirPath, xmlFileName); - function appendToXml(dom, root, color, colorValue) { - const appnameNode = dom.createElement('color'); - appnameNode.setAttribute('name', `${color}`); - const colorObj = Color.fromSemanticColorsEntry(colorValue); - appnameNode.appendChild(dom.createTextNode(colorObj.toARGBHexString())); - root.appendChild(dom.createTextNode('\n\t')); - root.appendChild(appnameNode); - } + let colorsFile = path.join(this.projectDir, 'Resources', 'android', 'semantic.colors.json'); - function writeXml(dom, dest, mode) { - if (fs.existsSync(dest)) { - _t.logger.debug(__('Merging %s semantic colors => %s', mode.cyan, dest.cyan)); - } else { - _t.logger.debug(__('Writing %s semantic colors => %s', mode.cyan, dest.cyan)); + if (!fs.existsSync(colorsFile)) { + // Fallback to root of Resources folder for Classic applications + colorsFile = path.join(this.projectDir, 'Resources', 'semantic.colors.json'); } - return fs.writeFile(dest, '\n' + dom.documentElement.toString()); - } - const colors = fs.readJSONSync(colorsFile); - const domLight = new DOMParser().parseFromString('', 'text/xml'); - const domNight = new DOMParser().parseFromString('', 'text/xml'); - - const rootLight = domLight.documentElement; - const rootNight = domNight.documentElement; + if (!fs.existsSync(colorsFile)) { + this.logger.debug('Skipping colorset generation as "semantic.colors.json" file does not exist'); + return; + } - for (const [ color, colorValue ] of Object.entries(colors)) { - if (!colorValue.light) { - this.logger.warn(`Skipping ${color} as it does not include a light value`); - continue; + function appendToXml(dom, root, color, colorValue) { + const appnameNode = dom.createElement('color'); + appnameNode.setAttribute('name', `${color}`); + const colorObj = Color.fromSemanticColorsEntry(colorValue); + appnameNode.appendChild(dom.createTextNode(colorObj.toARGBHexString())); + root.appendChild(dom.createTextNode('\n\t')); + root.appendChild(appnameNode); } - if (!colorValue.dark) { - this.logger.warn(`Skipping ${color} as it does not include a dark value`); - continue; + function writeXml(dom, dest, mode) { + if (fs.existsSync(dest)) { + _t.logger.debug(`Merging ${mode.cyan} semantic colors => ${dest.cyan}`); + } else { + _t.logger.debug(`Writing ${mode.cyan} semantic colors => ${dest.cyan}`); + } + return fs.writeFile(dest, '\n' + dom.documentElement.toString()); } - appendToXml(domLight, rootLight, color, colorValue.light); - appendToXml(domNight, rootNight, color, colorValue.dark); - } + const colors = fs.readJSONSync(colorsFile); + const domLight = new DOMParser().parseFromString('', 'text/xml'); + const domNight = new DOMParser().parseFromString('', 'text/xml'); - return Promise.all([ - writeXml(domLight, destLight, 'light'), - writeXml(domNight, destNight, 'night') - ]); -}; - -AndroidBuilder.prototype.generateTheme = async function generateTheme() { - // Log the theme XML file we're about to generate. - const xmlFileName = 'ti_styles.xml'; - this.logger.info(__('Generating theme file: %s', xmlFileName.cyan)); - - // Set default theme to be used in "AndroidManifest.xml" and style resources. - let defaultAppThemeName = 'Theme.Titanium.DayNight.Solid'; - if (this.tiapp.fullscreen || this.tiapp['statusbar-hidden']) { - defaultAppThemeName += '.Fullscreen'; - } else if (this.tiapp['navbar-hidden']) { - defaultAppThemeName += '.NoTitleBar'; - } + const rootLight = domLight.documentElement; + const rootNight = domNight.documentElement; + + for (const [ color, colorValue ] of Object.entries(colors)) { + if (!colorValue.light) { + this.logger.warn(`Skipping ${color} as it does not include a light value`); + continue; + } + + if (!colorValue.dark) { + this.logger.warn(`Skipping ${color} as it does not include a dark value`); + continue; + } - // Set up "Theme.AppDerived" to use the defined theme, if assigned. - let actualAppTheme = 'Theme.Titanium.App'; - if (this.customAndroidManifest) { - const appTheme = this.customAndroidManifest.getAppAttribute('android:theme'); - if (appTheme && !appTheme.startsWith('@style/Theme.AppDerived') && (appTheme !== '@style/Theme.Titanium')) { - actualAppTheme = appTheme; + appendToXml(domLight, rootLight, color, colorValue.light); + appendToXml(domNight, rootNight, color, colorValue.dark); } - } - // Use background/default PNG for splash if found. Otherwise theme will default to using app icon. - // Also show semi-transparent status/navigation bar if image is set, which was the 10.0.0 behavior. - const translucentXmlValue = this.hasSplashBackgroundImage ? 'true' : 'false'; - let windowBackgroundImageXmlString = ''; - if (this.hasSplashBackgroundImage) { - windowBackgroundImageXmlString = '@drawable/background'; + return Promise.all([ + writeXml(domLight, destLight, 'light'), + writeXml(domNight, destNight, 'night') + ]); } - // Create the theme XML file with above activity style. - // Also apply app's background image to root splash activity theme. - let valuesDirPath = path.join(this.buildAppMainResDir, 'values'); - let xmlLines = [ - '', - '', - ` ', - '' - ]; - await fs.ensureDir(valuesDirPath); - await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); - - // Create a theme XML for different Android OS versions depending on how the splash is configured. - const iconDrawable = '@drawable/titanium_splash_icon_background'; - const adaptiveIconDrawable = '@drawable/titanium_splash_adaptive_icon_background'; - if (this.hasSplashBackgroundImage) { - // Project uses background/default PNG for splash, but we will ignore it on Android 12 and higher. - // Note: Android 12 forces all apps to use an icon for splash screen. Cannot opt-out. - const iconValue = this.appRoundIconManifestValue ? this.appRoundIconManifestValue : this.appIconManifestValue; - const windowBackgroundValue = this.appRoundIconManifestValue ? adaptiveIconDrawable : iconDrawable; - valuesDirPath = path.join(this.buildAppMainResDir, 'values-v31'); - xmlLines = [ - '', - '', - ' ', - '' - ]; - await fs.ensureDir(valuesDirPath); - await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + async generateTheme() { + // Log the theme XML file we're about to generate. + const xmlFileName = 'ti_styles.xml'; + this.logger.info(`Generating theme file: ${xmlFileName.cyan}`); - // Set up translucent status/navigation bars to show dark icons/buttons on Android 8.1 - 11.x. - valuesDirPath = path.join(this.buildAppMainResDir, 'values-v27'); - xmlLines = [ - '', - '', - ' ', - '' - ]; - await fs.ensureDir(valuesDirPath); - await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + // Set default theme to be used in "AndroidManifest.xml" and style resources. + let defaultAppThemeName = 'Theme.Titanium.DayNight.Solid'; + if (this.tiapp.fullscreen || this.tiapp['statusbar-hidden']) { + defaultAppThemeName += '.Fullscreen'; + } else if (this.tiapp['navbar-hidden']) { + defaultAppThemeName += '.NoTitleBar'; + } + + // Set up "Theme.AppDerived" to use the defined theme, if assigned. + let actualAppTheme = 'Theme.Titanium.App'; + if (this.customAndroidManifest) { + const appTheme = this.customAndroidManifest.getAppAttribute('android:theme'); + if (appTheme && !appTheme.startsWith('@style/Theme.AppDerived') && (appTheme !== '@style/Theme.Titanium')) { + actualAppTheme = appTheme; + } + } + + // Use background/default PNG for splash if found. Otherwise theme will default to using app icon. + // Also show semi-transparent status/navigation bar if image is set, which was the 10.0.0 behavior. + const translucentXmlValue = this.hasSplashBackgroundImage ? 'true' : 'false'; + let windowBackgroundImageXmlString = ''; + if (this.hasSplashBackgroundImage) { + windowBackgroundImageXmlString = '@drawable/background'; + } - // Set up translucent status bars to show dark icons on Android 6.0 - 8.0. (Cannot do this with nav buttons.) - valuesDirPath = path.join(this.buildAppMainResDir, 'values-v23'); - xmlLines = [ + // Create the theme XML file with above activity style. + // Also apply app's background image to root splash activity theme. + let valuesDirPath = path.join(this.buildAppMainResDir, 'values'); + let xmlLines = [ '', '', + ` ', '' ]; await fs.ensureDir(valuesDirPath); await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); - } else if (this.appRoundIconManifestValue) { - // Project is set up to use app icon for the splash on all Android OS versions. (No fullscreen splash image.) - // Since manifest has an "android:roundIcon" adaptive icon defined, use it on Android 8 and higher. - valuesDirPath = path.join(this.buildAppMainResDir, 'values-v26'); - xmlLines = [ - '', - '', - ' ', - '' - ]; - await fs.ensureDir(valuesDirPath); - await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); - } -}; - -AndroidBuilder.prototype.fetchNeededManifestSettings = function fetchNeededManifestSettings() { - // Check if permission injection is disabled in "tiapp.xml". - // Note: Recommended solution is to use 'tools:node="remove"' attributes within instead. - const canAddPermissions = !this.tiapp['override-permissions']; - - // Define Android names needed by our core Titanium APIs. - const calendarPermissions = [ 'android.permission.READ_CALENDAR', 'android.permission.WRITE_CALENDAR' ]; - const cameraPermissions = [ 'android.permission.CAMERA', 'android.permission.WRITE_EXTERNAL_STORAGE' ]; - const contactsPermissions = [ 'android.permission.READ_CONTACTS', 'android.permission.WRITE_CONTACTS' ]; - const contactsReadPermissions = [ 'android.permission.READ_CONTACTS' ]; - const geoPermissions = [ 'android.permission.ACCESS_COARSE_LOCATION', 'android.permission.ACCESS_FINE_LOCATION' ]; - const storagePermissions = [ 'android.permission.WRITE_EXTERNAL_STORAGE' ]; - const vibratePermissions = [ 'android.permission.VIBRATE' ]; - const wallpaperPermissions = [ 'android.permission.SET_WALLPAPER' ]; - - // Define namespaces that need permissions when accessed in JavaScript. - const tiNamespacePermissions = { - Geolocation: geoPermissions - }; - - // Define methods that need permissions when invoked in JavaScript. - const tiMethodPermissions = { - 'Calendar.getAllAlerts': calendarPermissions, - 'Calendar.getAllCalendars': calendarPermissions, - 'Calendar.getCalendarById': calendarPermissions, - 'Calendar.getSelectableCalendars': calendarPermissions, - - 'Contacts.createPerson': contactsPermissions, - 'Contacts.removePerson': contactsPermissions, - 'Contacts.getAllContacts': contactsReadPermissions, - 'Contacts.showContactPicker': contactsReadPermissions, - 'Contacts.showContacts': contactsReadPermissions, - 'Contacts.getPeopleWithName': contactsReadPermissions, - 'Contacts.getAllPeople': contactsReadPermissions, - 'Contacts.getAllGroups': contactsReadPermissions, - - 'Filesystem.requestStoragePermissions': storagePermissions, - - 'Media.Android.setSystemWallpaper': wallpaperPermissions, - 'Media.saveToPhotoGallery': storagePermissions, - 'Media.showCamera': cameraPermissions, - 'Media.vibrate': vibratePermissions, - }; - - // Add Titanium's default permissions. - // Note: You would normally define needed permissions in AAR library's manifest file, - // but we want "tiapp.xml" property "override-permissions" to be able to override this behavior. - const neededPermissionDictionary = {}; - if (canAddPermissions) { - neededPermissionDictionary['android.permission.INTERNET'] = true; - neededPermissionDictionary['android.permission.ACCESS_WIFI_STATE'] = true; - neededPermissionDictionary['android.permission.ACCESS_NETWORK_STATE'] = true; - } - // Set the max API Level the "WRITE_EXTERNAL_STORAGE" permission should use. - // Android 10 and higher doesn't need this permission unless requestStoragePermissions() method is used. - let storagePermissionMaxSdkVersion = 28; + // Create a theme XML for different Android OS versions depending on how the splash is configured. + const iconDrawable = '@drawable/titanium_splash_icon_background'; + const adaptiveIconDrawable = '@drawable/titanium_splash_adaptive_icon_background'; + if (this.hasSplashBackgroundImage) { + // Project uses background/default PNG for splash, but we will ignore it on Android 12 and higher. + // Note: Android 12 forces all apps to use an icon for splash screen. Cannot opt-out. + const iconValue = this.appRoundIconManifestValue ? this.appRoundIconManifestValue : this.appIconManifestValue; + const windowBackgroundValue = this.appRoundIconManifestValue ? adaptiveIconDrawable : iconDrawable; + valuesDirPath = path.join(this.buildAppMainResDir, 'values-v31'); + xmlLines = [ + '', + '', + ' ', + '' + ]; + await fs.ensureDir(valuesDirPath); + await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + + // Set up translucent status/navigation bars to show dark icons/buttons on Android 8.1 - 11.x. + valuesDirPath = path.join(this.buildAppMainResDir, 'values-v27'); + xmlLines = [ + '', + '', + ' ', + '' + ]; + await fs.ensureDir(valuesDirPath); + await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + + // Set up translucent status bars to show dark icons on Android 6.0 - 8.0. (Cannot do this with nav buttons.) + valuesDirPath = path.join(this.buildAppMainResDir, 'values-v23'); + xmlLines = [ + '', + '', + ' ', + '' + ]; + await fs.ensureDir(valuesDirPath); + await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + } else if (this.appRoundIconManifestValue) { + // Project is set up to use app icon for the splash on all Android OS versions. (No fullscreen splash image.) + // Since manifest has an "android:roundIcon" adaptive icon defined, use it on Android 8 and higher. + valuesDirPath = path.join(this.buildAppMainResDir, 'values-v26'); + xmlLines = [ + '', + '', + ' ', + '' + ]; + await fs.ensureDir(valuesDirPath); + await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n')); + } + } + + fetchNeededManifestSettings() { + // Check if permission injection is disabled in "tiapp.xml". + // Note: Recommended solution is to use 'tools:node="remove"' attributes within instead. + const canAddPermissions = !this.tiapp['override-permissions']; + + // Define Android names needed by our core Titanium APIs. + const calendarPermissions = [ 'android.permission.READ_CALENDAR', 'android.permission.WRITE_CALENDAR' ]; + const cameraPermissions = [ 'android.permission.CAMERA', 'android.permission.WRITE_EXTERNAL_STORAGE' ]; + const contactsPermissions = [ 'android.permission.READ_CONTACTS', 'android.permission.WRITE_CONTACTS' ]; + const contactsReadPermissions = [ 'android.permission.READ_CONTACTS' ]; + const geoPermissions = [ 'android.permission.ACCESS_COARSE_LOCATION', 'android.permission.ACCESS_FINE_LOCATION' ]; + const storagePermissions = [ 'android.permission.WRITE_EXTERNAL_STORAGE' ]; + const vibratePermissions = [ 'android.permission.VIBRATE' ]; + const wallpaperPermissions = [ 'android.permission.SET_WALLPAPER' ]; + + // Define namespaces that need permissions when accessed in JavaScript. + const tiNamespacePermissions = { + Geolocation: geoPermissions + }; - // Define JavaScript methods that need manifest entries. - // The value strings are used as boolean property names in our "AndroidManifest.xml" EJS template. - const tiMethodQueries = { - 'UI.createEmailDialog': 'sendEmail', - 'UI.EmailDialog': 'sendEmail' - }; + // Define methods that need permissions when invoked in JavaScript. + const tiMethodPermissions = { + 'Calendar.getAllAlerts': calendarPermissions, + 'Calendar.getAllCalendars': calendarPermissions, + 'Calendar.getCalendarById': calendarPermissions, + 'Calendar.getSelectableCalendars': calendarPermissions, + + 'Contacts.createPerson': contactsPermissions, + 'Contacts.removePerson': contactsPermissions, + 'Contacts.getAllContacts': contactsReadPermissions, + 'Contacts.showContactPicker': contactsReadPermissions, + 'Contacts.showContacts': contactsReadPermissions, + 'Contacts.getPeopleWithName': contactsReadPermissions, + 'Contacts.getAllPeople': contactsReadPermissions, + 'Contacts.getAllGroups': contactsReadPermissions, + + 'Filesystem.requestStoragePermissions': storagePermissions, + + 'Media.Android.setSystemWallpaper': wallpaperPermissions, + 'Media.saveToPhotoGallery': storagePermissions, + 'Media.showCamera': cameraPermissions, + 'Media.vibrate': vibratePermissions, + }; - // To be populated with needed by the app. - // Uses the string values from "tiMethodQueries" as keys. - const neededQueriesDictionary = {}; + // Add Titanium's default permissions. + // Note: You would normally define needed permissions in AAR library's manifest file, + // but we want "tiapp.xml" property "override-permissions" to be able to override this behavior. + const neededPermissionDictionary = {}; + if (canAddPermissions) { + neededPermissionDictionary['android.permission.INTERNET'] = true; + neededPermissionDictionary['android.permission.ACCESS_WIFI_STATE'] = true; + neededPermissionDictionary['android.permission.ACCESS_NETWORK_STATE'] = true; + } + + // Set the max API Level the "WRITE_EXTERNAL_STORAGE" permission should use. + // Android 10 and higher doesn't need this permission unless requestStoragePermissions() method is used. + let storagePermissionMaxSdkVersion = 28; + + // Define JavaScript methods that need manifest entries. + // The value strings are used as boolean property names in our "AndroidManifest.xml" EJS template. + const tiMethodQueries = { + 'UI.createEmailDialog': 'sendEmail', + 'UI.EmailDialog': 'sendEmail' + }; - // Make sure Titanium symbols variable "tiSymbols" is valid. - if (!this.tiSymbols) { - this.tiSymbols = {}; - } + // To be populated with needed by the app. + // Uses the string values from "tiMethodQueries" as keys. + const neededQueriesDictionary = {}; - // Traverse all accessed namespaces/methods in JavaScript. - // Add any Android permissions/queries needed if matching the above mappings. - const accessedSymbols = {}; - for (const file in this.tiSymbols) { - // Fetch all symbols from the next JavaScript file. - const symbolArray = this.tiSymbols[file]; - if (!symbolArray) { - continue; + // Make sure Titanium symbols variable "tiSymbols" is valid. + if (!this.tiSymbols) { + this.tiSymbols = {}; } - // Traverse all of JavaScript symbols. - for (const symbol of symbolArray) { - // Do not continue if we've already evaluated this symbol before. - if (!symbol || accessedSymbols[symbol]) { + // Traverse all accessed namespaces/methods in JavaScript. + // Add any Android permissions/queries needed if matching the above mappings. + const accessedSymbols = {}; + for (const file in this.tiSymbols) { + // Fetch all symbols from the next JavaScript file. + const symbolArray = this.tiSymbols[file]; + if (!symbolArray) { continue; } - accessedSymbols[symbol] = true; - - // Check if symbol requires any Android permissions. - if (canAddPermissions) { - let permissionArray; - - // If symbol is a namespace, then check if it needs permission. - // Note: Check each namespace component separately, split via periods. - const namespaceParts = symbol.split('.').slice(0, -1); - for (;namespaceParts.length > 0; namespaceParts.pop()) { - const namespace = namespaceParts.join('.'); - if (namespace) { - permissionArray = tiNamespacePermissions[namespace]; - if (permissionArray) { // eslint-disable-line max-depth - for (const permission of permissionArray) { // eslint-disable-line max-depth - neededPermissionDictionary[permission] = true; + + // Traverse all of JavaScript symbols. + for (const symbol of symbolArray) { + // Do not continue if we've already evaluated this symbol before. + if (!symbol || accessedSymbols[symbol]) { + continue; + } + accessedSymbols[symbol] = true; + + // Check if symbol requires any Android permissions. + if (canAddPermissions) { + let permissionArray; + + // If symbol is a namespace, then check if it needs permission. + // Note: Check each namespace component separately, split via periods. + const namespaceParts = symbol.split('.').slice(0, -1); + for (;namespaceParts.length > 0; namespaceParts.pop()) { + const namespace = namespaceParts.join('.'); + if (namespace) { + permissionArray = tiNamespacePermissions[namespace]; + if (permissionArray) { // eslint-disable-line max-depth + for (const permission of permissionArray) { // eslint-disable-line max-depth + neededPermissionDictionary[permission] = true; + } } } } - } - // If symbol is a method, then check if it needs permission. - permissionArray = tiMethodPermissions[symbol]; - if (permissionArray) { - for (const permission of permissionArray) { - neededPermissionDictionary[permission] = true; - } - if (symbol === 'Filesystem.requestStoragePermissions') { - storagePermissionMaxSdkVersion = undefined; + // If symbol is a method, then check if it needs permission. + permissionArray = tiMethodPermissions[symbol]; + if (permissionArray) { + for (const permission of permissionArray) { + neededPermissionDictionary[permission] = true; + } + if (symbol === 'Filesystem.requestStoragePermissions') { + storagePermissionMaxSdkVersion = undefined; + } } } - } - // Check if symbol requires an Android entry. - const queryName = tiMethodQueries[symbol]; - if (queryName) { - neededQueriesDictionary[queryName] = true; + // Check if symbol requires an Android entry. + const queryName = tiMethodQueries[symbol]; + if (queryName) { + neededQueriesDictionary[queryName] = true; + } } } - } - // Return the entries needed to be injected into the generated "AndroidManifest.xml" file. - const neededSettings = { - queries: neededQueriesDictionary, - storagePermissionMaxSdkVersion: storagePermissionMaxSdkVersion, - usesPermissions: Object.keys(neededPermissionDictionary) - }; - return neededSettings; -}; - -AndroidBuilder.prototype.generateAndroidManifest = async function generateAndroidManifest() { - this.logger.info(__('Generating main "AndroidManifest.xml" files')); - - // Make sure app project's "./src/main" directory exists. - await fs.ensureDir(this.buildAppMainDir); - - // We no longer support setting the following option to false anymore. Log a warning if not set to merge. - // Note: Gradle handles the manifest merge between libraries and app project. Must use its features to manage it. - if (!this.config.get('android.mergeCustomAndroidManifest', true)) { - const message - = 'Titanium CLI option "android.mergeCustomAndroidManifest" is no longer supported. ' - + 'Use Google\'s "AndroidManifest.xml" feature "tools:remove" to remove XML elements instead.'; - this.logger.warn(__n(message)); + // Return the entries needed to be injected into the generated "AndroidManifest.xml" file. + const neededSettings = { + queries: neededQueriesDictionary, + storagePermissionMaxSdkVersion: storagePermissionMaxSdkVersion, + usesPermissions: Object.keys(neededPermissionDictionary) + }; + return neededSettings; } - // Generate all XML lines to be added as children within the manifest's block. - const appChildXmlLines = []; - if (this.tiapp.android) { - // Fetch all "ti:app/android/activities" defined in "tiapp.xml" file. - // These are our own custom JSActivity settings and are outside of the block. - const tiappActivities = this.tiapp.android.activities || {}; - for (const jsFileName in tiappActivities) { - // Get the next JSActivity. - const tiActivityInfo = tiappActivities[jsFileName]; - if (!tiActivityInfo || !tiActivityInfo.url || !tiActivityInfo.classname) { - continue; - } + async generateAndroidManifest() { + this.logger.info('Generating main "AndroidManifest.xml" files'); - // Add its XML string to array. - const xmlDoc = (new DOMParser()).parseFromString('', 'text/xml'); - for (const propertyName in tiActivityInfo) { - if (propertyName.startsWith('android:')) { - const propertyValue = tiActivityInfo[propertyName]; - xmlDoc.documentElement.setAttribute(propertyName, propertyValue ? propertyValue.toString() : ''); - } - } - xmlDoc.documentElement.setAttribute('android:name', `${this.appid}.${tiActivityInfo.classname}`); - appChildXmlLines.push(xmlDoc.documentElement.toString()); + // Make sure app project's "./src/main" directory exists. + await fs.ensureDir(this.buildAppMainDir); + + // We no longer support setting the following option to false anymore. Log a warning if not set to merge. + // Note: Gradle handles the manifest merge between libraries and app project. Must use its features to manage it. + if (!this.config.get('android.mergeCustomAndroidManifest', true)) { + this.logger.warn('Titanium CLI option "android.mergeCustomAndroidManifest" is no longer supported.'); + this.logger.warn('Use Google\'s "AndroidManifest.xml" feature "tools:remove" to remove XML elements instead.'); } - // Fetch all "ti:app/android/services" defined in "tiapp.xml" file. - // These are our own custom JSService settings and are outside of the block. - const tiappServices = this.tiapp.android.services || {}; - for (const jsFileName in tiappServices) { - // Get the next JSService. - const tiServiceInfo = tiappServices[jsFileName]; - if (!tiServiceInfo || !tiServiceInfo.url || !tiServiceInfo.classname) { - continue; - } + // Generate all XML lines to be added as children within the manifest's block. + const appChildXmlLines = []; + if (this.tiapp.android) { + // Fetch all "ti:app/android/activities" defined in "tiapp.xml" file. + // These are our own custom JSActivity settings and are outside of the block. + const tiappActivities = this.tiapp.android.activities || {}; + for (const jsFileName in tiappActivities) { + // Get the next JSActivity. + const tiActivityInfo = tiappActivities[jsFileName]; + if (!tiActivityInfo || !tiActivityInfo.url || !tiActivityInfo.classname) { + continue; + } - // Add its and XML string(s) to array. - const serviceName = `${this.appid}.${tiServiceInfo.classname}`; - if (tiServiceInfo.type === 'quicksettings') { - // QuickSettings service is generated via EJS template. - let xmlContent = await fs.readFile(path.join(this.templatesDir, 'QuickService.xml')); - xmlContent = ejs.render(xmlContent.toString(), { - icon: '@drawable/' + (tiServiceInfo.icon || this.tiapp.icon).replace(/((\.9)?\.(png|jpg))$/, ''), - label: tiServiceInfo.label || this.tiapp.name, - serviceName: serviceName - }); - const xmlDoc = (new DOMParser()).parseFromString(xmlContent, 'text/xml'); - const xmlString = xmlDoc.documentElement.toString().replace(/xmlns:android=""/g, ''); - const xmlLines = xmlString.split('\n'); - appChildXmlLines.push(...xmlLines); // Spread operator "..." turns an array into multiple arguments. - } else { - // This is a simple service. Add its 1 XML line to the array. - const xmlDoc = (new DOMParser()).parseFromString('', 'text/xml'); - for (const propertyName in tiServiceInfo) { + // Add its XML string to array. + const xmlDoc = (new DOMParser()).parseFromString('', 'text/xml'); + for (const propertyName in tiActivityInfo) { if (propertyName.startsWith('android:')) { - const propertyValue = tiServiceInfo[propertyName]; + const propertyValue = tiActivityInfo[propertyName]; xmlDoc.documentElement.setAttribute(propertyName, propertyValue ? propertyValue.toString() : ''); } } - xmlDoc.documentElement.setAttribute('android:name', serviceName); + xmlDoc.documentElement.setAttribute('android:name', `${this.appid}.${tiActivityInfo.classname}`); appChildXmlLines.push(xmlDoc.documentElement.toString()); } + + // Fetch all "ti:app/android/services" defined in "tiapp.xml" file. + // These are our own custom JSService settings and are outside of the block. + const tiappServices = this.tiapp.android.services || {}; + for (const jsFileName in tiappServices) { + // Get the next JSService. + const tiServiceInfo = tiappServices[jsFileName]; + if (!tiServiceInfo || !tiServiceInfo.url || !tiServiceInfo.classname) { + continue; + } + + // Add its and XML string(s) to array. + const serviceName = `${this.appid}.${tiServiceInfo.classname}`; + if (tiServiceInfo.type === 'quicksettings') { + // QuickSettings service is generated via EJS template. + let xmlContent = await fs.readFile(path.join(this.templatesDir, 'QuickService.xml')); + xmlContent = ejs.render(xmlContent.toString(), { + icon: '@drawable/' + (tiServiceInfo.icon || this.tiapp.icon).replace(/((\.9)?\.(png|jpg))$/, ''), + label: tiServiceInfo.label || this.tiapp.name, + serviceName: serviceName + }); + const xmlDoc = (new DOMParser()).parseFromString(xmlContent, 'text/xml'); + const xmlString = xmlDoc.documentElement.toString().replace(/xmlns:android=""/g, ''); + const xmlLines = xmlString.split('\n'); + appChildXmlLines.push(...xmlLines); // Spread operator "..." turns an array into multiple arguments. + } else { + // This is a simple service. Add its 1 XML line to the array. + const xmlDoc = (new DOMParser()).parseFromString('', 'text/xml'); + for (const propertyName in tiServiceInfo) { + if (propertyName.startsWith('android:')) { + const propertyValue = tiServiceInfo[propertyName]; + xmlDoc.documentElement.setAttribute(propertyName, propertyValue ? propertyValue.toString() : ''); + } + } + xmlDoc.documentElement.setAttribute('android:name', serviceName); + appChildXmlLines.push(xmlDoc.documentElement.toString()); + } + } } - } - // Scan app's JS code to see what and entries should be auto-injected into manifest. - const neededManifestSettings = this.fetchNeededManifestSettings(); - - // Generate the app's main manifest from EJS template. - let mainManifestContent = await fs.readFile(path.join(this.templatesDir, 'AndroidManifest.xml')); - mainManifestContent = ejs.render(mainManifestContent.toString(), { - appChildXmlLines: appChildXmlLines, - appIcon: this.appIconManifestValue, - appLabel: this.tiapp.name, - classname: this.classname, - storagePermissionMaxSdkVersion: neededManifestSettings.storagePermissionMaxSdkVersion, - packageName: this.appid, - queries: neededManifestSettings.queries, - usesPermissions: neededManifestSettings.usesPermissions - }); - const mainManifest = AndroidManifest.fromXmlString(mainManifestContent); - - // Write the main "AndroidManifest.xml" file providing Titanium's default app manifest settings. - const mainManifestFilePath = path.join(this.buildAppMainDir, 'AndroidManifest.xml'); - await new Promise((resolve) => { - const writeHook = this.cli.createHook('build.android.writeAndroidManifest', this, (file, xml, done) => { - done(); + // Scan app's JS code to see what and entries should be auto-injected into manifest. + const neededManifestSettings = this.fetchNeededManifestSettings(); + + // Generate the app's main manifest from EJS template. + let mainManifestContent = await fs.readFile(path.join(this.templatesDir, 'AndroidManifest.xml')); + mainManifestContent = ejs.render(mainManifestContent.toString(), { + appChildXmlLines: appChildXmlLines, + appIcon: this.appIconManifestValue, + appLabel: this.tiapp.name, + classname: this.classname, + storagePermissionMaxSdkVersion: neededManifestSettings.storagePermissionMaxSdkVersion, + packageName: this.appid, + queries: neededManifestSettings.queries, + usesPermissions: neededManifestSettings.usesPermissions }); - writeHook(mainManifestFilePath, null, resolve); - }); - await mainManifest.writeToFilePath(mainManifestFilePath); + const mainManifest = AndroidManifest.fromXmlString(mainManifestContent); - // Set up secondary manifest object which will store custom manifest settings provided by Titanium app developer. - // This will be written to app project's "debug" and "release" directories. - const secondaryManifest = new AndroidManifest(); + // Write the main "AndroidManifest.xml" file providing Titanium's default app manifest settings. + const mainManifestFilePath = path.join(this.buildAppMainDir, 'AndroidManifest.xml'); + await new Promise((resolve) => { + const writeHook = this.cli.createHook('build.android.writeAndroidManifest', this, (file, xml, done) => { + done(); + }); + writeHook(mainManifestFilePath, null, resolve); + }); + await mainManifest.writeToFilePath(mainManifestFilePath); - // Copy all CommonJS module "AndroidManifest.xml" settings to secondary manifest object first. - for (const module of this.modules) { - // Skip native modules. Their manifest files will be handled by gradle build system. - if (module.native) { - continue; - } + // Set up secondary manifest object which will store custom manifest settings provided by Titanium app developer. + // This will be written to app project's "debug" and "release" directories. + const secondaryManifest = new AndroidManifest(); - // Copy manifest settings from "timodule.xml" if provided. - const tiModuleXmlFilePath = path.join(module.modulePath, 'timodule.xml'); - try { - if (await fs.exists(tiModuleXmlFilePath)) { - const tiModuleInfo = new tiappxml(tiModuleXmlFilePath); - if (tiModuleInfo && tiModuleInfo.android && tiModuleInfo.android.manifest) { - const tiModuleManifest = AndroidManifest.fromXmlString(tiModuleInfo.android.manifest); - secondaryManifest.copyFromAndroidManifest(tiModuleManifest); - } + // Copy all CommonJS module "AndroidManifest.xml" settings to secondary manifest object first. + for (const module of this.modules) { + // Skip native modules. Their manifest files will be handled by gradle build system. + if (module.native) { + continue; } - } catch (ex) { - this.logger.error(`Unable to load Android content from: ${tiModuleXmlFilePath}`); - throw ex; - } - // Copy module's "./platform/android/AndroidManifest.xml" file if it exists. - const externalXmlFilePath = path.join(module.modulePath, 'platform', 'android', 'AndroidManifest.xml'); - try { - if (await fs.exists(externalXmlFilePath)) { - const externalManifest = await AndroidManifest.fromFilePath(externalXmlFilePath); - secondaryManifest.copyFromAndroidManifest(externalManifest); + // Copy manifest settings from "timodule.xml" if provided. + const tiModuleXmlFilePath = path.join(module.modulePath, 'timodule.xml'); + try { + if (await fs.exists(tiModuleXmlFilePath)) { + const tiModuleInfo = new tiappxml(tiModuleXmlFilePath); + if (tiModuleInfo && tiModuleInfo.android && tiModuleInfo.android.manifest) { + const tiModuleManifest = AndroidManifest.fromXmlString(tiModuleInfo.android.manifest); + secondaryManifest.copyFromAndroidManifest(tiModuleManifest); + } + } + } catch (ex) { + this.logger.error(`Unable to load Android content from: ${tiModuleXmlFilePath}`); + throw ex; } - } catch (ex) { - this.logger.error(`Unable to load file: ${externalXmlFilePath}`); - throw ex; - } - } - secondaryManifest.removeUsesSdk(); // Don't let modules define elements. - - // Copy the manifest settings loaded from "tiapp.xml" and Titanium project's "./platform/android" directory. - // Since this is copied last, it will overwrite all XML settings made by modules up above. - // Note: The "customAndroidManifest" field is expected to be loaded/assigned in build.validate() method. - if (this.customAndroidManifest) { - secondaryManifest.copyFromAndroidManifest(this.customAndroidManifest); - } - // Write secondary "AndroidManifest.xml" if not empty. - if (!secondaryManifest.isEmpty()) { - // Make sure package name is set in so that ".ClassName" references in XML can be resolved. - secondaryManifest.setPackageName(this.appid); - - // Replace ${tiapp.properties['key']} placeholders in manifest. - secondaryManifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); - - // Do not allow developers to override the "configChanges" attribute on "TiBaseActivity" derived activities. - // Most devs don't set this right, causing UI to disappear when a config change occurs for a missing setting. - const tiActivityNames = [ - `.${this.classname}Activity`, - `${this.appid}.${this.classname}Activity`, - 'org.appcelerator.titanium.TiActivity', - 'org.appcelerator.titanium.TiTranslucentActivity', - 'org.appcelerator.titanium.TiCameraActivity', - 'org.appcelerator.titanium.TiCameraXActivity', - 'org.appcelerator.titanium.TiVideoActivity' - ]; - for (const activityName of tiActivityNames) { - secondaryManifest.removeActivityAttribute(activityName, 'android:configChanges'); + // Copy module's "./platform/android/AndroidManifest.xml" file if it exists. + const externalXmlFilePath = path.join(module.modulePath, 'platform', 'android', 'AndroidManifest.xml'); + try { + if (await fs.exists(externalXmlFilePath)) { + const externalManifest = await AndroidManifest.fromFilePath(externalXmlFilePath); + secondaryManifest.copyFromAndroidManifest(externalManifest); + } + } catch (ex) { + this.logger.error(`Unable to load file: ${externalXmlFilePath}`); + throw ex; + } } + secondaryManifest.removeUsesSdk(); // Don't let modules define elements. + + // Copy the manifest settings loaded from "tiapp.xml" and Titanium project's "./platform/android" directory. + // Since this is copied last, it will overwrite all XML settings made by modules up above. + // Note: The "customAndroidManifest" field is expected to be loaded/assigned in build.validate() method. + if (this.customAndroidManifest) { + secondaryManifest.copyFromAndroidManifest(this.customAndroidManifest); + } + + // Write secondary "AndroidManifest.xml" if not empty. + if (!secondaryManifest.isEmpty()) { + // Make sure package name is set in so that ".ClassName" references in XML can be resolved. + secondaryManifest.setPackageName(this.appid); + + // Replace ${tiapp.properties['key']} placeholders in manifest. + secondaryManifest.replaceTiPlaceholdersUsing(this.tiapp, this.appid); + + // Do not allow developers to override the "configChanges" attribute on "TiBaseActivity" derived activities. + // Most devs don't set this right, causing UI to disappear when a config change occurs for a missing setting. + const tiActivityNames = [ + `.${this.classname}Activity`, + `${this.appid}.${this.classname}Activity`, + 'org.appcelerator.titanium.TiActivity', + 'org.appcelerator.titanium.TiTranslucentActivity', + 'org.appcelerator.titanium.TiCameraActivity', + 'org.appcelerator.titanium.TiCameraXActivity', + 'org.appcelerator.titanium.TiVideoActivity' + ]; + for (const activityName of tiActivityNames) { + secondaryManifest.removeActivityAttribute(activityName, 'android:configChanges'); + } - // Apply "tools:replace" attributes to , , and attributes set by app. - // Avoids Google build errors if app's attributes conflict with attributes set by libraries. - // Note: Old Titanium build system (before gradle) didn't error out. So, this is for backward compatibility. - secondaryManifest.applyToolsReplace(); + // Apply "tools:replace" attributes to , , and attributes set by app. + // Avoids Google build errors if app's attributes conflict with attributes set by libraries. + // Note: Old Titanium build system (before gradle) didn't error out. So, this is for backward compatibility. + secondaryManifest.applyToolsReplace(); - // Create the "debug" and "release" subdirectories. - const debugDirPath = path.join(this.buildAppDir, 'src', 'debug'); - const releaseDirPath = path.join(this.buildAppDir, 'src', 'release'); - await fs.ensureDir(debugDirPath); - await fs.ensureDir(releaseDirPath); + // Create the "debug" and "release" subdirectories. + const debugDirPath = path.join(this.buildAppDir, 'src', 'debug'); + const releaseDirPath = path.join(this.buildAppDir, 'src', 'release'); + await fs.ensureDir(debugDirPath); + await fs.ensureDir(releaseDirPath); - // Save manifest to above subdirectories. - await secondaryManifest.writeToFilePath(path.join(debugDirPath, 'AndroidManifest.xml')); - await secondaryManifest.writeToFilePath(path.join(releaseDirPath, 'AndroidManifest.xml')); + // Save manifest to above subdirectories. + await secondaryManifest.writeToFilePath(path.join(debugDirPath, 'AndroidManifest.xml')); + await secondaryManifest.writeToFilePath(path.join(releaseDirPath, 'AndroidManifest.xml')); + } } -}; -AndroidBuilder.prototype.buildAppProject = async function buildAppProject() { - this.logger.info(__('Building app')); + async buildAppProject() { + this.logger.info('Building app'); - // Configure keystore digital signing info via temporary environment variables. - // Helps keep release key info a secret. The "build.gradle" will default to debug keystore if not provided. - if (this.keystore) { - process.env.TI_ANDROID_APP_KEYSTORE_FILE = this.keystore; - } - if (this.keystoreStorePassword) { - process.env.TI_ANDROID_APP_KEYSTORE_PASSWORD = this.keystoreStorePassword; - } - if (this.keystoreAlias && this.keystoreAlias.name) { - process.env.TI_ANDROID_APP_KEYSTORE_ALIAS_NAME = this.keystoreAlias.name; - } - if (this.keystoreKeyPassword) { - process.env.TI_ANDROID_APP_KEYSTORE_ALIAS_PASSWORD = this.keystoreKeyPassword; - } + // Configure keystore digital signing info via temporary environment variables. + // Helps keep release key info a secret. The "build.gradle" will default to debug keystore if not provided. + if (this.keystore) { + process.env.TI_ANDROID_APP_KEYSTORE_FILE = this.keystore; + } + if (this.keystoreStorePassword) { + process.env.TI_ANDROID_APP_KEYSTORE_PASSWORD = this.keystoreStorePassword; + } + if (this.keystoreAlias && this.keystoreAlias.name) { + process.env.TI_ANDROID_APP_KEYSTORE_ALIAS_NAME = this.keystoreAlias.name; + } + if (this.keystoreKeyPassword) { + process.env.TI_ANDROID_APP_KEYSTORE_ALIAS_PASSWORD = this.keystoreKeyPassword; + } - // Build the "app" project. - const gradlew = new GradleWrapper(this.buildDir); - gradlew.logger = this.logger; - if (this.allowDebugging) { - // Build a debug version of the APK. (Native code can be debugged via Android Studio.) - await gradlew.assembleDebug('app'); - } else { - // Build a release version of the APK. - await gradlew.assembleRelease('app'); - - // Create an "*.aab" app-bundle file of the app. - // Note: This is a Google Play publishing format. App-bundles cannot be ran on Android devices. - // Google's server will generate multiple APKs from this split by architecture and image density. - await gradlew.bundleRelease('app'); - - // Set path to the app-bundle file that was built up above. - // Our "package.js" event hook will later copy it to the developer's chosen destination directory. - this.aabFile = path.join(this.buildDir, 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab'); + // Build the "app" project. + const gradlew = new GradleWrapper(this.buildDir); + gradlew.logger = this.logger; + if (this.allowDebugging) { + // Build a debug version of the APK. (Native code can be debugged via Android Studio.) + await gradlew.assembleDebug('app'); + } else { + // Build a release version of the APK. + await gradlew.assembleRelease('app'); + + // Create an "*.aab" app-bundle file of the app. + // Note: This is a Google Play publishing format. App-bundles cannot be ran on Android devices. + // Google's server will generate multiple APKs from this split by architecture and image density. + await gradlew.bundleRelease('app'); + + // Set path to the app-bundle file that was built up above. + // Our "package.js" event hook will later copy it to the developer's chosen destination directory. + this.aabFile = path.join(this.buildDir, 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab'); + } + + // Verify that we can find the above built file(s). + if (!await fs.exists(this.apkFile)) { + throw new Error(`Failed to find built APK file: ${this.apkFile}`); + } + if (this.aabFile && !await fs.exists(this.aabFile)) { + throw new Error(`Failed to find built AAB file: ${this.aabFile}`); + } + } + + async writeBuildManifest() { + this.logger.info(`Writing build manifest: ${this.buildManifestFile.cyan}`); + + await new Promise((resolve) => { + this.cli.createHook('build.android.writeBuildManifest', this, function (manifest, cb) { + fs.ensureDirSync(this.buildDir); + fs.existsSync(this.buildManifestFile) && fs.unlinkSync(this.buildManifestFile); + fs.writeFile(this.buildManifestFile, JSON.stringify(this.buildManifest = manifest, null, '\t'), cb); + })({ + target: this.target, + deployType: this.deployType, + classname: this.classname, + platformPath: this.platformPath, + modulesHash: this.modulesHash, + gitHash: ti.manifest.githash, + outputDir: this.cli.argv['output-dir'], + name: this.tiapp.name, + id: this.tiapp.id, + publisher: this.tiapp.publisher, + url: this.tiapp.url, + version: this.tiapp.version, + description: this.tiapp.description, + copyright: this.tiapp.copyright, + guid: this.tiapp.guid, + icon: this.tiapp.icon, + fullscreen: this.tiapp.fullscreen, + navbarHidden: this.tiapp['navbar-hidden'], + skipJSMinification: !!this.cli.argv['skip-js-minify'], + encryptJS: this.encryptJS, + minSDK: this.minSDK, + targetSDK: this.targetSDK, + propertiesHash: this.propertiesHash, + activitiesHash: this.activitiesHash, + servicesHash: this.servicesHash + }, resolve); + }); } - // Verify that we can find the above built file(s). - if (!await fs.exists(this.apkFile)) { - throw new Error(`Failed to find built APK file: ${this.apkFile}`); - } - if (this.aabFile && !await fs.exists(this.aabFile)) { - throw new Error(`Failed to find built AAB file: ${this.aabFile}`); + createGradleWrapper(directoryPath) { + // Creates a gradle handling object for plugins such as hyperloop. + return new GradleWrapper(directoryPath); } -}; - -AndroidBuilder.prototype.writeBuildManifest = async function writeBuildManifest() { - this.logger.info(__('Writing build manifest: %s', this.buildManifestFile.cyan)); - - await new Promise((resolve) => { - this.cli.createHook('build.android.writeBuildManifest', this, function (manifest, cb) { - fs.ensureDirSync(this.buildDir); - fs.existsSync(this.buildManifestFile) && fs.unlinkSync(this.buildManifestFile); - fs.writeFile(this.buildManifestFile, JSON.stringify(this.buildManifest = manifest, null, '\t'), cb); - })({ - target: this.target, - deployType: this.deployType, - classname: this.classname, - platformPath: this.platformPath, - modulesHash: this.modulesHash, - gitHash: ti.manifest.githash, - outputDir: this.cli.argv['output-dir'], - name: this.tiapp.name, - id: this.tiapp.id, - publisher: this.tiapp.publisher, - url: this.tiapp.url, - version: this.tiapp.version, - description: this.tiapp.description, - copyright: this.tiapp.copyright, - guid: this.tiapp.guid, - icon: this.tiapp.icon, - fullscreen: this.tiapp.fullscreen, - navbarHidden: this.tiapp['navbar-hidden'], - skipJSMinification: !!this.cli.argv['skip-js-minify'], - encryptJS: this.encryptJS, - minSDK: this.minSDK, - targetSDK: this.targetSDK, - propertiesHash: this.propertiesHash, - activitiesHash: this.activitiesHash, - servicesHash: this.servicesHash - }, resolve); - }); -}; - -AndroidBuilder.prototype.createGradleWrapper = function createGradleWrapper(directoryPath) { - // Creates a gradle handling object for plugins such as hyperloop. - return new GradleWrapper(directoryPath); -}; +} // create the builder instance and expose the public api -(function (androidBuilder) { - exports.config = androidBuilder.config.bind(androidBuilder); - exports.validate = androidBuilder.validate.bind(androidBuilder); - exports.run = androidBuilder.run.bind(androidBuilder); -}(new AndroidBuilder(module))); +const builder = new AndroidBuilder(module); +export const config = builder.config.bind(builder); +export const validate = builder.validate.bind(builder); +export const run = builder.run.bind(builder); diff --git a/android/cli/commands/_buildModule.js b/android/cli/commands/_buildModule.js index 30e6f003cf5..4dde7980f2f 100644 --- a/android/cli/commands/_buildModule.js +++ b/android/cli/commands/_buildModule.js @@ -11,927 +11,942 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const androidDetect = require('../lib/detect').detect, - AndroidManifest = require('../lib/android-manifest'), - appc = require('node-appc'), - archiver = require('archiver'), - Builder = require('node-titanium-sdk/lib/builder'), - ejs = require('ejs'), - fields = require('fields'), - fs = require('fs-extra'), - GradleWrapper = require('../lib/gradle-wrapper'), - markdown = require('markdown').markdown, - path = require('path'), - temp = require('temp'), - tiappxml = require('node-titanium-sdk/lib/tiappxml'), - util = require('util'), - semver = require('semver'), - spawn = require('child_process').spawn, // eslint-disable-line security/detect-child-process - - __ = appc.i18n(__dirname).__, - version = appc.version; - -function AndroidModuleBuilder() { - Builder.apply(this, arguments); - - this.requiredArchitectures = this.packageJson.architectures; - this.compileSdkVersion = this.packageJson.compileSDKVersion; // this should always be >= maxSupportedApiLevel - this.minSupportedApiLevel = parseInt(this.packageJson.minSDKVersion); - this.minTargetApiLevel = parseInt(version.parseMin(this.packageJson.vendorDependencies['android sdk'])); - this.maxSupportedApiLevel = parseInt(version.parseMax(this.packageJson.vendorDependencies['android sdk'])); -} - -util.inherits(AndroidModuleBuilder, Builder); - -/** - * Migrates an existing module with an outdated "apiversion" in the manifest to the latest one. - * It takes care of migrating the "apiversion", "version", "minsdk" and "architecture" properties. - * - * @return {Promise} - */ -AndroidModuleBuilder.prototype.migrate = async function migrate() { - const cliModuleAPIVersion = this.cli.sdk && this.cli.sdk.manifest && this.cli.sdk.manifest.moduleAPIVersion && this.cli.sdk.manifest.moduleAPIVersion.android; - const cliSDKVersion = this.cli.sdk.manifest.version; - const manifestSDKVersion = this.manifest.minsdk; - const manifestModuleAPIVersion = this.manifest.apiversion; - const manifestTemplateFile = path.join(this.platformPath, 'templates', 'module', 'default', 'template', 'android', 'manifest.ejs'); - let newVersion = semver.inc(this.manifest.version, 'major'); - - // Determine if the "manifest" file's "apiversion" needs updating. - let isApiVersionUpdateRequired = false; - if (cliModuleAPIVersion) { - isApiVersionUpdateRequired = (this.manifest.apiversion !== cliModuleAPIVersion); +import { detect as androidDetect } from '../lib/detect.js'; +import { AndroidManifest } from '../lib/android-manifest.js'; +import appc from 'node-appc'; +import archiver from 'archiver'; +import Builder from 'node-titanium-sdk/lib/builder.js'; +import ejs from 'ejs'; +import fields from 'fields'; +import fs from 'fs-extra'; +import { GradleWrapper } from '../lib/gradle-wrapper.js'; +import markdown from 'markdown'; +import path from 'node:path'; +import temp from 'temp'; +import tiappxml from 'node-titanium-sdk/lib/tiappxml.js'; +import util from 'node:util'; +import semver from 'semver'; +import spawn from 'node:child_process'; + +const version = appc.version; + +export class AndroidModuleBuilder extends Builder { + constructor(buildModule) { + super(buildModule); + + this.requiredArchitectures = this.packageJson.architectures; + this.compileSdkVersion = this.packageJson.compileSDKVersion; // this should always be >= maxSupportedApiLevel + this.minSupportedApiLevel = parseInt(this.packageJson.minSDKVersion); + this.minTargetApiLevel = parseInt(version.parseMin(this.packageJson.vendorDependencies['android sdk'])); + this.maxSupportedApiLevel = parseInt(version.parseMax(this.packageJson.vendorDependencies['android sdk'])); } - // Determine if the "manifest" file's "minsdk" needs updating. - // As of Titanium 9.0.0, modules are built as AARs to an "m2repository". Not supported on older Titanium versions. - let isMinSdkUpdateRequired = false; - const minSupportedSdkVersionMajorNumber = 9; - const minSupportedSdkVersionString = '9.0.0'; - if (!this.manifest.minsdk || (parseInt(this.manifest.minsdk.split('.')[0]) < minSupportedSdkVersionMajorNumber)) { - isMinSdkUpdateRequired = true; - } + /** + * Migrates an existing module with an outdated "apiversion" in the manifest to the latest one. + * It takes care of migrating the "apiversion", "version", "minsdk" and "architecture" properties. + * + * @return {Promise} + */ + async migrate() { + const cliModuleAPIVersion = this.cli.sdk && this.cli.sdk.manifest && this.cli.sdk.manifest.moduleAPIVersion && this.cli.sdk.manifest.moduleAPIVersion.android; + const cliSDKVersion = this.cli.sdk.manifest.version; + const manifestSDKVersion = this.manifest.minsdk; + const manifestModuleAPIVersion = this.manifest.apiversion; + const manifestTemplateFile = path.join(this.platformPath, 'templates', 'module', 'default', 'template', 'android', 'manifest.ejs'); + let newVersion = semver.inc(this.manifest.version, 'major'); + + // Determine if the "manifest" file's "apiversion" needs updating. + let isApiVersionUpdateRequired = false; + if (cliModuleAPIVersion) { + isApiVersionUpdateRequired = (this.manifest.apiversion !== cliModuleAPIVersion); + } - // Do not continue if manifest doesn't need updating. (Everything is okay.) - if (!isApiVersionUpdateRequired && !isMinSdkUpdateRequired) { - return; - } + // Determine if the "manifest" file's "minsdk" needs updating. + // As of Titanium 9.0.0, modules are built as AARs to an "m2repository". Not supported on older Titanium versions. + let isMinSdkUpdateRequired = false; + const minSupportedSdkVersionMajorNumber = 9; + const minSupportedSdkVersionString = '9.0.0'; + if (!this.manifest.minsdk || (parseInt(this.manifest.minsdk.split('.')[0]) < minSupportedSdkVersionMajorNumber)) { + isMinSdkUpdateRequired = true; + } - const logger = this.logger; - if (!this.cli.argv.prompt) { - if (isApiVersionUpdateRequired) { - logger.error(__('The module manifest apiversion is currently set to %s', manifestModuleAPIVersion)); - logger.error(__('Titanium SDK %s Android module apiversion is at %s', cliSDKVersion, cliModuleAPIVersion)); - logger.error(__('Please update module manifest apiversion to match Titanium SDK module apiversion')); - logger.error(__('and the minsdk to at least %s', minSupportedSdkVersionString)); - } else { - logger.error(__('The module "manifest" file\'s minsdk is currently set to %s', this.manifest.minsdk)); - logger.error(__('Please update the file\'s minsdk to at least version %s', minSupportedSdkVersionString)); + // Do not continue if manifest doesn't need updating. (Everything is okay.) + if (!isApiVersionUpdateRequired && !isMinSdkUpdateRequired) { + return; } - process.exit(1); - } - await new Promise((resolve, reject) => { - let titleMessage; - if (isApiVersionUpdateRequired) { - titleMessage = __( - 'Detected Titanium %s that requires API-level %s, but the module currently only supports %s and API-level %s.', - cliSDKVersion, cliModuleAPIVersion, manifestSDKVersion, manifestModuleAPIVersion); - } else { - titleMessage = __( - 'Modules built with Titanium %s cannot support Titanium versions older than %s. The "manifest" file\'s minsdk must be updated.', - cliSDKVersion, minSupportedSdkVersionString); + const logger = this.logger; + if (!this.cli.argv.prompt) { + if (isApiVersionUpdateRequired) { + logger.error(`The module manifest apiversion is currently set to ${manifestModuleAPIVersion}`); + logger.error(`Titanium SDK ${cliSDKVersion} Android module apiversion is at ${cliModuleAPIVersion}`); + logger.error('Please update module manifest apiversion to match Titanium SDK module apiversion'); + logger.error(`and the minsdk to at least ${minSupportedSdkVersionString}`); + } else { + logger.error(`The module "manifest" file's minsdk is currently set to ${this.manifest.minsdk}`); + logger.error(`Please update the file's minsdk to at least version ${minSupportedSdkVersionString}`); + } + process.exit(1); } - fields.select({ - title: titleMessage, - promptLabel: __('Do you want to migrate your module now?'), - default: 'yes', - display: 'prompt', - relistOnError: true, - complete: true, - suggest: true, - options: [ '__y__es', '__n__o' ] - }).prompt((err, value) => { - if (err) { - reject(err); - return; + + await new Promise((resolve, reject) => { + let titleMessage; + if (isApiVersionUpdateRequired) { + titleMessage = `Detected Titanium ${ + cliSDKVersion + } that requires API-level ${ + cliModuleAPIVersion + }, but the module currently only supports ${ + manifestSDKVersion + } and API-level ${ + manifestModuleAPIVersion + }.`; + } else { + titleMessage = `Modules built with Titanium ${ + cliSDKVersion + } cannot support Titanium versions older than ${ + minSupportedSdkVersionString + }. The "manifest" file's minsdk must be updated.`; } + fields.select({ + title: titleMessage, + promptLabel: 'Do you want to migrate your module now?', + default: 'yes', + display: 'prompt', + relistOnError: true, + complete: true, + suggest: true, + options: [ '__y__es', '__n__o' ] + }).prompt((err, value) => { + if (err) { + reject(err); + return; + } + + if (value !== 'yes') { + logger.error('Please update the module\'s "manifest" file in order to build it.'); + process.exit(1); + } + + resolve(); + }); + }); - if (value !== 'yes') { - logger.error(__('Please update the module\'s "manifest" file in order to build it.')); + this.logger.info('Migrating module manifest...'); + + // If a version is "1.0" instead of "1.0.0", semver currently fails. Work around it for now! + if (!newVersion) { + this.logger.warn(`Detected non-semantic version (${this.manifest.version}), will try to repair it!`); + try { + const semanticVersion = appc.version.format(this.manifest.version, 3, 3, true); + newVersion = semver.inc(semanticVersion, 'major'); + } catch (err) { + this.logger.error('Unable to migrate version for you. Please update it manually by using a semantic version like "1.0.0" and try the migration again.'); process.exit(1); } + } - resolve(); + // Update the "apiversion" to the CLI API-version + this.logger.info(`Setting ${'apiversion'.cyan} to ${cliModuleAPIVersion.cyan}`); + this.manifest.apiversion = cliModuleAPIVersion; + + // Update the "minsdk" to the required CLI SDK-version + this.logger.info(`Setting ${'minsdk'.cyan} to ${minSupportedSdkVersionString.cyan}`); + this.manifest.minsdk = minSupportedSdkVersionString; + + // Update the "apiversion" to the next major + this.logger.info(`Bumping version from ${this.manifest.version.cyan} to ${newVersion.cyan}`); + this.manifest.version = newVersion; + + // Add our new architecture(s) + this.manifest.architectures = this.requiredArchitectures.join(' '); + + // Pre-fill placeholders + let manifestContent = await fs.readFile(manifestTemplateFile); + manifestContent = ejs.render(manifestContent.toString(), { + moduleName: this.manifest.name, + moduleId: this.manifest.moduleid, + platform: this.manifest.platform, + tisdkVersion: this.manifest.minsdk, + guid: this.manifest.guid, + author: this.manifest.author, + publisher: this.manifest.author // The publisher does not have an own key in the manifest but can be different. Will override below }); - }); - this.logger.info(__('Migrating module manifest ...')); + // Migrate missing keys which don't have a placeholder (version, license, copyright & publisher) + manifestContent = manifestContent.replace(/version.*/, 'version: ' + this.manifest.version); + manifestContent = manifestContent.replace(/license.*/, 'license: ' + this.manifest.license); + manifestContent = manifestContent.replace(/copyright.*/, 'copyright: ' + this.manifest.copyright); + manifestContent = manifestContent.replace(/description.*/, 'description: ' + this.manifest.description); - // If a version is "1.0" instead of "1.0.0", semver currently fails. Work around it for now! - if (!newVersion) { - this.logger.warn(__('Detected non-semantic version (%s), will try to repair it!', this.manifest.version)); - try { - const semanticVersion = appc.version.format(this.manifest.version, 3, 3, true); - newVersion = semver.inc(semanticVersion, 'major'); - } catch (err) { - this.logger.error(__('Unable to migrate version for you. Please update it manually by using a semantic version like "1.0.0" and try the migration again.')); - process.exit(1); - } - } + // Make a backup of the old file in case something goes wrong + this.logger.info(`Backing up old manifest to ${'manifest.bak'.cyan}`); + await fs.rename(path.join(this.projectDir, 'manifest'), path.join(this.projectDir, 'manifest.bak')); - // Update the "apiversion" to the CLI API-version - this.logger.info(__('Setting %s to %s', 'apiversion'.cyan, cliModuleAPIVersion.cyan)); - this.manifest.apiversion = cliModuleAPIVersion; - - // Update the "minsdk" to the required CLI SDK-version - this.logger.info(__('Setting %s to %s', 'minsdk'.cyan, minSupportedSdkVersionString.cyan)); - this.manifest.minsdk = minSupportedSdkVersionString; - - // Update the "apiversion" to the next major - this.logger.info(__('Bumping version from %s to %s', this.manifest.version.cyan, newVersion.cyan)); - this.manifest.version = newVersion; - - // Add our new architecture(s) - this.manifest.architectures = this.requiredArchitectures.join(' '); - - // Pre-fill placeholders - let manifestContent = await fs.readFile(manifestTemplateFile); - manifestContent = ejs.render(manifestContent.toString(), { - moduleName: this.manifest.name, - moduleId: this.manifest.moduleid, - platform: this.manifest.platform, - tisdkVersion: this.manifest.minsdk, - guid: this.manifest.guid, - author: this.manifest.author, - publisher: this.manifest.author // The publisher does not have an own key in the manifest but can be different. Will override below - }); - - // Migrate missing keys which don't have a placeholder (version, license, copyright & publisher) - manifestContent = manifestContent.replace(/version.*/, 'version: ' + this.manifest.version); - manifestContent = manifestContent.replace(/license.*/, 'license: ' + this.manifest.license); - manifestContent = manifestContent.replace(/copyright.*/, 'copyright: ' + this.manifest.copyright); - manifestContent = manifestContent.replace(/description.*/, 'description: ' + this.manifest.description); - - // Make a backup of the old file in case something goes wrong - this.logger.info(__('Backing up old manifest to %s', 'manifest.bak'.cyan)); - await fs.rename(path.join(this.projectDir, 'manifest'), path.join(this.projectDir, 'manifest.bak')); - - // Write the new manifest file - this.logger.info(__('Writing new manifest')); - await fs.writeFile(path.join(this.projectDir, 'manifest'), manifestContent); - - this.logger.info(__('')); - this.logger.info(__('Migration completed! Building module ...')); -}; - -AndroidModuleBuilder.prototype.validate = function validate(logger, config, cli) { - Builder.prototype.config.apply(this, arguments); - Builder.prototype.validate.apply(this, arguments); - - return function (finished) { - this.projectDir = cli.argv['project-dir']; - this.buildOnly = cli.argv['build-only']; - this.target = cli.argv['target']; - this.deviceId = cli.argv['device-id']; - - this.cli = cli; - this.logger = logger; - fields.setup({ colors: cli.argv.colors }); - - this.manifest = this.cli.manifest; - - // detect android environment - androidDetect(config, { packageJson: this.packageJson }, function (androidInfo) { - this.androidInfo = androidInfo; - - const targetSDKMap = { - - // placeholder for gradle to use - [this.compileSdkVersion]: { - sdk: this.compileSdkVersion - } - }; - Object.keys(this.androidInfo.targets).forEach(function (id) { - var t = this.androidInfo.targets[id]; - if (t.type === 'platform') { - targetSDKMap[t.id.replace('android-', '')] = t; - } - }, this); + // Write the new manifest file + this.logger.info('Writing new manifest'); + await fs.writeFile(path.join(this.projectDir, 'manifest'), manifestContent); - // check the Android SDK we require to build exists - this.androidCompileSDK = targetSDKMap[this.compileSdkVersion]; + this.logger.info('\nMigration completed! Building module...'); + } - // if no target sdk, then default to most recent supported/installed - if (!this.targetSDK) { - this.targetSDK = this.maxSupportedApiLevel; - } - this.androidTargetSDK = targetSDKMap[this.targetSDK]; + validate(logger, config, cli) { + super.config(logger, config, cli); + super.validate(logger, config, cli); - if (!this.androidTargetSDK) { - this.androidTargetSDK = { - sdk: this.targetSDK - }; - } + return function (finished) { + this.projectDir = cli.argv['project-dir']; + this.buildOnly = cli.argv['build-only']; + this.target = cli.argv['target']; + this.deviceId = cli.argv['device-id']; - if (this.targetSDK < this.minSDK) { - logger.error(__('Target Android SDK version must be %s or newer', this.minSDK) + '\n'); - process.exit(1); - } + this.cli = cli; + this.logger = logger; + fields.setup({ colors: cli.argv.colors }); - if (this.maxSDK && this.maxSDK < this.targetSDK) { - logger.error(__('Maximum Android SDK version must be greater than or equal to the target SDK %s, but is currently set to %s', this.targetSDK, this.maxSDK) + '\n'); - process.exit(1); - } + this.manifest = this.cli.manifest; - if (this.maxSupportedApiLevel && this.targetSDK > this.maxSupportedApiLevel) { - // print warning that version this.targetSDK is not tested - logger.warn(__('Building with Android SDK %s which hasn\'t been tested against Titanium SDK %s', ('' + this.targetSDK).cyan, this.titaniumSdkVersion)); - } + // detect android environment + androidDetect(config, { packageJson: this.packageJson }, function (androidInfo) { + this.androidInfo = androidInfo; - // get javac params - this.javacMaxMemory = config.get('android.javac.maxMemory', '3072M'); + const targetSDKMap = { - // TODO remove in the next SDK - if (cli.timodule.properties['android.javac.maxmemory'] && cli.timodule.properties['android.javac.maxmemory'].value) { - logger.error(__('android.javac.maxmemory is deprecated and will be removed in the next version. Please use android.javac.maxMemory') + '\n'); - this.javacMaxMemory = cli.timodule.properties['android.javac.maxmemory'].value; - } + // placeholder for gradle to use + [this.compileSdkVersion]: { + sdk: this.compileSdkVersion + } + }; + Object.keys(this.androidInfo.targets).forEach(function (id) { + var t = this.androidInfo.targets[id]; + if (t.type === 'platform') { + targetSDKMap[t.id.replace('android-', '')] = t; + } + }, this); + + // check the Android SDK we require to build exists + this.androidCompileSDK = targetSDKMap[this.compileSdkVersion]; + + // if no target sdk, then default to most recent supported/installed + if (!this.targetSDK) { + this.targetSDK = this.maxSupportedApiLevel; + } + this.androidTargetSDK = targetSDKMap[this.targetSDK]; - if (cli.timodule.properties['android.javac.maxMemory'] && cli.timodule.properties['android.javac.maxMemory'].value) { - this.javacMaxMemory = cli.timodule.properties['android.javac.maxMemory'].value; - } + if (!this.androidTargetSDK) { + this.androidTargetSDK = { + sdk: this.targetSDK + }; + } - // detect java development kit - appc.jdk.detect(config, null, function (jdkInfo) { - if (!jdkInfo.version) { - logger.error(__('Unable to locate the Java Development Kit') + '\n'); - logger.log(__('You can specify the location by setting the %s environment variable.', 'JAVA_HOME'.cyan) + '\n'); + if (this.targetSDK < this.minSDK) { + logger.error(`Target Android SDK version must be ${this.minSDK} or newer\n`); process.exit(1); } - if (!version.satisfies(jdkInfo.version, this.packageJson.vendorDependencies.java)) { - logger.error(__('JDK version %s detected, but only version %s is supported', jdkInfo.version, this.packageJson.vendorDependencies.java) + '\n'); + if (this.maxSDK && this.maxSDK < this.targetSDK) { + logger.error(`Maximum Android SDK version must be greater than or equal to the target SDK ${ + this.targetSDK + }, but is currently set to ${ + this.maxSDK + }\n`); process.exit(1); } - this.jdkInfo = jdkInfo; + if (this.maxSupportedApiLevel && this.targetSDK > this.maxSupportedApiLevel) { + // print warning that version this.targetSDK is not tested + logger.warn(`Building with Android SDK ${ + String(this.targetSDK).cyan + } which hasn't been tested against Titanium SDK ${ + this.titaniumSdkVersion + }`); + } + + // get javac params + this.javacMaxMemory = config.get('android.javac.maxMemory', '3072M'); + + // TODO remove in the next SDK + if (cli.timodule.properties['android.javac.maxmemory'] && cli.timodule.properties['android.javac.maxmemory'].value) { + logger.error('android.javac.maxmemory is deprecated and will be removed in the next version. Please use android.javac.maxMemory\n'); + this.javacMaxMemory = cli.timodule.properties['android.javac.maxmemory'].value; + } + + if (cli.timodule.properties['android.javac.maxMemory'] && cli.timodule.properties['android.javac.maxMemory'].value) { + this.javacMaxMemory = cli.timodule.properties['android.javac.maxMemory'].value; + } - finished(); + // detect java development kit + appc.jdk.detect(config, null, function (jdkInfo) { + if (!jdkInfo.version) { + logger.error('Unable to locate the Java Development Kit\n'); + logger.log(`You can specify the location by setting the ${'JAVA_HOME'.cyan} environment variable.\n`); + process.exit(1); + } + + if (!version.satisfies(jdkInfo.version, this.packageJson.vendorDependencies.java)) { + logger.error(`JDK version ${ + jdkInfo.version + } detected, but only version ${ + this.packageJson.vendorDependencies.java + } is supported\n`); + process.exit(1); + } + + this.jdkInfo = jdkInfo; + + finished(); + }.bind(this)); }.bind(this)); - }.bind(this)); - }.bind(this); -}; + }.bind(this); + } -AndroidModuleBuilder.prototype.run = async function run(logger, config, cli, finished) { - try { - // Call the base builder's run() method. - Builder.prototype.run.apply(this, arguments); + async run(logger, config, cli, finished) { + try { + // Call the base builder's run() method. + super.run(logger, config, cli, finished); - // Notify plugins that we're about to begin. - await new Promise((resolve) => { - cli.emit('build.module.pre.construct', this, resolve); - }); + // Notify plugins that we're about to begin. + await new Promise((resolve) => { + cli.emit('build.module.pre.construct', this, resolve); + }); - // Update module's config files, if necessary. - await this.migrate(); + // Update module's config files, if necessary. + await this.migrate(); - // Initialize build variables and directory. - await this.initialize(); - await this.loginfo(); - await this.cleanup(); + // Initialize build variables and directory. + await this.initialize(); + await this.loginfo(); + await this.cleanup(); - // Notify plugins that we're prepping to compile. - await new Promise((resolve) => { - cli.emit('build.module.pre.compile', this, resolve); - }); + // Notify plugins that we're prepping to compile. + await new Promise((resolve) => { + cli.emit('build.module.pre.compile', this, resolve); + }); - // Update module files such as "manifest" if needed. - await this.updateModuleFiles(); + // Update module files such as "manifest" if needed. + await this.updateModuleFiles(); - // Generate all gradle project files. - await this.generateRootProjectFiles(); - await this.generateModuleProject(); + // Generate all gradle project files. + await this.generateRootProjectFiles(); + await this.generateModuleProject(); - // Build the library and output it to "dist" directory. - await this.buildModuleProject(); - // Notify plugins that the build is done. - await new Promise((resolve) => { - cli.emit('build.module.post.compile', this, resolve); - }); + // Build the library and output it to "dist" directory. + await this.buildModuleProject(); + // Notify plugins that the build is done. + await new Promise((resolve) => { + cli.emit('build.module.post.compile', this, resolve); + }); - await this.packageZip(); + await this.packageZip(); - await new Promise((resolve) => { - cli.emit('build.module.finalize', this, resolve); - }); + await new Promise((resolve) => { + cli.emit('build.module.finalize', this, resolve); + }); + + // Run the built module via "example" project. + await this.runModule(cli); + } catch (err) { + // Failed to build module. Print the error message and stack trace (if possible). + // Note: "err" can be whatever type (including undefined) that was passed into Promise.reject(). + if (err instanceof Error) { + this.logger.error(err.stack || err.message); + } else if ((typeof err === 'string') && (err.length > 0)) { + this.logger.error(err); + } else { + this.logger.error('Build failed. Reason: Unknown'); + } - // Run the built module via "example" project. - await this.runModule(cli); - } catch (err) { - // Failed to build module. Print the error message and stack trace (if possible). - // Note: "err" can be whatever type (including undefined) that was passed into Promise.reject(). - if (err instanceof Error) { - this.logger.error(err.stack || err.message); - } else if ((typeof err === 'string') && (err.length > 0)) { - this.logger.error(err); - } else { - this.logger.error('Build failed. Reason: Unknown'); + // Exit out with an error. + if (finished) { + finished(err); + } else { + process.exit(1); + } } - // Exit out with an error. + // We're done. Invoke optional callback if provided. if (finished) { - finished(err); - } else { - process.exit(1); + finished(); } } - // We're done. Invoke optional callback if provided. - if (finished) { - finished(); - } -}; - -AndroidModuleBuilder.prototype.dirWalker = async function dirWalker(directoryPath, callback) { - const fileNameArray = await fs.readdir(directoryPath); - for (const fileName of fileNameArray) { - const filePath = path.join(directoryPath, fileName); - if ((await fs.stat(filePath)).isDirectory()) { - await this.dirWalker(filePath, callback); - } else { - callback(filePath, fileName); + async dirWalker(directoryPath, callback) { + const fileNameArray = await fs.readdir(directoryPath); + for (const fileName of fileNameArray) { + const filePath = path.join(directoryPath, fileName); + if ((await fs.stat(filePath)).isDirectory()) { + await this.dirWalker(filePath, callback); + } else { + callback(filePath, fileName); + } } } -}; -AndroidModuleBuilder.prototype.initialize = async function initialize() { - // Create a "tiSymbols" dictionary. It's needed by our "process-js-task" node module. - this.tiSymbols = {}; + async initialize() { + // Create a "tiSymbols" dictionary. It's needed by our "process-js-task" node module. + this.tiSymbols = {}; - // Fetch the module's "manifest" property file. - this.manifestFile = path.join(this.projectDir, 'manifest'); + // Fetch the module's "manifest" property file. + this.manifestFile = path.join(this.projectDir, 'manifest'); - // Get the paths to the module's main directories. - // Folders under the "android" subdirectory take precedence over the ones in the root directory. - const getPathForProjectDirName = async (directoryName) => { - let directoryPath = path.join(this.projectDir, directoryName); - if (!await fs.exists(directoryPath)) { - directoryPath = path.join(this.projectDir, '..', directoryName); + // Get the paths to the module's main directories. + // Folders under the "android" subdirectory take precedence over the ones in the root directory. + const getPathForProjectDirName = async (directoryName) => { + let directoryPath = path.join(this.projectDir, directoryName); + if (!await fs.exists(directoryPath)) { + directoryPath = path.join(this.projectDir, '..', directoryName); + } + return directoryPath; + }; + this.assetsDir = await getPathForProjectDirName('assets'); + this.documentationDir = await getPathForProjectDirName('documentation'); + this.exampleDir = await getPathForProjectDirName('example'); + this.platformDir = await getPathForProjectDirName('platform'); + this.resourcesDir = await getPathForProjectDirName('Resources'); + + // Fetch the "timodule.xml" file and load it. + // Provides Android specific info such as "AndroidManfiest.xml" elements and module dependencies. + this.timoduleXmlFile = path.join(this.projectDir, 'timodule.xml'); + if (await fs.exists(this.timoduleXmlFile)) { + this.timodule = new tiappxml(this.timoduleXmlFile); } - return directoryPath; - }; - this.assetsDir = await getPathForProjectDirName('assets'); - this.documentationDir = await getPathForProjectDirName('documentation'); - this.exampleDir = await getPathForProjectDirName('example'); - this.platformDir = await getPathForProjectDirName('platform'); - this.resourcesDir = await getPathForProjectDirName('Resources'); - - // Fetch the "timodule.xml" file and load it. - // Provides Android specific info such as "AndroidManfiest.xml" elements and module dependencies. - this.timoduleXmlFile = path.join(this.projectDir, 'timodule.xml'); - if (await fs.exists(this.timoduleXmlFile)) { - this.timodule = new tiappxml(this.timoduleXmlFile); + + // Legacy "/android/libs" directory used before Titanium 9.0.0. + // - Module devs used to put their C/C++ "*.so" dependencies here. (Now goes to "/platform/android/jniLibs".) + // - Old build system used to output module's built "*.so" libraries here. (Now packaged in built AAR file.) + this.libsDir = path.join(this.projectDir, 'libs'); + + // Set up build directory paths. + this.buildDir = path.join(this.projectDir, 'build'); + this.buildModuleDir = path.join(this.buildDir, 'module'); + + // The output directory where a zip of the built module will go. + this.distDir = path.join(this.projectDir, 'dist'); + + // The SDK's module template directory path. + this.moduleTemplateDir = path.join(this.platformPath, 'templates', 'module', 'generated'); + } + + async loginfo() { + this.logger.info(`Assets Dir: ${this.assetsDir.cyan}`); + this.logger.info(`Documentation Dir: ${this.documentationDir.cyan}`); + this.logger.info(`Example Dir: ${this.exampleDir.cyan}`); + this.logger.info(`Platform Dir: ${this.platformDir.cyan}`); + this.logger.info(`Resources Dir: ${this.resourcesDir.cyan}`); } - // Legacy "/android/libs" directory used before Titanium 9.0.0. - // - Module devs used to put their C/C++ "*.so" dependencies here. (Now goes to "/platform/android/jniLibs".) - // - Old build system used to output module's built "*.so" libraries here. (Now packaged in built AAR file.) - this.libsDir = path.join(this.projectDir, 'libs'); - - // Set up build directory paths. - this.buildDir = path.join(this.projectDir, 'build'); - this.buildModuleDir = path.join(this.buildDir, 'module'); - - // The output directory where a zip of the built module will go. - this.distDir = path.join(this.projectDir, 'dist'); - - // The SDK's module template directory path. - this.moduleTemplateDir = path.join(this.platformPath, 'templates', 'module', 'generated'); -}; - -AndroidModuleBuilder.prototype.loginfo = async function loginfo() { - this.logger.info(__('Assets Dir: %s', this.assetsDir.cyan)); - this.logger.info(__('Documentation Dir: %s', this.documentationDir.cyan)); - this.logger.info(__('Example Dir: %s', this.exampleDir.cyan)); - this.logger.info(__('Platform Dir: %s', this.platformDir.cyan)); - this.logger.info(__('Resources Dir: %s', this.resourcesDir.cyan)); -}; - -AndroidModuleBuilder.prototype.cleanup = async function cleanup() { - // Clean last packaged build in "dist" directory in case this build fails. - await fs.emptyDir(this.distDir); - await fs.emptyDir(this.buildDir); - - // Delete entire "build" directory tree if we can't find a gradle "module" project directory under it. - // This assumes last built module was using older version of Titanium that did not support gradle. - // Otherwise, keep gradle project files so that we can do an incremental build. - const hasGradleModuleDir = await fs.exists(path.join(this.buildDir, 'module')); - if (!hasGradleModuleDir) { + async cleanup() { + // Clean last packaged build in "dist" directory in case this build fails. + await fs.emptyDir(this.distDir); await fs.emptyDir(this.buildDir); + + // Delete entire "build" directory tree if we can't find a gradle "module" project directory under it. + // This assumes last built module was using older version of Titanium that did not support gradle. + // Otherwise, keep gradle project files so that we can do an incremental build. + const hasGradleModuleDir = await fs.exists(path.join(this.buildDir, 'module')); + if (!hasGradleModuleDir) { + await fs.emptyDir(this.buildDir); + } + + // Delete this module's last built "*.so" libraries from "libs" directory. + // Do not delete all files from "libs". Some modules put 3rd party "*.so" library dependencies there. + if (await fs.exists(this.libsDir)) { + const MODULE_LIB_FILE_NAME = `lib${this.manifest.moduleid}.so`; + await this.dirWalker(this.libsDir, (filePath, fileName) => { + if (fileName === MODULE_LIB_FILE_NAME) { + this.logger.debug(`Removing ${filePath.cyan}`); + fs.removeSync(filePath); + } + }); + } } - // Delete this module's last built "*.so" libraries from "libs" directory. - // Do not delete all files from "libs". Some modules put 3rd party "*.so" library dependencies there. - if (await fs.exists(this.libsDir)) { - const MODULE_LIB_FILE_NAME = `lib${this.manifest.moduleid}.so`; - await this.dirWalker(this.libsDir, (filePath, fileName) => { - if (fileName === MODULE_LIB_FILE_NAME) { - this.logger.debug(__('Removing %s', filePath.cyan)); - fs.removeSync(filePath); + async updateModuleFiles() { + // Add empty "build.gradle" template file to project folder if missing. Used to define library dependencies. + // Note: Appcelerator Studio looks for this file to determine if this is an Android module project. + const buildGradleFileName = 'build.gradle'; + const buildGradleFilePath = path.join(this.projectDir, buildGradleFileName); + if (!await fs.exists(buildGradleFilePath)) { + await fs.copyFile( + path.join(this.platformPath, 'templates', 'module', 'default', 'template', 'android', buildGradleFileName), + buildGradleFilePath); + } + + // Determine if "assets" directory contains at least 1 JavaScript file. + let hasJSFile = false; + if (await fs.exists(this.assetsDir)) { + await this.dirWalker(this.assetsDir, (filePath) => { + if (path.extname(filePath).toLowerCase() === '.js') { + hasJSFile = true; + } + }); + } + + // If JS file was found, then change module's "manifest" file setting "commonjs" to true. + if (hasJSFile && !this.manifest.commonjs) { + let wasFound = false; + let manifestContents = await fs.readFile(this.manifestFile); + manifestContents = manifestContents.toString().replace(/^commonjs:\s*.+$/mg, () => { + wasFound = true; + return 'commonjs: true'; + }); + if (!wasFound) { + manifestContents = manifestContents.trim() + '\ncommonjs: true\n'; } - }); + await fs.writeFile(this.manifestFile, manifestContents); + this.manifest.commonjs = true; + this.logger.info('Manifest re-written to set commonjs value'); + } } -}; - -AndroidModuleBuilder.prototype.updateModuleFiles = async function updateModuleFiles() { - // Add empty "build.gradle" template file to project folder if missing. Used to define library dependencies. - // Note: Appcelerator Studio looks for this file to determine if this is an Android module project. - const buildGradleFileName = 'build.gradle'; - const buildGradleFilePath = path.join(this.projectDir, buildGradleFileName); - if (!await fs.exists(buildGradleFilePath)) { + + async generateRootProjectFiles() { + this.logger.info('Generating root project files'); + + // Copy our SDK's gradle files to the build directory. (Includes "gradlew" scripts and "gradle" directory tree.) + const gradlew = new GradleWrapper(this.buildDir); + gradlew.logger = this.logger; + await gradlew.installTemplate(path.join(this.platformPath, 'templates', 'gradle')); + + // Create a "gradle.properties" file. Will add network proxy settings if needed. + const gradleProperties = await gradlew.fetchDefaultGradleProperties(); + gradleProperties.push({ key: 'android.useAndroidX', value: 'true' }); + gradleProperties.push({ key: 'android.suppressUnsupportedCompileSdk', value: '33' }); + gradleProperties.push({ + key: 'org.gradle.jvmargs', + value: `-Xmx${this.javacMaxMemory} -Dkotlin.daemon.jvm.options="-Xmx${this.javacMaxMemory}"` + }); + + // Kotlin KAPT compatibility for JDK16 + // NOTE: This parameter is removed in JDK17 and will prevent modules from compiling. + // https://youtrack.jetbrains.com/issue/KT-45545 + gradleProperties.push({ key: 'org.gradle.jvmargs', value: '--illegal-access=permit' }); + + await gradlew.writeGradlePropertiesFile(gradleProperties); + + // Create a "local.properties" file providing a path to the Android SDK directory. + await gradlew.writeLocalPropertiesFile(this.androidInfo.sdk.path); + + // Copy our root "build.gradle" template script to the root build directory. + const templatesDir = path.join(this.platformPath, 'templates', 'build'); await fs.copyFile( - path.join(this.platformPath, 'templates', 'module', 'default', 'template', 'android', buildGradleFileName), - buildGradleFilePath); + path.join(templatesDir, 'root.build.gradle'), + path.join(this.buildDir, 'build.gradle')); + + // Copy our Titanium template's gradle constants file. + // This provides the Google library versions we use and defines our custom "AndroidManifest.xml" placeholders. + const tiConstantsGradleFileName = 'ti.constants.gradle'; + await fs.copyFile( + path.join(templatesDir, tiConstantsGradleFileName), + path.join(this.buildDir, tiConstantsGradleFileName)); + + // Create a "settings.gradle" file providing a reference to the module's gradle subproject. + // By default, a subproject's name must match its subdirectory name. + const fileLines = [ + `rootProject.name = '${this.manifest.moduleid}'`, + "include ':module'" // eslint-disable-line quotes + ]; + await fs.writeFile(path.join(this.buildDir, 'settings.gradle'), fileLines.join('\n') + '\n'); } - // Determine if "assets" directory contains at least 1 JavaScript file. - let hasJSFile = false; - if (await fs.exists(this.assetsDir)) { - await this.dirWalker(this.assetsDir, (filePath) => { - if (path.extname(filePath).toLowerCase() === '.js') { - hasJSFile = true; + async generateModuleProject() { + this.logger.info(`Generating gradle project: ${'module'.cyan}`); + + // Create the "module" project directory tree. + // Delete all files under its "./src/main" subdirectory if it already exists. + // Note: Do not delete the "./build" subdirectory. It contains incremental build info. + const moduleMainDir = path.join(this.buildModuleDir, 'src', 'main'); + const moduleJavaPackageDir = path.join(moduleMainDir, 'java', ...this.manifest.moduleid.split('.')); + const moduleJniDir = path.join(moduleMainDir, 'jni'); + await fs.emptyDir(moduleMainDir); + await fs.ensureDir(moduleJavaPackageDir); + await fs.ensureDir(moduleJniDir); + + // Create a maven ":" from "moduleid". + // For example, "ti.map" becomes maven repository name "ti:map". + const moduleId = this.manifest.moduleid; + let mavenGroupId = moduleId; + let mavenArtifactId = moduleId; + let index = moduleId.lastIndexOf('.'); + if ((index > 0) && ((index + 1) < moduleId.length)) { + mavenGroupId = moduleId.substring(0, index); + mavenArtifactId = moduleId.substring(index + 1); + } + + // Generate a "build.gradle" file for this project from the SDK's EJS module template. + let buildGradleContent = await fs.readFile(path.join(this.moduleTemplateDir, 'build.gradle')); + buildGradleContent = ejs.render(buildGradleContent.toString(), { + compileSdkVersion: this.compileSdkVersion, + krollAptJarPath: path.join(this.platformPath, 'kroll-apt.jar'), + minSdkVersion: this.minSupportedApiLevel, + moduleAuthor: this.manifest.author, + moduleCopyright: this.manifest.copyright, + moduleDescription: this.manifest.description, + moduleId: moduleId, + moduleLicense: this.manifest.license, + moduleMavenGroupId: mavenGroupId, + moduleMavenArtifactId: mavenArtifactId, + moduleName: this.manifest.name, + moduleVersion: this.manifest.version, + moduleMinSdkVersion: this.manifest.minsdk, + moduleArchitectures: this.manifest.architectures.split(' '), + tiBindingsJsonPath: path.join(this.platformPath, 'titanium.bindings.json'), + tiMavenUrl: encodeURI('file://' + path.join(this.platformPath, 'm2repository').replace(/\\/g, '/')), + tiSdkModuleTemplateDir: this.moduleTemplateDir, + tiSdkVersion: this.titaniumSdkVersion + }); + await fs.writeFile(path.join(this.buildModuleDir, 'build.gradle'), buildGradleContent); + + // Copy module template's C++ code generation script to gradle project. + // Project's "build.gradle" will invoke this script after "kroll-apt" Java annotation processor has finished. + await fs.copyFile( + path.join(this.moduleTemplateDir, 'generate-cpp-files.js'), + path.join(this.buildModuleDir, 'generate-cpp-files.js')); + + // If module has "AndroidManifest.xml" file under its "./platform/android" directory, + // then copy it to this gradle project's "debug" and "release" subdirectories. + // This makes them extend main "AndroidManifest.xml" under "./src/main" which is taken from "timodule.xml". + const externalManifestFilePath = path.join(this.projectDir, 'platform', 'android', 'AndroidManifest.xml'); + if (await fs.exists(externalManifestFilePath)) { + const debugDirPath = path.join(this.buildModuleDir, 'src', 'debug'); + const releaseDirPath = path.join(this.buildModuleDir, 'src', 'release'); + await fs.ensureDir(debugDirPath); + await fs.ensureDir(releaseDirPath); + await fs.copyFile(externalManifestFilePath, path.join(debugDirPath, 'AndroidManifest.xml')); + await fs.copyFile(externalManifestFilePath, path.join(releaseDirPath, 'AndroidManifest.xml')); + } + + // Create main "AndroidManifest.xml" file under gradle project's "./src/main". + // If manifest settings exist in "timodule.xml", then merge it into main manifest. + const mainManifest = AndroidManifest.fromXmlString(''); + try { + if (this.timodule && this.timodule.android && this.timodule.android.manifest) { + const tiModuleManifest = AndroidManifest.fromXmlString(this.timodule.android.manifest); + mainManifest.copyFromAndroidManifest(tiModuleManifest); } + } catch (err) { + this.logger.error('Unable to load Android content from "timodule.xml" file.'); + throw err; + } + let packageName = moduleId; + if (packageName.indexOf('.') < 0) { + packageName = `ti.${packageName}`; + } + mainManifest.setPackageName(packageName); + await mainManifest.writeToFilePath(path.join(moduleMainDir, 'AndroidManifest.xml')); + + // Generate Java file used to provide this module's JS source code to Titanium's JS runtime. + let fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'CommonJsSourceProvider.java')); + fileContent = ejs.render(fileContent.toString(), { moduleId: moduleId }); + const javaFilePath = path.join(moduleJavaPackageDir, 'CommonJsSourceProvider.java'); + await fs.writeFile(javaFilePath, fileContent); + + // Generate Java file used to load below C++ bootstrap. + fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'TiModuleBootstrap.java')); + fileContent = ejs.render(fileContent.toString(), { moduleId: moduleId }); + await fs.writeFile(path.join(moduleJavaPackageDir, 'TiModuleBootstrap.java'), fileContent); + + // Generate the C/C++ makefile. + fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'Android.mk')); + fileContent = ejs.render(fileContent.toString(), { + moduleId: this.manifest.moduleid, + tiSdkDirPath: this.platformPath }); + await fs.writeFile(path.join(moduleJniDir, 'Android.mk'), fileContent); } - // If JS file was found, then change module's "manifest" file setting "commonjs" to true. - if (hasJSFile && !this.manifest.commonjs) { - let wasFound = false; - let manifestContents = await fs.readFile(this.manifestFile); - manifestContents = manifestContents.toString().replace(/^commonjs:\s*.+$/mg, () => { - wasFound = true; - return 'commonjs: true'; + async buildModuleProject() { + this.logger.info('Building module'); + + // Emit a "javac" hook event for plugins. + await new Promise((resolve) => { + const javacHook = this.cli.createHook('build.android.javac', this, (exe, args, opts, done) => { + done(); + }); + javacHook('', [], {}, resolve); }); - if (!wasFound) { - manifestContents = manifestContents.trim() + '\ncommonjs: true\n'; - } - await fs.writeFile(this.manifestFile, manifestContents); - this.manifest.commonjs = true; - this.logger.info(__('Manifest re-written to set commonjs value')); - } -}; - -AndroidModuleBuilder.prototype.generateRootProjectFiles = async function generateRootProjectFiles() { - this.logger.info(__('Generating root project files')); - - // Copy our SDK's gradle files to the build directory. (Includes "gradlew" scripts and "gradle" directory tree.) - const gradlew = new GradleWrapper(this.buildDir); - gradlew.logger = this.logger; - await gradlew.installTemplate(path.join(this.platformPath, 'templates', 'gradle')); - - // Create a "gradle.properties" file. Will add network proxy settings if needed. - const gradleProperties = await gradlew.fetchDefaultGradleProperties(); - gradleProperties.push({ key: 'android.useAndroidX', value: 'true' }); - gradleProperties.push({ key: 'android.suppressUnsupportedCompileSdk', value: '33' }); - gradleProperties.push({ - key: 'org.gradle.jvmargs', - value: `-Xmx${this.javacMaxMemory} -Dkotlin.daemon.jvm.options="-Xmx${this.javacMaxMemory}"` - }); - - // Kotlin KAPT compatibility for JDK16 - // NOTE: This parameter is removed in JDK17 and will prevent modules from compiling. - // https://youtrack.jetbrains.com/issue/KT-45545 - gradleProperties.push({ key: 'org.gradle.jvmargs', value: '--illegal-access=permit' }); - - await gradlew.writeGradlePropertiesFile(gradleProperties); - - // Create a "local.properties" file providing a path to the Android SDK directory. - await gradlew.writeLocalPropertiesFile(this.androidInfo.sdk.path); - - // Copy our root "build.gradle" template script to the root build directory. - const templatesDir = path.join(this.platformPath, 'templates', 'build'); - await fs.copyFile( - path.join(templatesDir, 'root.build.gradle'), - path.join(this.buildDir, 'build.gradle')); - - // Copy our Titanium template's gradle constants file. - // This provides the Google library versions we use and defines our custom "AndroidManifest.xml" placeholders. - const tiConstantsGradleFileName = 'ti.constants.gradle'; - await fs.copyFile( - path.join(templatesDir, tiConstantsGradleFileName), - path.join(this.buildDir, tiConstantsGradleFileName)); - - // Create a "settings.gradle" file providing a reference to the module's gradle subproject. - // By default, a subproject's name must match its subdirectory name. - const fileLines = [ - `rootProject.name = '${this.manifest.moduleid}'`, - "include ':module'" // eslint-disable-line quotes - ]; - await fs.writeFile(path.join(this.buildDir, 'settings.gradle'), fileLines.join('\n') + '\n'); -}; - -AndroidModuleBuilder.prototype.generateModuleProject = async function generateModuleProject() { - this.logger.info(__('Generating gradle project: %s', 'module'.cyan)); - - // Create the "module" project directory tree. - // Delete all files under its "./src/main" subdirectory if it already exists. - // Note: Do not delete the "./build" subdirectory. It contains incremental build info. - const moduleMainDir = path.join(this.buildModuleDir, 'src', 'main'); - const moduleJavaPackageDir = path.join(moduleMainDir, 'java', ...this.manifest.moduleid.split('.')); - const moduleJniDir = path.join(moduleMainDir, 'jni'); - await fs.emptyDir(moduleMainDir); - await fs.ensureDir(moduleJavaPackageDir); - await fs.ensureDir(moduleJniDir); - - // Create a maven ":" from "moduleid". - // For example, "ti.map" becomes maven repository name "ti:map". - const moduleId = this.manifest.moduleid; - let mavenGroupId = moduleId; - let mavenArtifactId = moduleId; - let index = moduleId.lastIndexOf('.'); - if ((index > 0) && ((index + 1) < moduleId.length)) { - mavenGroupId = moduleId.substring(0, index); - mavenArtifactId = moduleId.substring(index + 1); - } - // Generate a "build.gradle" file for this project from the SDK's EJS module template. - let buildGradleContent = await fs.readFile(path.join(this.moduleTemplateDir, 'build.gradle')); - buildGradleContent = ejs.render(buildGradleContent.toString(), { - compileSdkVersion: this.compileSdkVersion, - krollAptJarPath: path.join(this.platformPath, 'kroll-apt.jar'), - minSdkVersion: this.minSupportedApiLevel, - moduleAuthor: this.manifest.author, - moduleCopyright: this.manifest.copyright, - moduleDescription: this.manifest.description, - moduleId: moduleId, - moduleLicense: this.manifest.license, - moduleMavenGroupId: mavenGroupId, - moduleMavenArtifactId: mavenArtifactId, - moduleName: this.manifest.name, - moduleVersion: this.manifest.version, - moduleMinSdkVersion: this.manifest.minsdk, - moduleArchitectures: this.manifest.architectures.split(' '), - tiBindingsJsonPath: path.join(this.platformPath, 'titanium.bindings.json'), - tiMavenUrl: encodeURI('file://' + path.join(this.platformPath, 'm2repository').replace(/\\/g, '/')), - tiSdkModuleTemplateDir: this.moduleTemplateDir, - tiSdkVersion: this.titaniumSdkVersion - }); - await fs.writeFile(path.join(this.buildModuleDir, 'build.gradle'), buildGradleContent); - - // Copy module template's C++ code generation script to gradle project. - // Project's "build.gradle" will invoke this script after "kroll-apt" Java annotation processor has finished. - await fs.copyFile( - path.join(this.moduleTemplateDir, 'generate-cpp-files.js'), - path.join(this.buildModuleDir, 'generate-cpp-files.js')); - - // If module has "AndroidManifest.xml" file under its "./platform/android" directory, - // then copy it to this gradle project's "debug" and "release" subdirectories. - // This makes them extend main "AndroidManifest.xml" under "./src/main" which is taken from "timodule.xml". - const externalManifestFilePath = path.join(this.projectDir, 'platform', 'android', 'AndroidManifest.xml'); - if (await fs.exists(externalManifestFilePath)) { - const debugDirPath = path.join(this.buildModuleDir, 'src', 'debug'); - const releaseDirPath = path.join(this.buildModuleDir, 'src', 'release'); - await fs.ensureDir(debugDirPath); - await fs.ensureDir(releaseDirPath); - await fs.copyFile(externalManifestFilePath, path.join(debugDirPath, 'AndroidManifest.xml')); - await fs.copyFile(externalManifestFilePath, path.join(releaseDirPath, 'AndroidManifest.xml')); - } + // Build the module library project as an AAR. + const gradlew = new GradleWrapper(this.buildDir); + gradlew.logger = this.logger; + await gradlew.assembleRelease('module'); - // Create main "AndroidManifest.xml" file under gradle project's "./src/main". - // If manifest settings exist in "timodule.xml", then merge it into main manifest. - const mainManifest = AndroidManifest.fromXmlString(''); - try { - if (this.timodule && this.timodule.android && this.timodule.android.manifest) { - const tiModuleManifest = AndroidManifest.fromXmlString(this.timodule.android.manifest); - mainManifest.copyFromAndroidManifest(tiModuleManifest); - } - } catch (err) { - this.logger.error('Unable to load Android content from "timodule.xml" file.'); - throw err; + // Create a local maven repository directory tree containing above AAR and "*.pom" file listing its dependencies. + // This is what the Titanium app will reference in its "build.gradle" file under its "dependencies" section. + await gradlew.publish('module'); } - let packageName = moduleId; - if (packageName.indexOf('.') < 0) { - packageName = `ti.${packageName}`; - } - mainManifest.setPackageName(packageName); - await mainManifest.writeToFilePath(path.join(moduleMainDir, 'AndroidManifest.xml')); - - // Generate Java file used to provide this module's JS source code to Titanium's JS runtime. - let fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'CommonJsSourceProvider.java')); - fileContent = ejs.render(fileContent.toString(), { moduleId: moduleId }); - const javaFilePath = path.join(moduleJavaPackageDir, 'CommonJsSourceProvider.java'); - await fs.writeFile(javaFilePath, fileContent); - - // Generate Java file used to load below C++ bootstrap. - fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'TiModuleBootstrap.java')); - fileContent = ejs.render(fileContent.toString(), { moduleId: moduleId }); - await fs.writeFile(path.join(moduleJavaPackageDir, 'TiModuleBootstrap.java'), fileContent); - - // Generate the C/C++ makefile. - fileContent = await fs.readFile(path.join(this.moduleTemplateDir, 'Android.mk')); - fileContent = ejs.render(fileContent.toString(), { - moduleId: this.manifest.moduleid, - tiSdkDirPath: this.platformPath - }); - await fs.writeFile(path.join(moduleJniDir, 'Android.mk'), fileContent); -}; - -AndroidModuleBuilder.prototype.buildModuleProject = async function buildModuleProject() { - this.logger.info(__('Building module')); - - // Emit a "javac" hook event for plugins. - await new Promise((resolve) => { - const javacHook = this.cli.createHook('build.android.javac', this, (exe, args, opts, done) => { - done(); + + async packageZip() { + this.logger.info('Packaging the module'); + + // Create/Clean the "dist" directory that we'll be writing the zipped-up module to. + await fs.emptyDir(this.distDir); + + // Define relative folder path to files will be stored as in zip file. + // When installing this module, it'll be unzipped to main Titanium SDK directory or app project's directory. + const moduleFolder = path.join('modules', 'android', this.manifest.moduleid, this.manifest.version); + + // Define the zip file name and path. + // Store the zip path to "this.moduleZipPath" to be accessed by build script's runModule() method. + const zipFileName = `${this.manifest.moduleid}-android-${this.manifest.version}.zip`; + const moduleZipPath = path.join(this.distDir, zipFileName); + this.moduleZipPath = moduleZipPath; + + // Create the zip archive buffer. + const dest = archiver('zip', { forceUTC: true }); + dest.catchEarlyExitAttached = true; // silence exceptions + dest.pipe(fs.createWriteStream(moduleZipPath)); + this.logger.info('Creating module zip'); + + // Add the module's built AAR maven repository directory tree to the archive. + // This is the library dependency an app project's "build.gradle" will reference. + const mavenDirPath = path.join(this.buildModuleDir, 'build', 'outputs', 'm2repository'); + await this.dirWalker(mavenDirPath, (filePath) => { + const zipEntryName = path.join(moduleFolder, 'm2repository', path.relative(mavenDirPath, filePath)); + dest.append(fs.createReadStream(filePath), { name: zipEntryName }); }); - javacHook('', [], {}, resolve); - }); - - // Build the module library project as an AAR. - const gradlew = new GradleWrapper(this.buildDir); - gradlew.logger = this.logger; - await gradlew.assembleRelease('module'); - - // Create a local maven repository directory tree containing above AAR and "*.pom" file listing its dependencies. - // This is what the Titanium app will reference in its "build.gradle" file under its "dependencies" section. - await gradlew.publish('module'); -}; - -AndroidModuleBuilder.prototype.packageZip = async function () { - this.logger.info(__('Packaging the module')); - - // Create/Clean the "dist" directory that we'll be writing the zipped-up module to. - await fs.emptyDir(this.distDir); - - // Define relative folder path to files will be stored as in zip file. - // When installing this module, it'll be unzipped to main Titanium SDK directory or app project's directory. - const moduleFolder = path.join('modules', 'android', this.manifest.moduleid, this.manifest.version); - - // Define the zip file name and path. - // Store the zip path to "this.moduleZipPath" to be accessed by build script's runModule() method. - const zipFileName = `${this.manifest.moduleid}-android-${this.manifest.version}.zip`; - const moduleZipPath = path.join(this.distDir, zipFileName); - this.moduleZipPath = moduleZipPath; - - // Create the zip archive buffer. - const dest = archiver('zip', { forceUTC: true }); - dest.catchEarlyExitAttached = true; // silence exceptions - dest.pipe(fs.createWriteStream(moduleZipPath)); - this.logger.info(__('Creating module zip')); - - // Add the module's built AAR maven repository directory tree to the archive. - // This is the library dependency an app project's "build.gradle" will reference. - const mavenDirPath = path.join(this.buildModuleDir, 'build', 'outputs', 'm2repository'); - await this.dirWalker(mavenDirPath, (filePath) => { - const zipEntryName = path.join(moduleFolder, 'm2repository', path.relative(mavenDirPath, filePath)); - dest.append(fs.createReadStream(filePath), { name: zipEntryName }); - }); - - // Add module's proxy binding JSON file to archive. - // Needed by the app build system when generating the "TiApplication" derived class to inject - // this module's classes into the KrollRuntime and to invoke module's onAppCreate() if defined. - const bindingsFileName = this.manifest.name + '.json'; - const bindingsFilePath = path.join(this.buildModuleDir, 'build', 'ti-generated', 'json', bindingsFileName); - if (await fs.exists(bindingsFilePath)) { - dest.append(fs.createReadStream(bindingsFilePath), { name: path.join(moduleFolder, bindingsFileName) }); - } - // Add the "documentation" files to the archive. - const archiveDocFilesInDirectory = async (directoryPath, zipEntryName) => { - for (const fileName of await fs.readdir(directoryPath)) { - const filePath = path.join(directoryPath, fileName); - if ((await fs.stat(filePath)).isDirectory()) { - await archiveDocFilesInDirectory(filePath, path.join(zipEntryName, fileName)); - } else { - let newFileName = fileName; - let fileContent = await fs.readFile(filePath); - if (fileName.toLowerCase().endsWith('.md')) { - fileContent = markdown.toHTML(fileContent.toString()); - newFileName = fileName.substring(0, fileName.lastIndexOf('.')) + '.html'; + // Add module's proxy binding JSON file to archive. + // Needed by the app build system when generating the "TiApplication" derived class to inject + // this module's classes into the KrollRuntime and to invoke module's onAppCreate() if defined. + const bindingsFileName = this.manifest.name + '.json'; + const bindingsFilePath = path.join(this.buildModuleDir, 'build', 'ti-generated', 'json', bindingsFileName); + if (await fs.exists(bindingsFilePath)) { + dest.append(fs.createReadStream(bindingsFilePath), { name: path.join(moduleFolder, bindingsFileName) }); + } + + // Add the "documentation" files to the archive. + const archiveDocFilesInDirectory = async (directoryPath, zipEntryName) => { + for (const fileName of await fs.readdir(directoryPath)) { + const filePath = path.join(directoryPath, fileName); + if ((await fs.stat(filePath)).isDirectory()) { + await archiveDocFilesInDirectory(filePath, path.join(zipEntryName, fileName)); + } else { + let newFileName = fileName; + let fileContent = await fs.readFile(filePath); + if (fileName.toLowerCase().endsWith('.md')) { + fileContent = markdown.toHTML(fileContent.toString()); + newFileName = fileName.substring(0, fileName.lastIndexOf('.')) + '.html'; + } + dest.append(fileContent, { name: path.join(zipEntryName, newFileName) }); } - dest.append(fileContent, { name: path.join(zipEntryName, newFileName) }); } + }; + if (await fs.exists(this.documentationDir)) { + await archiveDocFilesInDirectory(this.documentationDir, path.join(moduleFolder, 'documentation')); } - }; - if (await fs.exists(this.documentationDir)) { - await archiveDocFilesInDirectory(this.documentationDir, path.join(moduleFolder, 'documentation')); - } - // Add the "example" app project files to the archive. - /* - if (await fs.exists(this.exampleDir)) { - await this.dirWalker(this.exampleDir, (filePath) => { - const zipEntryName = path.join(moduleFolder, 'example', path.relative(this.exampleDir, filePath)); - dest.append(fs.createReadStream(filePath), { name: zipEntryName }); - }); - } - */ - - // Add the event hook plugin scripts to the archive. - const hookFiles = {}; - const archiveHookFilesInDirectory = async (directoryPath) => { - if (await fs.exists(directoryPath)) { - await this.dirWalker(directoryPath, (filePath) => { - const relativeFilePath = path.relative(directoryPath, filePath); - if (!hookFiles[relativeFilePath]) { - hookFiles[relativeFilePath] = true; - const zipEntryName = path.join(moduleFolder, 'hooks', relativeFilePath); + // Add the "example" app project files to the archive. + /* + if (await fs.exists(this.exampleDir)) { + await this.dirWalker(this.exampleDir, (filePath) => { + const zipEntryName = path.join(moduleFolder, 'example', path.relative(this.exampleDir, filePath)); + dest.append(fs.createReadStream(filePath), { name: zipEntryName }); + }); + } + */ + + // Add the event hook plugin scripts to the archive. + const hookFiles = {}; + const archiveHookFilesInDirectory = async (directoryPath) => { + if (await fs.exists(directoryPath)) { + await this.dirWalker(directoryPath, (filePath) => { + const relativeFilePath = path.relative(directoryPath, filePath); + if (!hookFiles[relativeFilePath]) { + hookFiles[relativeFilePath] = true; + const zipEntryName = path.join(moduleFolder, 'hooks', relativeFilePath); + dest.append(fs.createReadStream(filePath), { name: zipEntryName }); + } + }); + } + }; + await archiveHookFilesInDirectory(path.join(this.projectDir, 'hooks')); + await archiveHookFilesInDirectory(path.join(this.projectDir, '..', 'hooks')); + + // Add module's "Resources" directory to the archive. + // These files will be copied to the app project's root "Resources" directory. + if (await fs.exists(this.resourcesDir)) { + await this.dirWalker(this.resourcesDir, (filePath, fileName) => { + if (fileName !== 'README.md') { + const zipEntryName = path.join(moduleFolder, 'Resources', path.relative(this.resourcesDir, filePath)); dest.append(fs.createReadStream(filePath), { name: zipEntryName }); } }); } - }; - await archiveHookFilesInDirectory(path.join(this.projectDir, 'hooks')); - await archiveHookFilesInDirectory(path.join(this.projectDir, '..', 'hooks')); - - // Add module's "Resources" directory to the archive. - // These files will be copied to the app project's root "Resources" directory. - if (await fs.exists(this.resourcesDir)) { - await this.dirWalker(this.resourcesDir, (filePath, fileName) => { - if (fileName !== 'README.md') { - const zipEntryName = path.join(moduleFolder, 'Resources', path.relative(this.resourcesDir, filePath)); - dest.append(fs.createReadStream(filePath), { name: zipEntryName }); - } - }); - } - // Add "assets" directory files to the archive. - // TODO: We already include "assets" files as JAR resources (via gradle), which is what we officially document. - // We should not add these files to APK "assets" as well. This doubles storage space taken. - if (await fs.exists(this.assetsDir)) { - await this.dirWalker(this.assetsDir, (filePath, fileName) => { - if (fileName !== 'README') { - let zipEntryName; - const relativeFilePath = path.relative(this.assetsDir, filePath); - if (path.extname(filePath).toLowerCase() !== '.js') { - // All files (except JavaScript) will be added to APK "assets/Resources" directory. - zipEntryName = path.join(moduleFolder, 'assets', relativeFilePath); - } else { - // JavaScript files are added to APK "assets/Resources/" to be loaded like CommonJS. - zipEntryName = path.join(moduleFolder, 'Resources', this.manifest.moduleid, relativeFilePath); + // Add "assets" directory files to the archive. + // TODO: We already include "assets" files as JAR resources (via gradle), which is what we officially document. + // We should not add these files to APK "assets" as well. This doubles storage space taken. + if (await fs.exists(this.assetsDir)) { + await this.dirWalker(this.assetsDir, (filePath, fileName) => { + if (fileName !== 'README') { + let zipEntryName; + const relativeFilePath = path.relative(this.assetsDir, filePath); + if (path.extname(filePath).toLowerCase() !== '.js') { + // All files (except JavaScript) will be added to APK "assets/Resources" directory. + zipEntryName = path.join(moduleFolder, 'assets', relativeFilePath); + } else { + // JavaScript files are added to APK "assets/Resources/" to be loaded like CommonJS. + zipEntryName = path.join(moduleFolder, 'Resources', this.manifest.moduleid, relativeFilePath); + } + dest.append(fs.createReadStream(filePath), { name: zipEntryName }); } - dest.append(fs.createReadStream(filePath), { name: zipEntryName }); - } - }); - } + }); + } - // Add the license file to the archive. - let hasLicenseFile = false; - let licenseFilePath = path.join(this.projectDir, 'LICENSE'); - hasLicenseFile = await fs.exists(licenseFilePath); - if (!hasLicenseFile) { - licenseFilePath = path.join(this.projectDir, '..', 'LICENSE'); + // Add the license file to the archive. + let hasLicenseFile = false; + let licenseFilePath = path.join(this.projectDir, 'LICENSE'); hasLicenseFile = await fs.exists(licenseFilePath); - } - if (hasLicenseFile) { - dest.append(fs.createReadStream(licenseFilePath), { name: path.join(moduleFolder, 'LICENSE') }); - } - - // Add "manifest" file to the archive. - dest.append(fs.createReadStream(this.manifestFile), { name: path.join(moduleFolder, 'manifest') }); + if (!hasLicenseFile) { + licenseFilePath = path.join(this.projectDir, '..', 'LICENSE'); + hasLicenseFile = await fs.exists(licenseFilePath); + } + if (hasLicenseFile) { + dest.append(fs.createReadStream(licenseFilePath), { name: path.join(moduleFolder, 'LICENSE') }); + } - // Add "timanifest.xml" file to the archive. - dest.append(fs.createReadStream(this.timoduleXmlFile), { name: path.join(moduleFolder, 'timodule.xml') }); + // Add "manifest" file to the archive. + dest.append(fs.createReadStream(this.manifestFile), { name: path.join(moduleFolder, 'manifest') }); - // Create the zip file containing the above archived/buffered files. - this.logger.info(__('Writing module zip: %s', moduleZipPath)); - dest.finalize(); -}; + // Add "timanifest.xml" file to the archive. + dest.append(fs.createReadStream(this.timoduleXmlFile), { name: path.join(moduleFolder, 'timodule.xml') }); -AndroidModuleBuilder.prototype.runModule = async function (cli) { - // Do not run built module in an app if given command line argument "--build-only". - if (this.buildOnly) { - return; + // Create the zip file containing the above archived/buffered files. + this.logger.info(`Writing module zip: ${moduleZipPath}`); + dest.finalize(); } - const tmpDir = temp.path('ti-android-module-build-'); - let tmpProjectDir; + async runModule(cli) { + // Do not run built module in an app if given command line argument "--build-only". + if (this.buildOnly) { + return; + } - function checkLine(line, logger) { - const re = new RegExp( // eslint-disable-line security/detect-non-literal-regexp - '(?:\u001b\\[\\d+m)?\\[?(' - + logger.getLevels().join('|') - + ')\\]?\\s*(?:\u001b\\[\\d+m)?(.*)', 'i' - ); + const tmpDir = temp.path('ti-android-module-build-'); + let tmpProjectDir; - if (line) { - const m = line.match(re); - if (m) { - logger[m[1].toLowerCase()](m[2].trim()); - } else { - logger.debug(line); - } - } - } + function checkLine(line, logger) { + const re = new RegExp( // eslint-disable-line security/detect-non-literal-regexp + '(?:\u001b\\[\\d+m)?\\[?(' + + logger.getLevels().join('|') + + ')\\]?\\s*(?:\u001b\\[\\d+m)?(.*)', 'i' + ); - async function runTiCommand(cmd, args, logger) { - return new Promise((resolve) => { - // when calling on Windows, we need to escape ampersands in the command - if (process.platform === 'win32') { - cmd.replace(/&/g, '^&'); + if (line) { + const m = line.match(re); + if (m) { + logger[m[1].toLowerCase()](m[2].trim()); + } else { + logger.debug(line); + } } + } - const child = spawn(cmd, args); - child.stdout.on('data', function (data) { - for (const line of data.toString().split('\n')) { - checkLine(line, logger); - } - }); - child.stderr.on('data', function (data) { - for (const line of data.toString().split('\n')) { - checkLine(line, logger); - } - }); - child.on('close', function (code) { - if (code) { - logger.error(__('Failed to run ti %s', args[0])); - logger.log(); - process.exit(1); + async function runTiCommand(cmd, args, logger) { + return new Promise((resolve) => { + // when calling on Windows, we need to escape ampersands in the command + if (process.platform === 'win32') { + cmd.replace(/&/g, '^&'); } - resolve(); - }); - }); - } - // Create a temp directory to build the example app project to. - await fs.mkdirs(tmpDir); - - // Generate a new Titanium app in the temp directory which we'll later copy the "example" files to. - // Note: App must have a different id/package-name. Avoids class name collision with module generating Java code. - this.logger.debug(__('Staging module project at %s', tmpDir.cyan)); - await runTiCommand( - process.execPath, - [ - process.argv[1], - 'create', - '--id', this.manifest.moduleid + '.app', - '-n', this.manifest.name, - '-t', 'app', - '-u', 'localhost', - '-d', tmpDir, - '-p', 'android', - '--force' - ], - this.logger - ); - - // Inject this module's reference into the temp app project's "tiapp.xml" file. - // TODO: It would be more reliable to do this via "DOMParser" instead. - tmpProjectDir = path.join(tmpDir, this.manifest.name); - this.logger.debug(__('Created example project %s', tmpProjectDir.cyan)); - let fileContent = await fs.readFile(path.join(tmpProjectDir, 'tiapp.xml')); - fileContent = fileContent.toString().replace( - //g, `\n\t\t${this.manifest.moduleid}`); - await fs.writeFile(path.join(tmpProjectDir, 'tiapp.xml'), fileContent); - - // Copy files from module's "example" directory to temp app's "Resources" directory. - appc.fs.copyDirSyncRecursive( - this.exampleDir, - path.join(tmpProjectDir, 'Resources'), - { - preserve: true, - logger: this.logger.debug + const child = spawn(cmd, args); + child.stdout.on('data', function (data) { + for (const line of data.toString().split('\n')) { + checkLine(line, logger); + } + }); + child.stderr.on('data', function (data) { + for (const line of data.toString().split('\n')) { + checkLine(line, logger); + } + }); + child.on('close', function (code) { + if (code) { + logger.error(`Failed to run ti ${args[0]}`); + logger.log(); + process.exit(1); + } + resolve(); + }); + }); } - ); - // Copy example/platform to tmp/platform to use a custom build.gradle - if (fs.existsSync(path.join(this.exampleDir, 'platform'))) { + // Create a temp directory to build the example app project to. + await fs.mkdirs(tmpDir); + + // Generate a new Titanium app in the temp directory which we'll later copy the "example" files to. + // Note: App must have a different id/package-name. Avoids class name collision with module generating Java code. + this.logger.debug(`Staging module project at ${tmpDir.cyan}`); + await runTiCommand( + process.execPath, + [ + process.argv[1], + 'create', + '--id', this.manifest.moduleid + '.app', + '-n', this.manifest.name, + '-t', 'app', + '-u', 'localhost', + '-d', tmpDir, + '-p', 'android', + '--force' + ], + this.logger + ); + + // Inject this module's reference into the temp app project's "tiapp.xml" file. + // TODO: It would be more reliable to do this via "DOMParser" instead. + tmpProjectDir = path.join(tmpDir, this.manifest.name); + this.logger.debug(`Created example project ${tmpProjectDir.cyan}`); + let fileContent = await fs.readFile(path.join(tmpProjectDir, 'tiapp.xml')); + fileContent = fileContent.toString().replace( + //g, `\n\t\t${this.manifest.moduleid}`); + await fs.writeFile(path.join(tmpProjectDir, 'tiapp.xml'), fileContent); + + // Copy files from module's "example" directory to temp app's "Resources" directory. appc.fs.copyDirSyncRecursive( - path.join(this.exampleDir, 'platform'), - path.join(tmpProjectDir, 'platform'), + this.exampleDir, + path.join(tmpProjectDir, 'Resources'), { preserve: true, logger: this.logger.debug } ); - } - // Unzip module into temp app's "modules" directory. - await util.promisify(appc.zip.unzip)(this.moduleZipPath, tmpProjectDir, null); + // Copy example/platform to tmp/platform to use a custom build.gradle + if (fs.existsSync(path.join(this.exampleDir, 'platform'))) { + appc.fs.copyDirSyncRecursive( + path.join(this.exampleDir, 'platform'), + path.join(tmpProjectDir, 'platform'), + { + preserve: true, + logger: this.logger.debug + } + ); + } - // Emit hook so modules can also alter project before launch - await new Promise(resolve => cli.emit('create.module.app.finalize', [ this, tmpProjectDir ], resolve)); + // Unzip module into temp app's "modules" directory. + await util.promisify(appc.zip.unzip)(this.moduleZipPath, tmpProjectDir, null); - // Run the temp app. - this.logger.debug(__('Running example project...', tmpDir.cyan)); - let buildArgs = [ process.argv[1], 'build', '-p', 'android', '-d', tmpProjectDir ]; - if (this.target) { - buildArgs.push('-T'); - buildArgs.push(this.target); - } - if (this.deviceId) { - buildArgs.push('-C'); - buildArgs.push(this.deviceId); + // Emit hook so modules can also alter project before launch + await new Promise(resolve => cli.emit('create.module.app.finalize', [ this, tmpProjectDir ], resolve)); + + // Run the temp app. + this.logger.debug(`Running example project... ${tmpDir.cyan}`); + let buildArgs = [ process.argv[1], 'build', '-p', 'android', '-d', tmpProjectDir ]; + if (this.target) { + buildArgs.push('-T'); + buildArgs.push(this.target); + } + if (this.deviceId) { + buildArgs.push('-C'); + buildArgs.push(this.deviceId); + } + await runTiCommand(process.execPath, buildArgs, this.logger); } - await runTiCommand(process.execPath, buildArgs, this.logger); -}; +} // create the builder instance and expose the public api -(function (androidModuleBuilder) { - exports.config = androidModuleBuilder.config.bind(androidModuleBuilder); - exports.validate = androidModuleBuilder.validate.bind(androidModuleBuilder); - exports.run = androidModuleBuilder.run.bind(androidModuleBuilder); -}(new AndroidModuleBuilder(module))); +const moduleBuilder = new AndroidModuleBuilder(module); +export const config = moduleBuilder.config.bind(moduleBuilder); +export const validate = moduleBuilder.validate.bind(moduleBuilder); +export const run = moduleBuilder.run.bind(moduleBuilder); diff --git a/android/cli/commands/_cleanModule.js b/android/cli/commands/_cleanModule.js index 8bf3fa327c6..33e4a3ddda9 100644 --- a/android/cli/commands/_cleanModule.js +++ b/android/cli/commands/_cleanModule.js @@ -11,15 +11,11 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; +import fs from 'fs-extra'; +import { GradleWrapper } from '../lib/gradle-wrapper.js'; +import path from 'node:path'; -const appc = require('node-appc'); -const fs = require('fs-extra'); -const GradleWrapper = require('../lib/gradle-wrapper'); -const path = require('path'); -const __ = appc.i18n(__dirname).__; - -exports.run = async function run(logger, config, cli, finished) { +export async function run(logger, config, cli, finished) { try { const projectDir = cli.argv['project-dir']; @@ -43,7 +39,7 @@ exports.run = async function run(logger, config, cli, finished) { continue; } const filePath = path.join(buildDir, file); - logger.debug(__('Deleting %s', filePath.cyan)); + logger.debug(`Deleting ${filePath.cyan}`); await fs.remove(filePath); } @@ -52,7 +48,7 @@ exports.run = async function run(logger, config, cli, finished) { for (const nextFileName of fileNames) { const nextFilePath = path.join(projectDir, nextFileName); if (await fs.exists(nextFilePath)) { - logger.debug(__('Deleting %s', nextFilePath.cyan)); + logger.debug(`Deleting ${nextFilePath.cyan}`); await fs.remove(nextFilePath); } } @@ -66,7 +62,7 @@ exports.run = async function run(logger, config, cli, finished) { for (const architectureFolderName of await fs.readdir(libsDirPath)) { const libFilePath = path.join(libsDirPath, architectureFolderName, libFileName); if (await fs.exists(libFilePath)) { - logger.debug(__('Deleting %s', libFilePath.cyan)); + logger.debug(`Deleting ${libFilePath.cyan}`); await fs.remove(libFilePath); } } @@ -76,4 +72,4 @@ exports.run = async function run(logger, config, cli, finished) { } finished(); -}; +} diff --git a/android/cli/hooks/package.js b/android/cli/hooks/package.js index 95280c36723..ef08e67a5ff 100644 --- a/android/cli/hooks/package.js +++ b/android/cli/hooks/package.js @@ -5,17 +5,13 @@ * See the LICENSE file for more information. */ -'use strict'; +import appc from 'node-appc'; +import fs from 'fs-extra'; +import path from 'node:path'; -const appc = require('node-appc'), - fs = require('fs-extra'), - path = require('path'), - __ = appc.i18n(__dirname).__; - -exports.cliVersion = '>=3.2'; - -exports.init = function (logger, config, cli) { +export const cliVersion = '>=3.2'; +export function init(logger, config, cli) { cli.on('build.post.compile', { priority: 10000, post: function (builder, finished) { @@ -27,7 +23,7 @@ exports.init = function (logger, config, cli) { // Do not continue if developer did not provide a destination directory. const outputDir = builder.outputDir; if (!outputDir) { - logger.error(__('Packaging output directory path cannot be empty.')); + logger.error('Packaging output directory path cannot be empty.'); return finished(); } @@ -52,11 +48,10 @@ exports.init = function (logger, config, cli) { appc.fs.copyFileSync(builder.aabFile, outputFilePath, { logger: logger.debug }); } - logger.info(__('Packaging complete')); - logger.info(__('Package location: %s', outputDir.cyan)); + logger.info('Packaging complete'); + logger.info(`Package location: ${outputDir.cyan}`); finished(); } }); - -}; +} diff --git a/android/cli/hooks/run.js b/android/cli/hooks/run.js index b8b2e07ef06..3fbb55a975a 100644 --- a/android/cli/hooks/run.js +++ b/android/cli/hooks/run.js @@ -5,18 +5,14 @@ * See the LICENSE file for more information. */ -'use strict'; +import ADB from 'node-titanium-sdk/lib/adb.js'; +import async from 'async'; +import EmulatorManager from 'node-titanium-sdk/lib/emulator.js'; +import fs from 'node:fs'; -const ADB = require('node-titanium-sdk/lib/adb'), - appc = require('node-appc'), - async = require('async'), - EmulatorManager = require('node-titanium-sdk/lib/emulator'), - fs = require('fs'), - __ = appc.i18n(__dirname).__; +export const cliVersion = '>=3.2'; -exports.cliVersion = '>=3.2'; - -exports.init = function (logger, config, cli) { +export function init(logger, config, cli) { let deviceInfo = []; const ignoreLog = config.cli.ignoreLog || []; @@ -28,25 +24,25 @@ exports.init = function (logger, config, cli) { } if (builder.target === 'emulator') { - logger.info(__('Launching emulator: %s', builder.deviceId.cyan)); + logger.info(`Launching emulator: ${builder.deviceId.cyan}`); cli.createHook('build.android.startEmulator', function (deviceId, opts, cb) { const emulator = new EmulatorManager(config); - logger.trace(__('Starting emulator: %s', deviceId.cyan)); + logger.trace(`Starting emulator: ${deviceId.cyan}`); emulator.start(deviceId, opts, function (err, emu) { if (err) { - logger.error(__('Unable to start emulator "%s"', deviceId.cyan) + '\n'); + logger.error(`Unable to start emulator "${deviceId.cyan}"\n`); logger.error(err.message || err); logger.log(); process.exit(1); } - logger.trace(__('Emulator process started')); + logger.trace('Emulator process started'); emu.on('ready', function (device) { - logger.info(__('Emulator ready!')); + logger.info('Emulator ready!'); deviceInfo = [ device ]; }); @@ -56,13 +52,13 @@ exports.init = function (logger, config, cli) { }); emu.on('timeout', function (err) { - logger.error(__('Emulator timeout after waiting %s ms', err.waited)); + logger.error(`Emulator timeout after waiting ${err.waited} ms`); logger.log(); process.exit(1); }); emu.on('error', function (err) { - logger.error(__('An emulator error occurred')); + logger.error('An emulator error occurred'); logger.error(err); logger.log(); process.exit(1); @@ -70,7 +66,7 @@ exports.init = function (logger, config, cli) { emu.on('exit', function (code) { if (code) { - logger.error(__('Emulator exited with error: %s', code)); + logger.error(`Emulator exited with error: ${code}`); stderr.trim().split('\n').forEach(logger.error); logger.log(); process.exit(1); @@ -100,11 +96,11 @@ exports.init = function (logger, config, cli) { if (!deviceInfo.length) { if (builder.deviceId === 'all') { - logger.error(__('Unable to find any connected devices')); + logger.error('Unable to find any connected devices'); } else { - logger.error(__('Unable to find device "%s"', builder.deviceId)); + logger.error(`Unable to find device "${builder.deviceId}"`); } - logger.error(__('Did you unplug it?') + '\n'); + logger.error('Did you unplug it?\n'); process.exit(1); } @@ -125,12 +121,12 @@ exports.init = function (logger, config, cli) { } if (builder.buildOnly) { - logger.info(__('Performed build only, skipping installing of the application')); + logger.info('Performed build only, skipping installing of the application'); return finished(); } if (!builder.apkFile || !fs.existsSync(builder.apkFile)) { - logger.error(__('No APK file to install and run, skipping')); + logger.error('No APK file to install and run, skipping'); return finished(); } @@ -138,7 +134,7 @@ exports.init = function (logger, config, cli) { async.series([ function (next) { - logger.info(__('Making sure the adb server is running')); + logger.info('Making sure the adb server is running'); adb.startServer(next); }, @@ -147,7 +143,7 @@ exports.init = function (logger, config, cli) { return next(); } - logger.info(__('Waiting for emulator to become ready...')); + logger.info('Waiting for emulator to become ready...'); const timeout = config.get('android.emulatorStartTimeout', 2 * 60 * 1000), // 2 minute default waitUntil = Date.now() + timeout, @@ -156,9 +152,9 @@ exports.init = function (logger, config, cli) { clearInterval(timer); next(); } else if (Date.now() > waitUntil) { - logger.error(__('Emulator failed to start in a timely manner') + '\n'); - logger.log(__('The current timeout is set to %s ms', String(timeout).cyan)); - logger.log(__('You can increase this timeout by running: %s', (cli.argv.$ + ' config android.emulatorStartTimeout ').cyan) + '\n'); + logger.error('Emulator failed to start in a timely manner\n'); + logger.log(`The current timeout is set to ${String(timeout).cyan} ms`); + logger.log(`You can increase this timeout by running: ${(cli.argv.$ + ' config android.emulatorStartTimeout ').cyan}\n`); process.exit(1); } }, 250); @@ -166,51 +162,51 @@ exports.init = function (logger, config, cli) { function (next) { // install the app - logger.info(__('Installing apk: %s', builder.apkFile.cyan)); + logger.info(`Installing apk: ${builder.apkFile.cyan}`); let failCounter = 0; const installTimeout = config.get('android.appInstallTimeout', 4 * 60 * 1000); // 4 minute default let retryInterval = config.get('android.appInstallRetryInterval', 2000); // 2 second default async.eachSeries(deviceInfo, function (device, cb) { - builder.target === 'device' && logger.info(__('Installing app on device: %s', (device.model || device.manufacturer || device.id).cyan)); + builder.target === 'device' && logger.info(`Installing app on device: ${(device.model || device.manufacturer || device.id).cyan}`); let intervalTimer = null; const abortTimer = setTimeout(function () { clearTimeout(intervalTimer); - logger.error(__('Application failed to install') + '\n'); - logger.log(__('The current timeout is set to %s ms', String(installTimeout).cyan)); - logger.log(__('You can increase this timeout by running: %s', (cli.argv.$ + ' config android.appInstallTimeout ').cyan) + '\n'); + logger.error('Application failed to install\n'); + logger.log(`The current timeout is set to ${String(installTimeout).cyan} ms`); + logger.log(`You can increase this timeout by running: ${(cli.argv.$ + ' config android.appInstallTimeout ').cyan}\n`); if (++failCounter >= deviceInfo.length) { process.exit(1); } }, installTimeout); - logger.trace(__('Checking if package manager service is started')); + logger.trace('Checking if package manager service is started'); (function installApp() { adb.ps(device.id, function (err, output) { if (err || output.toString().indexOf('system_server') === -1) { - logger.trace(__('Package manager not started yet, trying again in %sms...', retryInterval)); + logger.trace(`Package manager not started yet, trying again in ${retryInterval} ms...`); intervalTimer = setTimeout(installApp, retryInterval); return; } - logger.trace(__('Package manager has started')); + logger.trace('Package manager has started'); adb.installApp(device.id, builder.apkFile, { logger: logger }, function (err) { if (err) { if (err instanceof Error && (err.message.indexOf('Could not access the Package Manager') !== -1 || err.message.indexOf('Can\'t find service: package') !== -1)) { - logger.debug(__('ADB install failed because package manager service is still starting, trying again in %sms...', retryInterval)); + logger.debug(`ADB install failed because package manager service is still starting, trying again in ${retryInterval} ms...`); intervalTimer = setTimeout(installApp, retryInterval); return; } - logger.error(__('Failed to install apk on "%s"', device.id)); + logger.error(`Failed to install apk on "${device.id}"`); err = err.toString(); err.split('\n').forEach(logger.error); if (err.indexOf('INSTALL_PARSE_FAILED_NO_CERTIFICATES') !== -1) { - logger.error(__('Make sure your keystore is signed with a compatible signature algorithm such as "SHA1withRSA" or "MD5withRSA".')); + logger.error('Make sure your keystore is signed with a compatible signature algorithm such as "SHA1withRSA" or "MD5withRSA".'); } logger.log(); process.exit(1); @@ -219,7 +215,7 @@ exports.init = function (logger, config, cli) { clearTimeout(intervalTimer); clearTimeout(abortTimer); - logger.info(__('App successfully installed')); + logger.info('App successfully installed'); cb(); }); }); @@ -233,7 +229,7 @@ exports.init = function (logger, config, cli) { function (next) { if (!cli.argv.launch) { - logger.info(__('Skipping launch of: %s', (builder.appid + '/.' + builder.classname + 'Activity').cyan)); + logger.info(`Skipping launch of: ${(builder.appid + '/.' + builder.classname + 'Activity').cyan}`); return next(true); } next(); @@ -298,7 +294,7 @@ exports.init = function (logger, config, cli) { // logcat now guarantees we get per-line output if (device.appPidRegExp) { if (displayStartLog) { - const startLogTxt = __('Start application log'); + const startLogTxt = 'Start application log'; logger.log(('-- ' + startLogTxt + ' ' + (new Array(75 - startLogTxt.length)).join('-')).grey); displayStartLog = false; } @@ -321,7 +317,7 @@ exports.init = function (logger, config, cli) { }, function () { if (--instances === 0 && !displayStartLog) { // the adb server shutdown, the emulator quit, or the device was unplugged - const endLogTxt = __('End application log'); + const endLogTxt = 'End application log'; logger.log(('-- ' + endLogTxt + ' ' + (new Array(75 - endLogTxt.length)).join('-')).grey + '\n'); endLog = true; } @@ -331,7 +327,7 @@ exports.init = function (logger, config, cli) { // listen for ctrl-c process.on('SIGINT', function () { if (!endLog && !displayStartLog) { - const endLogTxt = __('End application log'); + const endLogTxt = 'End application log'; logger.log('\r' + ('-- ' + endLogTxt + ' ' + (new Array(75 - endLogTxt.length)).join('-')).grey + '\n'); } process.exit(0); @@ -341,7 +337,7 @@ exports.init = function (logger, config, cli) { }, function (next) { - logger.info(__('Starting app: %s', (builder.appid + '/.' + builder.classname + 'Activity').cyan)); + logger.info(`Starting app: ${(builder.appid + '/.' + builder.classname + 'Activity').cyan}`); let failCounter = 0; const retryInterval = config.get('android.appStartRetryInterval', 30 * 1000), // 30 second default @@ -352,16 +348,16 @@ exports.init = function (logger, config, cli) { intervalTimer = null; const abortTimer = setTimeout(function () { clearTimeout(intervalTimer); - logger.error(__('Application failed to launch') + '\n'); - logger.log(__('The current timeout is set to %s ms', String(startTimeout).cyan)); - logger.log(__('You can increase this timeout by running: %s', (cli.argv.$ + ' config android.appStartTimeout ').cyan) + '\n'); + logger.error('Application failed to launch\n'); + logger.log(`The current timeout is set to ${String(startTimeout).cyan} ms`); + logger.log(`You can increase this timeout by running: ${(cli.argv.$ + ' config android.appStartTimeout ').cyan}\n`); if (++failCounter >= deviceInfo.length) { process.exit(1); } }, startTimeout); (function startApp() { - logger.debug(__('Trying to start the app...')); + logger.debug('Trying to start the app...'); adb.startApp(device.id, builder.appid, builder.classname + 'Activity', function (err) { // eslint-disable-line no-unused-vars if (watchingPid) { return; @@ -379,7 +375,7 @@ exports.init = function (logger, config, cli) { clearTimeout(intervalTimer); clearTimeout(abortTimer); - logger.info(__('Application pid: %s', String(pid).cyan)); + logger.info(`Application pid: ${String(pid).cyan}`); device.appPidRegExp = new RegExp('\\(\\s*' + pid + '\\):'); // eslint-disable-line security/detect-non-literal-regexp done = true; setTimeout(cb2, 0); @@ -391,7 +387,7 @@ exports.init = function (logger, config, cli) { }); intervalTimer = setTimeout(function () { - logger.debug(__('App still not started, trying again')); + logger.debug('App still not started, trying again'); startApp(); }, retryInterval); }()); @@ -400,7 +396,7 @@ exports.init = function (logger, config, cli) { function (next) { if (builder.debugPort) { - logger.info(__('Forwarding host port %s to device for debugging', builder.debugPort)); + logger.info(`Forwarding host port ${builder.debugPort} to device for debugging`); const forwardPort = 'tcp:' + builder.debugPort; async.series(deviceInfo.map(function (device) { return function (cb) { @@ -414,7 +410,7 @@ exports.init = function (logger, config, cli) { function (next) { if (builder.profilerPort) { - logger.info(__('Forwarding host port %s to device for profiling', builder.profilerPort)); + logger.info(`Forwarding host port ${builder.profilerPort} to device for profiling}`); const forwardPort = 'tcp:' + builder.profilerPort; async.series(deviceInfo.map(function (device) { return function (cb) { @@ -434,5 +430,4 @@ exports.init = function (logger, config, cli) { }); } }); - -}; +} diff --git a/android/cli/lib/android-manifest.js b/android/cli/lib/android-manifest.js index fec4761eca7..279f89218d3 100644 --- a/android/cli/lib/android-manifest.js +++ b/android/cli/lib/android-manifest.js @@ -17,18 +17,16 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const DOMParser = require('xmldom').DOMParser; -const fs = require('fs-extra'); -const os = require('os'); +import { DOMParser } from 'xmldom'; +import fs from 'fs-extra'; +import os from 'node:os'; /** * Class used to load, merge, edit, and save "AndroidManifest.xml" files. * * You are expected to load XML files via the static fromFilePath() or fromXmlString() methods. */ -class AndroidManifest { +export class AndroidManifest { /** * Creates a new AndroidManifest instance wrapping the given "xmldom.Document" object. * @param {Object} xmlDomDocument @@ -820,5 +818,3 @@ function applyToolsReplaceToElement(element) { element.removeAttribute('tools:replace'); } } - -module.exports = AndroidManifest; diff --git a/android/cli/lib/detect.js b/android/cli/lib/detect.js index b4ef3d25cab..3a66147cc55 100644 --- a/android/cli/lib/detect.js +++ b/android/cli/lib/detect.js @@ -11,13 +11,9 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const android = require('node-titanium-sdk/lib/android'), - ADB = require('node-titanium-sdk/lib/adb'), - EmulatorManager = require('node-titanium-sdk/lib/emulator'), - appc = require('node-appc'), - __ = appc.i18n(__dirname).__; +import { detect } from 'node-titanium-sdk/lib/android.js'; +import ADB from 'node-titanium-sdk/lib/adb.js'; +import EmulatorManager from 'node-titanium-sdk/lib/emulator.js'; /** * Detects current Android environment. @@ -26,7 +22,7 @@ const android = require('node-titanium-sdk/lib/android'), * @param {Object} opts - Detection options; currently only 'bypassCache' * @param {Function} finished - Callback when detection is finished */ -exports.detect = android.detect; +export { detect }; /** * Detects connected Android emulators. @@ -35,7 +31,7 @@ exports.detect = android.detect; * @param {String} [opts.type] - The type of emulator to load (avd, genymotion); defaults to all * @param {Function} finished - Callback when detection is finished */ -exports.detectEmulators = function detectEmulators(config, opts, finished) { +export function detectEmulators(config, opts, finished) { if (opts && typeof opts === 'function') { finished = opts; opts = {}; @@ -48,14 +44,14 @@ exports.detectEmulators = function detectEmulators(config, opts, finished) { finished(null, emus); } }); -}; +} /** * Detects connected Android devices. * @param {Object} config - The CLI config object * @param {Function} finished - Callback when detection is finished */ -exports.detectDevices = function detectDevices(config, finished) { +export function detectDevices(config, finished) { new ADB(config).devices(function (err, devices) { if (err) { return finished(err); @@ -64,8 +60,8 @@ exports.detectDevices = function detectDevices(config, finished) { finished(null, devices.filter(function (d) { return !d.emulator; }).map(function (d) { - d.name = d.model || d.manufacturer || d.name || (d.release ? __('Android %s Device', d.release) : __('Android Device')); + d.name = d.model || d.manufacturer || d.name || (d.release ? `Android ${d.release} Device` : 'Android Device'); return d; })); }); -}; +} diff --git a/android/cli/lib/gradle-wrapper.js b/android/cli/lib/gradle-wrapper.js index 0cc8e60a064..fcb41029219 100644 --- a/android/cli/lib/gradle-wrapper.js +++ b/android/cli/lib/gradle-wrapper.js @@ -11,13 +11,11 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const appc = require('node-appc'); -const exec = require('child_process').exec; // eslint-disable-line security/detect-child-process -const fs = require('fs-extra'); -const path = require('path'); -const url = require('url'); +import appc from 'node-appc'; +import { exec } from 'node:child_process'; +import fs from 'fs-extra'; +import path from 'node:path'; +import url from 'node:url'; /** * Determines if we're running on a Windows machine. @@ -39,7 +37,7 @@ const isWindows = (process.platform === 'win32'); */ /** Class used to install, configure, and run gradle in a root project directory. */ -class GradleWrapper { +export class GradleWrapper { /** * Creates an object used to install/configure/run the "gradlew" script at the given root project directory. * @param {String} directoryPath @@ -207,6 +205,7 @@ class GradleWrapper { // Run the gradlew command line async. await new Promise((resolve, reject) => { + // eslint-disable-next-line security/detect-child-process const childProcess = exec(commandLineString, { cwd: this._gradlewDirPath }); if (this._logger) { childProcess.stdout.on('data', createReadableDataHandlerUsing(this._logger, 'info')); @@ -474,5 +473,3 @@ async function writeJavaPropertiesFile(filePath, properties) { // Create the properties files with the text lines generated above. await fs.writeFile(filePath, fileLines.join('\n') + '\n'); } - -module.exports = GradleWrapper; diff --git a/android/cli/lib/info.js b/android/cli/lib/info.js index 1c84a20630a..004ccdd23d1 100644 --- a/android/cli/lib/info.js +++ b/android/cli/lib/info.js @@ -1,14 +1,14 @@ -'use strict'; -const appc = require('node-appc'), - __ = appc.i18n(__dirname).__, - fs = require('fs'), - path = require('path'); +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -exports.name = 'android'; +export const name = 'android'; -exports.title = 'Android'; +export const title = 'Android'; -exports.detect = function (types, config, next) { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export function detect(types, config, next) { const tisdk = path.basename((function scan(dir) { const file = path.join(dir, 'manifest.json'); if (fs.existsSync(file)) { @@ -18,45 +18,45 @@ exports.detect = function (types, config, next) { return dir !== '/' && scan(dir); }(__dirname))); - const mod = require('./detect'); - - // detect android environment - mod.detect(config, null, function (result) { - // detect devices - mod.detectDevices(config, function (err, devices) { - // detect emulators - mod.detectEmulators(config, function (err, emus) { - result.tisdk = tisdk; - result.devices = devices; - result.emulators = emus; - delete result.avds; - - this.data = result; - if (result.issues.length) { - this.issues = this.issues.concat(result.issues); - } - - next(null, { android: result }); + import('./detect.js').then(({ default: mod }) => { + // detect android environment + mod.detect(config, null, function (result) { + // detect devices + mod.detectDevices(config, function (err, devices) { + // detect emulators + mod.detectEmulators(config, function (err, emus) { + result.tisdk = tisdk; + result.devices = devices; + result.emulators = emus; + delete result.avds; + + this.data = result; + if (result.issues.length) { + this.issues = this.issues.concat(result.issues); + } + + next(null, { android: result }); + }.bind(this)); }.bind(this)); }.bind(this)); - }.bind(this)); -}; + }).catch(next); +} -exports.render = function (logger, config, rpad, styleHeading, styleValue, styleBad) { +export function render(logger, config, rpad, styleHeading, styleValue, styleBad) { var data = this.data; if (!data) { return; } - logger.log(styleHeading(__('Android SDK')) + '\n' - + ' ' + rpad(__('Android Executable')) + ' = ' + styleValue(data.sdk && data.sdk.executables.android || __('not found')) + '\n' - + ' ' + rpad(__('ADB Executable')) + ' = ' + styleValue(data.sdk && data.sdk.executables.adb || __('not found')) + '\n' - + ' ' + rpad(__('SDK Path')) + ' = ' + styleValue(data.sdk && data.sdk.path || __('not found')) + '\n' + logger.log(styleHeading('Android SDK') + '\n' + + ' ' + rpad('Android Executable') + ' = ' + styleValue(data.sdk && data.sdk.executables.android || 'not found') + '\n' + + ' ' + rpad('ADB Executable') + ' = ' + styleValue(data.sdk && data.sdk.executables.adb || 'not found') + '\n' + + ' ' + rpad('SDK Path') + ' = ' + styleValue(data.sdk && data.sdk.path || 'not found') + '\n' ); - logger.log(styleHeading(__('Android NDK')) + '\n' - + ' ' + rpad(__('NDK Path')) + ' = ' + styleValue(data.ndk && data.ndk.path || __('not found')) + '\n' - + ' ' + rpad(__('NDK Version')) + ' = ' + styleValue(data.ndk && data.ndk.version || __('not found')) + '\n' + logger.log(styleHeading('Android NDK') + '\n' + + ' ' + rpad('NDK Path') + ' = ' + styleValue(data.ndk && data.ndk.path || 'not found') + '\n' + + ' ' + rpad('NDK Version') + ' = ' + styleValue(data.ndk && data.ndk.version || 'not found') + '\n' ); let androidPlatforms = '', @@ -67,10 +67,10 @@ exports.render = function (logger, config, rpad, styleHeading, styleValue, style Object.keys(data.targets).forEach(function (targetId) { var target = data.targets[targetId], supported = (target.supported === 'maybe' - ? (' (' + __('not supported by Titanium SDK %s, but may work', data.tisdk) + ')').yellow + ? (` (not supported by Titanium SDK ${data.tisdk}, but may work)`).yellow : target.supported ? '' - : styleBad(' **' + __('Not supported by Titanium SDK %s', data.tisdk) + '**')); + : styleBad(` **Not supported by Titanium SDK ${data.tisdk}**`)); if (target.type === 'platform') { const m = target.name.match(/Android\s+(\d(?:\.\d(?:\.\d)?)?)/); @@ -78,40 +78,40 @@ exports.render = function (logger, config, rpad, styleHeading, styleValue, style apiLevelMap[m[1]] = target['api-level']; } androidPlatforms += ' ' + (targetId + ') ' + target.id).cyan + '\n' - + ' ' + rpad(' ' + __('Name')) + ' = ' + styleValue(target.name) + supported + '\n' - + ' ' + rpad(' ' + __('API Level')) + ' = ' + styleValue(target['api-level']) + '\n' - + ' ' + rpad(' ' + __('Revision')) + ' = ' + styleValue(target.revision) + '\n' - + ' ' + rpad(' ' + __('Skins')) + ' = ' + styleValue(target.skins.join(', ')) + '\n' - + ' ' + rpad(' ' + __('ABIs')) + ' = ' + styleValue(target.abis.join(', ')) + '\n' - + ' ' + rpad(' ' + __('Path')) + ' = ' + styleValue(target.path) + '\n'; + + ' ' + rpad(' Name') + ' = ' + styleValue(target.name) + supported + '\n' + + ' ' + rpad(' API Level') + ' = ' + styleValue(target['api-level']) + '\n' + + ' ' + rpad(' Revision') + ' = ' + styleValue(target.revision) + '\n' + + ' ' + rpad(' Skins') + ' = ' + styleValue(target.skins.join(', ')) + '\n' + + ' ' + rpad(' ABIs') + ' = ' + styleValue(target.abis.join(', ')) + '\n' + + ' ' + rpad(' Path') + ' = ' + styleValue(target.path) + '\n'; } else if (target.type === 'add-on') { androidAddons += ' ' + (targetId + ') ' + target.id).cyan + '\n' - + ' ' + rpad(' ' + __('Name')) + ' = ' + styleValue(target.name - + ' (' + (target['based-on'] ? __('Android %s (API level %s)', target['based-on']['android-version'], target['based-on']['api-level']) : __('unknown')) + ')') + supported + '\n' - + ' ' + rpad(' ' + __('Vendor')) + ' = ' + styleValue(target.vendor || __('n/a')) + '\n' - + ' ' + rpad(' ' + __('Revision')) + ' = ' + styleValue(target.revision) + '\n' - + ' ' + rpad(' ' + __('Description')) + ' = ' + styleValue(target.description || __('n/a')) + '\n' - + ' ' + rpad(' ' + __('Skins')) + ' = ' + styleValue(target.skins && target.skins.length ? target.skins.join(', ') : __('none')) + '\n' - + ' ' + rpad(' ' + __('ABIs')) + ' = ' + styleValue(target.abis && target.abis.length ? target.abis.join(', ') : __('none')) + '\n' - + ' ' + rpad(' ' + __('Path')) + ' = ' + styleValue(target.path) + '\n'; + + ' ' + rpad(' Name') + ' = ' + styleValue(target.name + + ' (' + (target['based-on'] ? `Android ${target['based-on']['android-version']} (API level ${target['based-on']['api-level']})` : 'unknown') + ')') + supported + '\n' + + ' ' + rpad(' Vendor') + ' = ' + styleValue(target.vendor || 'n/a') + '\n' + + ' ' + rpad(' Revision') + ' = ' + styleValue(target.revision) + '\n' + + ' ' + rpad(' Description') + ' = ' + styleValue(target.description || 'n/a') + '\n' + + ' ' + rpad(' Skins') + ' = ' + styleValue(target.skins && target.skins.length ? target.skins.join(', ') : 'none') + '\n' + + ' ' + rpad(' ABIs') + ' = ' + styleValue(target.abis && target.abis.length ? target.abis.join(', ') : 'none') + '\n' + + ' ' + rpad(' Path') + ' = ' + styleValue(target.path) + '\n'; if (target.libraries && Object.keys(target.libraries).length) { Object.keys(target.libraries).forEach(function (lib, i) { - androidAddons += ' ' + (i === 0 ? rpad(' ' + __('Libraries')) + ' = ' : rpad('') + ' ') + androidAddons += ' ' + (i === 0 ? rpad(' Libraries') + ' = ' : rpad('') + ' ') + styleValue(lib + ': ' + target.libraries[lib].description + ' (' + target.libraries[lib].jar + ')') + '\n'; }); androidAddons += '\n'; } else { - androidAddons += ' ' + rpad(' ' + __('Libraries')) + ' = ' + styleValue(__('none')) + '\n'; + androidAddons += ' ' + rpad(' Libraries') + ' = ' + styleValue('none') + '\n'; } } }); } - logger.log(styleHeading(__('Android Platforms')) + '\n' + (androidPlatforms ? androidPlatforms : ' ' + __('None').grey + '\n')); - logger.log(styleHeading(__('Android Add-Ons')) + '\n' + (androidAddons ? androidAddons : ' ' + __('None').grey + '\n')); + logger.log(styleHeading('Android Platforms') + '\n' + (androidPlatforms ? androidPlatforms : ' ' + 'none'.grey + '\n')); + logger.log(styleHeading('Android Add-Ons') + '\n' + (androidAddons ? androidAddons : ' ' + 'none'.grey + '\n')); - logger.log(styleHeading(__('Android Emulators'))); + logger.log(styleHeading('Android Emulators')); if (data.emulators) { const emus = data.emulators.filter(function (e) { return e.type === 'avd'; @@ -119,82 +119,49 @@ exports.render = function (logger, config, rpad, styleHeading, styleValue, style if (emus.length) { logger.log(emus.map(function (emu) { return ' ' + emu.name.cyan + '\n' - + ' ' + rpad(' ' + __('ID')) + ' = ' + styleValue(emu.id) + '\n' - + ' ' + rpad(' ' + __('SDK Version')) + ' = ' + styleValue(emu.target || __('not installed')) + '\n' - + ' ' + rpad(' ' + __('ABI')) + ' = ' + styleValue(emu.abi) + '\n' - + ' ' + rpad(' ' + __('Skin')) + ' = ' + styleValue(emu.skin) + '\n' - + ' ' + rpad(' ' + __('Path')) + ' = ' + styleValue(emu.path) + '\n' - + ' ' + rpad(' ' + __('SD Card')) + ' = ' + styleValue(emu.sdcard || __('no sd card')) + '\n' + + ' ' + rpad(' ID') + ' = ' + styleValue(emu.id) + '\n' + + ' ' + rpad(' SDK Version') + ' = ' + styleValue(emu.target || 'not installed') + '\n' + + ' ' + rpad(' ABI') + ' = ' + styleValue(emu.abi) + '\n' + + ' ' + rpad(' Skin') + ' = ' + styleValue(emu.skin) + '\n' + + ' ' + rpad(' Path') + ' = ' + styleValue(emu.path) + '\n' + + ' ' + rpad(' SD Card') + ' = ' + styleValue(emu.sdcard || 'no sd card') + '\n' + (emu['based-on'] - ? ' ' + rpad(' ' + __('Based On')) + ' = ' + styleValue(__('Android %s (API level %s)', emu['based-on']['android-version'], emu['based-on']['api-level'])) + '\n' + ? ' ' + rpad(' Based On') + ' = ' + styleValue(`Android ${emu['based-on']['android-version']} (API level ${emu['based-on']['api-level']})`) + '\n' : '' ) - + ' ' + rpad(' ' + __('Google APIs')) + ' = ' + styleValue(emu.googleApis ? __('yes') : __('no')); + + ' ' + rpad(' Google APIs') + ' = ' + styleValue(emu.googleApis ? 'yes' : 'no'); }).join('\n') + '\n'); } else { - logger.log(' ' + __('None').grey + '\n'); + logger.log(' ' + 'none'.grey + '\n'); } } else { - logger.log(' ' + __('None').grey + '\n'); + logger.log(' ' + 'none'.grey + '\n'); } - logger.log(styleHeading(__('Genymotion Emulators'))); - if (data.emulators) { - const emus = data.emulators.filter(function (e) { - return e.type === 'genymotion'; - }); - if (emus.length) { - logger.log(emus.map(function (emu) { - return ' ' + emu.name.cyan + '\n' - + +' ' + rpad(' ' + __('ID')) + ' = ' + styleValue(emu.id) + '\n' - + ' ' + rpad(' ' + __('SDK Version')) + ' = ' + styleValue(emu.target + (apiLevelMap[emu.target] ? ' (android-' + apiLevelMap[emu.target] + ')' : '')) + '\n' - + ' ' + rpad(' ' + __('ABI')) + ' = ' + styleValue(emu.abi || __('unknown')) + '\n' - + ' ' + rpad(' ' + __('Genymotion Version')) + ' = ' + styleValue(emu.genymotion || __('unknown')) + '\n' - + ' ' + rpad(' ' + __('Display')) + ' = ' + styleValue(emu.display || __('unknown')) + '\n' - + ' ' + rpad(' ' + __('DPI')) + ' = ' + styleValue(emu.dpi || __('unknown')) + '\n' - + ' ' + rpad(' ' + __('OpenGL Acceleration')) + ' = ' + styleValue(emu.hardwareOpenGL ? __('yes') : __('no')) + '\n' - + ' ' + rpad(' ' + __('Google APIs')) + ' = ' + styleValue(emu.googleApis === null ? __('unknown, emulator not running') : emu.googleApis ? __('yes') : __('no')); - }).join('\n') + '\n'); - } else { - logger.log(' ' + __('None').grey + '\n'); - } - } else { - logger.log(' ' + __('None').grey + '\n'); - } - - logger.log(styleHeading(__('Connected Android Devices'))); + logger.log(styleHeading('Connected Android Devices')); if (data.devices && data.devices.length) { logger.log(data.devices.map(function (device) { var name = device.name, result = [ - ' ' + rpad(__('ID')) + ' = ' + styleValue(device.id), - ' ' + rpad(__('State')) + ' = ' + styleValue(device.state) + ' ' + rpad('ID') + ' = ' + styleValue(device.id), + ' ' + rpad('State') + ' = ' + styleValue(device.state) ]; if (device.release) { - result.push(' ' + rpad(__('SDK Version')) + ' = ' + styleValue(device.release + ' (android-' + device.sdk + ')')); + result.push(' ' + rpad('SDK Version') + ' = ' + styleValue(device.release + ' (android-' + device.sdk + ')')); } if (Array.isArray(device.abi)) { - result.push(' ' + rpad(__('ABIs')) + ' = ' + styleValue(device.abi.join(', '))); + result.push(' ' + rpad('ABIs') + ' = ' + styleValue(device.abi.join(', '))); } if (device.emulator) { switch (device.emulator.type) { case 'avd': name = 'Android Emulator: ' + device.emulator.name; - result.push(' ' + rpad(__('Skin')) + ' = ' + styleValue(device.emulator.skin || __('unknown'))); - result.push(' ' + rpad(__('SD Card')) + ' = ' + styleValue(device.emulator.sdcard || __('unknown'))); - result.push(' ' + rpad(__('Google APIs')) + ' = ' + styleValue(device.emulator.googleApis ? __('yes') : __('no'))); - break; - - case 'genymotion': - name = 'Genymotion Emulator: ' + device.emulator.name; - result.push(' ' + rpad(__('Genymotion Version')) + ' = ' + styleValue(device.emulator.genymotion || __('unknown'))); - result.push(' ' + rpad(__('Display')) + ' = ' + styleValue(device.emulator.display || __('unknown'))); - result.push(' ' + rpad(__('DPI')) + ' = ' + styleValue(device.emulator.dpi || __('unknown'))); - result.push(' ' + rpad(__('OpenGL Acceleration')) + ' = ' + styleValue(device.emulator.hardwareOpenGL ? __('yes') : __('no'))); - result.push(' ' + rpad(__('Google APIs')) + ' = ' + styleValue(device.emulator.googleApis ? __('yes') : __('no'))); + result.push(' ' + rpad('Skin') + ' = ' + styleValue(device.emulator.skin || 'unknown')); + result.push(' ' + rpad('SD Card') + ' = ' + styleValue(device.emulator.sdcard || 'unknown')); + result.push(' ' + rpad('Google APIs') + ' = ' + styleValue(device.emulator.googleApis ? 'yes' : 'no')); break; } @@ -204,6 +171,6 @@ exports.render = function (logger, config, rpad, styleHeading, styleValue, style } }).join('\n') + '\n'); } else { - logger.log(' ' + __('None').grey + '\n'); + logger.log(' ' + 'none'.grey + '\n'); } -}; +} diff --git a/android/cli/lib/process-drawables-task.js b/android/cli/lib/process-drawables-task.js index 61814bb126f..c168a72aa88 100644 --- a/android/cli/lib/process-drawables-task.js +++ b/android/cli/lib/process-drawables-task.js @@ -1,16 +1,12 @@ -const path = require('path'); -const CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task'); - -const appc = require('node-appc'); -const i18n = appc.i18n(__dirname); -const __ = i18n.__; +import path from 'node:path'; +import { CopyResourcesTask } from '../../../cli/lib/tasks/copy-resources-task.js'; const drawableDpiRegExp = /^(high|medium|low)$/; /** * Task that copies Android drawables into the app. */ -class ProcessDrawablesTask extends CopyResourcesTask { +export class ProcessDrawablesTask extends CopyResourcesTask { /** * Constructs a new processing task. @@ -39,7 +35,7 @@ class ProcessDrawablesTask extends CopyResourcesTask { // We have a drawable image file. (Rename it if it contains invalid characters.) const warningMessages = []; if (parts.length > 3) { - warningMessages.push(__('- Files cannot be put into subdirectories.')); + warningMessages.push('- Files cannot be put into subdirectories.'); // retain subdirs under the res- folder to be mangled into the destination filename // i.e. take images/res-mdpi/logos/app.png and store logos/app, which below will become logos_app.png base = parts.slice(2, parts.length - 1).join(path.sep) + path.sep + base; @@ -48,24 +44,22 @@ class ProcessDrawablesTask extends CopyResourcesTask { // basename may have .9 suffix, if so, we do not want to convert that .9 to _9 let destFilteredFilename = `${base.toLowerCase().replace(/(?!\.9$)[^a-z0-9_]/g, '_')}.${info.ext}`; if (destFilteredFilename !== destFilename) { - warningMessages.push(__('- Names must contain only lowercase a-z, 0-9, or underscore.')); + warningMessages.push('- Names must contain only lowercase a-z, 0-9, or underscore.'); } if (/^\d/.test(destFilteredFilename)) { - warningMessages.push(__('- Names cannot start with a number.')); + warningMessages.push('- Names cannot start with a number.'); destFilteredFilename = `_${destFilteredFilename}`; } if (warningMessages.length > 0) { // relPath here is relative the the folder we searched, NOT the project dir, so make full path relative to project dir for log - logger.warn(__(`Invalid "res" file: ${path.relative(builder.projectDir, info.src)}`)); + logger.warn(`Invalid "res" file: ${path.relative(builder.projectDir, info.src)}`); for (const nextMessage of warningMessages) { logger.warn(nextMessage); } - logger.warn(__(`- Titanium will rename to: ${destFilteredFilename}`)); + logger.warn(`- Titanium will rename to: ${destFilteredFilename}`); } info.dest = path.join(appMainResDir, foldername, destFilteredFilename); }); super(options); } } - -module.exports = ProcessDrawablesTask; diff --git a/android/cli/lib/process-splashes-task.js b/android/cli/lib/process-splashes-task.js index 4e838822238..07dcd365a9b 100644 --- a/android/cli/lib/process-splashes-task.js +++ b/android/cli/lib/process-splashes-task.js @@ -1,12 +1,12 @@ -const path = require('path'); -const CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task'); +import path from 'node:path'; +import { CopyResourcesTask } from '../../../cli/lib/tasks/copy-resources-task.js'; const drawableDpiRegExp = /^(high|medium|low)$/; /** * Task that copies Android splash screens into the app. */ -class ProcessSplashesTask extends CopyResourcesTask { +export class ProcessSplashesTask extends CopyResourcesTask { /** * Constructs a new processing task. @@ -43,5 +43,3 @@ class ProcessSplashesTask extends CopyResourcesTask { super(options); } } - -module.exports = ProcessSplashesTask; diff --git a/android/cli/locales/bn.js b/android/cli/locales/bn.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/bn.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/de.js b/android/cli/locales/de.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/de.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/el.js b/android/cli/locales/el.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/el.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/en.js b/android/cli/locales/en.js deleted file mode 100644 index 5537438a580..00000000000 --- a/android/cli/locales/en.js +++ /dev/null @@ -1,375 +0,0 @@ -{ - "No valid Android SDK targets found.": "No valid Android SDK targets found.", - "No Android SDK targets found.": "No Android SDK targets found.", - "Please download an Android SDK target API level %s or newer from the Android SDK Manager and try again.": "Please download an Android SDK target API level %s or newer from the Android SDK Manager and try again.", - "Unable to locate the Java Development Kit": "Unable to locate the Java Development Kit", - "You can specify the location by setting the %s environment variable.": "You can specify the location by setting the %s environment variable.", - "JDK version %s detected, but only version %s is supported": "JDK version %s detected, but only version %s is supported", - "the alias for the keystore": "the alias for the keystore", - "What is the name of the keystore's certificate alias?": "What is the name of the keystore's certificate alias?", - "Select a certificate alias by number or name": "Select a certificate alias by number or name", - "Invalid \"--alias\" value \"%s\"": "Invalid \"--alias\" value \"%s\"", - "Certificates that use the %s or %s signature algorithm will provide better compatibility.": "Certificates that use the %s or %s signature algorithm will provide better compatibility.", - "the path to the Android SDK": "the path to the Android SDK", - "path": "path", - "Where is the Android SDK?": "Where is the Android SDK?", - "Invalid Android SDK path": "Invalid Android SDK path", - "The Android SDK path cannot contain ampersands (&) on Windows": "The Android SDK path cannot contain ampersands (&) on Windows", - "Invalid Android SDK path: %s": "Invalid Android SDK path: %s", - "the abi for the Android emulator; deprecated, use --device-id": "the abi for the Android emulator; deprecated, use --device-id", - "abi": "abi", - "the id for the Android emulator; deprecated, use --device-id": "the id for the Android emulator; deprecated, use --device-id", - "id": "id", - "the skin for the Android emulator; deprecated, use --device-id": "the skin for the Android emulator; deprecated, use --device-id", - "skin": "skin", - "the type of deployment; only used when target is %s or %s": "the type of deployment; only used when target is %s or %s", - "type": "type", - "the name of the Android emulator or the device id to install the application to": "the name of the Android emulator or the device id to install the application to", - "name": "name", - "Devices": "Devices", - "Which device do you want to install your app on?": "Which device do you want to install your app on?", - "Select a device by number or name": "Select a device by number or name", - "Android Emulators": "Android Emulators", - "Genymotion Emulators": "Genymotion Emulators", - "NOTE: Genymotion emulator must be running to detect Google API support": "NOTE: Genymotion emulator must be running to detect Google API support", - "Which emulator do you want to launch your app in?": "Which emulator do you want to launch your app in?", - "Select an emulator by number or name": "Select an emulator by number or name", - "Unable to find any devices": "Unable to find any devices", - "Please plug in an Android device, then try again.": "Please plug in an Android device, then try again.", - "Unable to find any emulators": "Unable to find any emulators", - "Please create an Android emulator, then try again.": "Please create an Android emulator, then try again.", - "Google APIs supported": "Google APIs supported", - "Google APIs support unknown": "Google APIs support unknown", - "Invalid Android device \"%s\"": "Invalid Android device \"%s\"", - "Invalid Android emulator \"%s\"": "Invalid Android emulator \"%s\"", - "Found %s avd with id \"%%s\"": { - "one": "Found %s avd with id \"%%s\"", - "other": "Found %s avds with id \"%%s\"" - }, - "Specify --avd-skin and --avd-abi to select a specific emulator": "Specify --avd-skin and --avd-abi to select a specific emulator", - "No emulators found with id \"%s\" and skin \"%s\"": "No emulators found with id \"%s\" and skin \"%s\"", - "Found %s avd with id \"%%s\" and skin \"%%s\"": { - "one": "Found %s avd with id \"%%s\" and skin \"%%s\"", - "other": "Found %s avds with id \"%%s\" and skin \"%%s\"" - }, - "Specify --avd-abi to select a specific emulator": "Specify --avd-abi to select a specific emulator", - "No emulators found with id \"%s\", skin \"%s\", and abi \"%s\"": "No emulators found with id \"%s\", skin \"%s\", and abi \"%s\"", - "%s options have been %s, please use %s": "%s options have been %s, please use %s", - "Available Emulators:": "Available Emulators:", - "the password for the keystore private key (defaults to the store-password)": "the password for the keystore private key (defaults to the store-password)", - "the type of a digital signature algorithm. only used when overriding keystore signing algorithm": "the type of a digital signature algorithm. only used when overriding keystore signing algorithm", - "What is the keystore's __key password__?": "What is the keystore's __key password__?", - "(leave blank to use the store password)": "(leave blank to use the store password)", - "Bad key password": "Bad key password", - "the location of the keystore file": "the location of the keystore file", - "Where is the __keystore file__ used to sign the app?": "Where is the __keystore file__ used to sign the app?", - "Please specify the path to your keystore file": "Please specify the path to your keystore file", - "Invalid keystore file": "Invalid keystore file", - "the output directory when using %s": "the output directory when using %s", - "Where would you like the output APK file saved?": "Where would you like the output APK file saved?", - "Invalid output directory": "Invalid output directory", - "the password for the keystore": "the password for the keystore", - "What is the keystore's __password__?": "What is the keystore's __password__?", - "Please specify a keystore password": "Please specify a keystore password", - "Keystore does not contain any certificates": "Keystore does not contain any certificates", - "the target to build for": "the target to build for", - "The %s tiapp.xml property has been deprecated, please use the %s option": "The %s tiapp.xml property has been deprecated, please use the %s option", - "The %s tiapp.xml property has been deprecated, please use the %s option to bypass JavaScript minification": "The %s tiapp.xml property has been deprecated, please use the %s option to bypass JavaScript minification", - "The app name \"%s\" contains an ampersand (&) which will most likely cause problems.": "The app name \"%s\" contains an ampersand (&) which will most likely cause problems.", - "It is recommended that you define the app name using i18n strings.": "It is recommended that you define the app name using i18n strings.", - "Refer to %s for more information.": "Refer to %s for more information.", - "To allow ampersands in the app name, run:": "To allow ampersands in the app name, run:", - "tiapp.xml contains an invalid app id \"%s\"": "tiapp.xml contains an invalid app id \"%s\"", - "The app id must consist only of letters, numbers, dashes, and underscores.": "The app id must consist only of letters, numbers, dashes, and underscores.", - "Note: Android does not allow dashes.": "Note: Android does not allow dashes.", - "The first character must be a letter or underscore.": "The first character must be a letter or underscore.", - "Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)": "Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)", - "The app id must consist of letters, numbers, and underscores.": "The app id must consist of letters, numbers, and underscores.", - "The first character after a period must not be a number.": "The first character after a period must not be a number.", - "Invalid app id \"%s\"": "Invalid app id \"%s\"", - "The app id must not contain Java reserved words.": "The app id must not contain Java reserved words.", - "Invalid \"ti.ui.defaultunit\" property value \"%s\"": "Invalid \"ti.ui.defaultunit\" property value \"%s\"", - "Valid units:": "Valid units:", - "Missing ProGuard configuration file": "Missing ProGuard configuration file", - "ProGuard settings must go in the file \"%s\"": "ProGuard settings must go in the file \"%s\"", - "For example configurations, visit %s": "For example configurations, visit %s", - "Malformed definition in the section of the tiapp.xml": "Malformed definition in the section of the tiapp.xml", - "Malformed custom AndroidManifest.xml file: %s": "Malformed custom AndroidManifest.xml file: %s", - "%s has been deprecated, please specify the target SDK using the %s tag:": "%s has been deprecated, please specify the target SDK using the %s tag:", - "The minimum supported SDK version must be %s or newer, but is currently set to %s": "The minimum supported SDK version must be %s or newer, but is currently set to %s", - "Update the %s in the tiapp.xml or custom AndroidManifest to at least %s:": "Update the %s in the tiapp.xml or custom AndroidManifest to at least %s:", - "The target SDK version must be %s or newer, but is currently set to %s": "The target SDK version must be %s or newer, but is currently set to %s", - "The target SDK must be greater than or equal to the minimum SDK %s, but is currently set to %s": "The target SDK must be greater than or equal to the minimum SDK %s, but is currently set to %s", - "Unable to find a suitable installed Android SDK that is >=%s and <=%s": "Unable to find a suitable installed Android SDK that is >=%s and <=%s", - "Target Android SDK %s is not installed": "Target Android SDK %s is not installed", - "To target Android SDK %s, you first must install it using the Android SDK manager.": "To target Android SDK %s, you first must install it using the Android SDK manager.", - "Alternatively, you can set the %s in the %s section of the tiapp.xml to one of the following installed Android target SDKs: %s": "Alternatively, you can set the %s in the %s section of the tiapp.xml to one of the following installed Android target SDKs: %s", - "To target Android SDK %s, you first must install it using the Android SDK manager": "To target Android SDK %s, you first must install it using the Android SDK manager", - "Target Android SDK %s is missing \"android.jar\"": "Target Android SDK %s is missing \"android.jar\"", - "Target Android SDK version must be %s or newer": "Target Android SDK version must be %s or newer", - "Maximum Android SDK version must be greater than or equal to the target SDK %s, but is currently set to %s": "Maximum Android SDK version must be greater than or equal to the target SDK %s, but is currently set to %s", - "Building with Android SDK %s which hasn't been tested against Titanium SDK %s": "Building with Android SDK %s which hasn't been tested against Titanium SDK %s", - "Invalid ABI \"%s\"": "Invalid ABI \"%s\"", - "Valid ABIs:": "Valid ABIs:", - "Auto selecting device that closest matches %s": "Auto selecting device that closest matches %s", - "Auto selecting emulator that closest matches %s": "Auto selecting emulator that closest matches %s", - "Auto selected device %s %s": "Auto selected device %s %s", - "Auto selected emulator %s %s": "Auto selected emulator %s %s", - "Searching for version >= %s and has Google APIs": "Searching for version >= %s and has Google APIs", - "Searching for version >= %s and may have Google APIs": "Searching for version >= %s and may have Google APIs", - "Searching for version >= %s and no Google APIs": "Searching for version >= %s and no Google APIs", - "Searching for version < %s and has Google APIs": "Searching for version < %s and has Google APIs", - "Searching for version < %s and no Google APIs": "Searching for version < %s and no Google APIs", - "Selecting first device": "Selecting first device", - "The emulator \"%%s\" does not support the desired ABI %%s": { - "one": "The emulator \"%%s\" does not support the desired ABI %%s", - "other": "The emulator \"%%s\" does not support the desired ABIs %%s" - }, - "The device \"%%s\" does not support the desired ABI %%s": { - "one": "The device \"%%s\" does not support the desired ABI %%s", - "other": "The device \"%%s\" does not support the desired ABIs %%s" - }, - "Supported ABIs: %s": "Supported ABIs: %s", - "You need to add at least one of the device's supported ABIs to the tiapp.xml": "You need to add at least one of the device's supported ABIs to the tiapp.xml", - "Invalid %s host \"%s\"": "Invalid %s host \"%s\"", - "The %s host must be in the format \"host:port\".": "The %s host must be in the format \"host:port\".", - "The port must be a valid integer between 1 and 65535.": "The port must be a valid integer between 1 and 65535.", - "Unable find emulator \"%s\"": "Unable find emulator \"%s\"", - "The selected emulator \"%s\" does not have an SD card.": "The selected emulator \"%s\" does not have an SD card.", - "An SD card is required for profiling.": "An SD card is required for profiling.", - "An SD card is required for debugging.": "An SD card is required for debugging.", - "Cannot debug application when --device-id is set to \"all\" and more than one device is connected.": "Cannot debug application when --device-id is set to \"all\" and more than one device is connected.", - "Please specify a single device to debug on.": "Please specify a single device to debug on.", - "The build directory is not writeable: %s": "The build directory is not writeable: %s", - "Make sure the build directory is writeable and that you have sufficient free disk space.": "Make sure the build directory is writeable and that you have sufficient free disk space.", - "The project directory is not writeable: %s": "The project directory is not writeable: %s", - "Make sure the project directory is writeable and that you have sufficient free disk space.": "Make sure the project directory is writeable and that you have sufficient free disk space.", - "Could not find all required Titanium Modules:": "Could not find all required Titanium Modules:", - "Found incompatible Titanium Modules:": "Found incompatible Titanium Modules:", - "Found conflicting Titanium modules:": "Found conflicting Titanium modules:", - "Titanium module \"%s\" requested for both Android and CommonJS platforms, but only one may be used at a time.": "Titanium module \"%s\" requested for both Android and CommonJS platforms, but only one may be used at a time.", - "Module %s version %s is missing module file: %s": "Module %s version %s is missing module file: %s", - "Module %s version %s does not have a main jar file": "Module %s version %s does not have a main jar file", - "The module %%s does not support the ABI: %%s": { - "one": "The module %%s does not support the ABI: %%s", - "other": "The module %%s does not support the ABIs: %s" - }, - "It only supports the following ABIs: %s": "It only supports the following ABIs: %s", - "Your application will most likely encounter issues": "Your application will most likely encounter issues", - "Module %s version %s is missing bindings json file": "Module %s version %s is missing bindings json file", - "The module \"%s\" has an invalid jar file: %s": "The module \"%s\" has an invalid jar file: %s", - "Conflicting jar files detected:": "Conflicting jar files detected:", - "The following modules have different \"%s\" files": "The following modules have different \"%s\" files", - " %s (version %s) (hash=%s)": " %s (version %s) (hash=%s)", - "You can either select a version of these modules where the conflicting jar file is the same or you can try copying the jar file from one module's \"lib\" folder to the other module's \"lib\" folder.": "You can either select a version of these modules where the conflicting jar file is the same or you can try copying the jar file from one module's \"lib\" folder to the other module's \"lib\" folder.", - "Finished building the application in %s": "Finished building the application in %s", - "Unable to find Android emulator \"%s\"": "Unable to find Android emulator \"%s\"", - "Titanium SDK Android directory: %s": "Titanium SDK Android directory: %s", - "Deploy type: %s": "Deploy type: %s", - "Building for target: %s": "Building for target: %s", - "Performing build only": "Performing build only", - "Building for emulator: %s": "Building for emulator: %s", - "Building for device: %s": "Building for device: %s", - "Targeting Android SDK: %s": "Targeting Android SDK: %s", - "Building for the following architectures: %s": "Building for the following architectures: %s", - "Signing with keystore: %s": "Signing with keystore: %s", - "App ID: %s": "App ID: %s", - "Classname: %s": "Classname: %s", - "Debugging enabled via debug port: %s": "Debugging enabled via debug port: %s", - "Debugging disabled": "Debugging disabled", - "Profiler enabled via profiler port: %s": "Profiler enabled via profiler port: %s", - "Profiler disabled": "Profiler disabled", - "Forcing rebuild: %s flag was set": "Forcing rebuild: %s flag was set", - "Forcing rebuild: %s does not exist": "Forcing rebuild: %s does not exist", - "Forcing rebuild: target changed since last build": "Forcing rebuild: target changed since last build", - "Was: %s": "Was: %s", - "Now: %s": "Now: %s", - "Forcing rebuild: deploy type changed since last build": "Forcing rebuild: deploy type changed since last build", - "Forcing rebuild: classname changed since last build": "Forcing rebuild: classname changed since last build", - "Forcing rebuild: JavaScript files need to be re-encrypted": "Forcing rebuild: JavaScript files need to be re-encrypted", - "Forcing rebuild: JavaScript encryption flag changed": "Forcing rebuild: JavaScript encryption flag changed", - "Forcing rebuild: Titanium SDK path changed since last build": "Forcing rebuild: Titanium SDK path changed since last build", - "Forcing rebuild: githash changed since last build": "Forcing rebuild: githash changed since last build", - "Forcing rebuild: modules hash changed since last build": "Forcing rebuild: modules hash changed since last build", - "Forcing rebuild: module manifest hash changed since last build": "Forcing rebuild: module manifest hash changed since last build", - "Forcing rebuild: native modules hash changed since last build": "Forcing rebuild: native modules hash changed since last build", - "Forcing rebuild: native modules bindings hash changed since last build": "Forcing rebuild: native modules bindings hash changed since last build", - "Forcing rebuild: tiapp.xml project name changed since last build": "Forcing rebuild: tiapp.xml project name changed since last build", - "Forcing rebuild: tiapp.xml app id changed since last build": "Forcing rebuild: tiapp.xml app id changed since last build", - "Forcing rebuild: tiapp.xml analytics flag changed since last build": "Forcing rebuild: tiapp.xml analytics flag changed since last build", - "Forcing rebuild: tiapp.xml publisher changed since last build": "Forcing rebuild: tiapp.xml publisher changed since last build", - "Forcing rebuild: tiapp.xml url changed since last build": "Forcing rebuild: tiapp.xml url changed since last build", - "Forcing rebuild: tiapp.xml version changed since last build": "Forcing rebuild: tiapp.xml version changed since last build", - "Forcing rebuild: tiapp.xml description changed since last build": "Forcing rebuild: tiapp.xml description changed since last build", - "Forcing rebuild: tiapp.xml copyright changed since last build": "Forcing rebuild: tiapp.xml copyright changed since last build", - "Forcing rebuild: tiapp.xml guid changed since last build": "Forcing rebuild: tiapp.xml guid changed since last build", - "Forcing rebuild: tiapp.xml icon changed since last build": "Forcing rebuild: tiapp.xml icon changed since last build", - "Forcing rebuild: tiapp.xml fullscreen changed since last build": "Forcing rebuild: tiapp.xml fullscreen changed since last build", - "Forcing rebuild: Android minimum SDK changed since last build": "Forcing rebuild: Android minimum SDK changed since last build", - "Forcing rebuild: Android target SDK changed since last build": "Forcing rebuild: Android target SDK changed since last build", - "Forcing rebuild: tiapp.xml properties changed since last build": "Forcing rebuild: tiapp.xml properties changed since last build", - "Forcing rebuild: Android activities in tiapp.xml changed since last build": "Forcing rebuild: Android activites in tiapp.xml changed since last build", - "Forcing rebuild: Android services in tiapp.xml SDK changed since last build": "Forcing rebuild: Android services in tiapp.xml SDK changed since last build", - "Forcing rebuild: One or more JSS files changed since last build": "Forcing rebuild: One or more JSS files changed since last build", - "Forcing rebuild: mergeCustomAndroidManifest config has changed since last build": "Forcing rebuild: mergeCustomAndroidManifest config has changed since last build", - "Overwriting file %s": "Overwriting file %s", - "Symlinking %s => %s": "Symlinking %s => %s", - "Copying %s => %s": "Copying %s => %s", - "Ignoring %s": "Ignoring %s", - "Found conflicting resources:": "Found conflicting resources:", - "You cannot have resources that resolve to the same resource entry name": "You cannot have resources that resolve to the same resource entry name", - "Copying and minifying %s => %s": "Copying and minifying %s => %s", - "You have both an %s folder and an %s folder": "You have both an %s folder and an %s folder", - "Files from both of these folders will end up in %s": "Files from both of these folders will end up in %s", - "If two files are named the same, there is no guarantee which one will be copied last and therefore be the one the application uses": "If two files are named the same, there is no guarantee which one will be copied last and therefore be the one the application uses", - "You should use just one of these folders to avoid conflicts": "You should use just one of these folders to avoid conflicts", - "There is a project resource \"%s\" that conflicts with a CommonJS module": "There is a project resource \"%s\" that conflicts with a CommonJS module", - "Please rename the file, then rebuild": "Please rename the file, then rebuild", - "Processing JavaScript files": "Processing JavaScript files", - "Encrypting JavaScript files: %s": "Encrypting JavaScript files: %s", - "Failed to encrypt JavaScript files": "Failed to encrypt JavaScript files", - "32-bit titanium prep failed, trying again using 64-bit": "32-bit titanium prep failed, trying again using 64-bit", - "Adding library %s": "Adding library %s", - "Unknown namespace %s, skipping": "Unknown namespace %s, skipping", - "Adding dependency library %s": "Adding dependency library %s", - "The \"apiversion\" for \"%s\" in the module manifest is less than version 2.": "The \"apiversion\" for \"%s\" in the module manifest is less than version 2.", - "The module was likely built against a Titanium SDK 1.8.0.1 or older.": "The module was likely built against a Titanium SDK 1.8.0.1 or older.", - "Please use a version of the module that has \"apiversion\" 2 or greater": "Please use a version of the module that has \"apiversion\" 2 or greater", - "Writing %s": "Writing %s", - "Forcing rebuild: Detected change in Titanium APIs used and need to recompile": "Forcing rebuild: Detected change in Titanium APIs used and need to recompile", - "Extracting module resources: %s": "Extracting module resources: %s", - "Failed to extract module resource zip: %s": "Failed to extract module resource zip: %s", - "Removing old file: %s": "Removing old file: %s", - "Removing old symlink: %s": "Removing old symlink: %s", - "Copying template %s => %s": "Copying template %s => %s", - "Generating activity class: %s": "Generating activity class: %s", - "Generating interval service class: %s": "Generating interval service class: %s", - "Generating service class: %s": "Generating service class: %s", - "Overwriting XML node %s in file %s": "Overwriting XML node %s in file %s", - "Merging %s => %s": "Merging %s => %s", - "Android SDK %s missing framework aidl, skipping": "Android SDK %s missing framework aidl, skipping", - "No aidl files to compile, continuing": "No aidl files to compile, continuing", - "Compiling aidl file: %s": "Compiling aidl file: %s", - "Generating i18n files": "Generating i18n files", - "Merging %s strings => %s": "Merging %s strings => %s", - "Writing %s strings => %s": "Writing %s strings => %s", - "Found invalid i18n string names:": "Found invalid i18n string names:", - "Android does not allow i18n string names with spaces.": "Android does not allow i18n string names with spaces.", - "To exclude invalid i18n strings from the build, run:": "To exclude invalid i18n strings from the build, run:", - "Generating %s": "Generating %s", - "Writing unmerged custom AndroidManifest.xml": "Writing unmerged custom AndroidManifest.xml", - "Detected %s call which requires Google APIs, however the selected emulator %s may or may not support Google APIs": "Detected %s call which requires Google APIs, however the selected emulator %s may or may not support Google APIs", - "If the emulator does not support Google APIs, the %s call will fail": "If the emulator does not support Google APIs, the %s call will fail", - "Detected %s call which requires Google APIs, but the selected emulator %s does not support Google APIs": "Detected %s call which requires Google APIs, but the selected emulator %s does not support Google APIs", - "Expect the %s call to fail": "Expect the %s call to fail", - "You should use, or create, an Android emulator that does support Google APIs": "You should use, or create, an Android emulator that does support Google APIs", - "Packaging application: %s": "Packaging application: %s", - "Failed to package application:": "Failed to package application:", - "Unable to find generated R.java file": "Unable to find generated R.java file", - "Skipping duplicate jar file: %s": "Skipping duplicate jar file: %s", - "Building Java source files: %s": "Building Java source files: %s", - "Failed to compile Java source files:": "Failed to compile Java source files:", - "Running ProGuard: %s": "Running ProGuard: %s", - "Failed to run ProGuard": "Failed to run ProGuard", - "Running dexer: %s": "Running dexer: %s", - "Failed to run dexer:": "Failed to run dexer:", - "Creating unsigned apk": "Creating unsigned apk", - "Processing %s": "Processing %s", - "Adding %s": "Adding %s", - "Deflating %s": "Deflating %s", - "Invalid native Titanium library ABI \"%s\"": "Invalid native Titanium library ABI \"%s\"", - "The module %s does not support the ABI: %s": "The module %s does not support the ABI: %s", - "Writing unsigned apk: %s": "Writing unsigned apk: %s", - "Using %s signature algorithm": "Using %s signature algorithm", - "Signing apk: %s": "Signing apk: %s", - "Failed to sign apk:": "Failed to sign apk:", - "Aligning zip file: %s": "Aligning zip file: %s", - "Failed to zipalign apk:": "Failed to zipalign apk:", - "Writing build manifest: %s": "Writing build manifest: %s", - "No APK file to deploy, skipping": "No APK file to deploy, skipping", - "Packaging complete": "Packaging complete", - "Package location: %s": "Package location: %s", - "Launching emulator: %s": "Launching emulator: %s", - "Starting emulator: %s": "Starting emulator: %s", - "Unable to start emulator \"%s\"": "Unable to start emulator \"%s\"", - "Emulator process started": "Emulator process started", - "Emulator ready!": "Emulator ready!", - "Emulator timeout after waiting %s ms": "Emulator timeout after waiting %s ms", - "An emulator error occurred": "An emulator error occurred", - "Emulator exited with error: %s": "Emulator exited with error: %s", - "Unable to find any connected devices": "Unable to find any connected devices", - "Unable to find device \"%s\"": "Unable to find device \"%s\"", - "Did you unplug it?": "Did you unplug it?", - "Performed build only, skipping installing of the application": "Performed build only, skipping installing of the application", - "No APK file to install and run, skipping": "No APK file to install and run, skipping", - "Making sure the adb server is running": "Making sure the adb server is running", - "Waiting for emulator to become ready...": "Waiting for emulator to become ready...", - "Emulator failed to start in a timely manner": "Emulator failed to start in a timely manner", - "The current timeout is set to %s ms": "The current timeout is set to %s ms", - "You can increase this timeout by running: %s": "You can increase this timeout by running: %s", - "Installing apk: %s": "Installing apk: %s", - "Installing app on device: %s": "Installing app on device: %s", - "Application failed to install": "Application failed to install", - "Checking if package manager service is started": "Checking if package manager service is started", - "Package manager not started yet, trying again in %sms...": "Package manager not started yet, trying again in %sms...", - "Package manager has started": "Package manager has started", - "ADB install failed because package manager service is still starting, trying again in %sms...": "ADB install failed because package manager service is still starting, trying again in %sms...", - "Failed to install apk on \"%s\"": "Failed to install apk on \"%s\"", - "Make sure your keystore is signed with a compatible signature algorithm such as \"SHA1withRSA\" or \"MD5withRSA\".": "Make sure your keystore is signed with a compatible signature algorithm such as \"SHA1withRSA\" or \"MD5withRSA\".", - "App successfully installed": "App successfully installed", - "Start application log": "Start application log", - "End application log": "End application log", - "Starting app: %s": "Starting app: %s", - "Application failed to launch": "Application failed to launch", - "Trying to start the app...": "Trying to start the app...", - "Application pid: %s": "Application pid: %s", - "App still not started, trying again": "App still not started, trying again", - "Forwarding host port %s to device for debugging": "Forwarding host port %s to device for debugging", - "Forwarding host port %s to device for profiling": "Forwarding host port %s to device for profiling", - "AndroidManifest.xml file does not exist": "AndroidManifest.xml file does not exist", - "Failed to merge, source must be an AndroidManifest object": "Failed to merge, source must be an AndroidManifest object", - "Android %s Device": "Android %s Device", - "Android Device": "Android Device", - "Android SDK": "Android SDK", - "Android Executable": "Android Executable", - "not found": "not found", - "ADB Executable": "ADB Executable", - "SDK Path": "SDK Path", - "Android NDK": "Android NDK", - "NDK Path": "NDK Path", - "NDK Version": "NDK Version", - "not supported by Titanium SDK %s, but may work": "not supported by Titanium SDK %s, but may work", - "Not supported by Titanium SDK %s": "Not supported by Titanium SDK %s", - "Name": "Name", - "API Level": "API Level", - "Revision": "Revision", - "Skins": "Skins", - "ABIs": "ABIs", - "Path": "Path", - "Android %s (API level %s)": "Android %s (API level %s)", - "unknown": "unknown", - "Vendor": "Vendor", - "Description": "Description", - "Libraries": "Libraries", - "none": "none", - "Android Platforms": "Android Platforms", - "None": "None", - "Android Add-Ons": "Android Add-Ons", - "SDK Version": "SDK Version", - "ABI": "ABI", - "Skin": "Skin", - "SD Card": "SD Card", - "no sd card": "no sd card", - "Based On": "Based On", - "Google APIs": "Google APIs", - "yes": "yes", - "no": "no", - "Genymotion Version": "Genymotion Version", - "Display": "Display", - "DPI": "DPI", - "OpenGL Acceleration": "OpenGL Acceleration", - "unknown, emulator not running": "unknown, emulator not running", - "Connected Android Devices": "Connected Android Devices", - "ID": "ID", - "State": "State" -} diff --git a/android/cli/locales/es.js b/android/cli/locales/es.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/es.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/fr.js b/android/cli/locales/fr.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/fr.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/hi.js b/android/cli/locales/hi.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/hi.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/it.js b/android/cli/locales/it.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/it.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/ja.js b/android/cli/locales/ja.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/ja.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/ko.js b/android/cli/locales/ko.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/ko.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/ru.js b/android/cli/locales/ru.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/ru.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/uk.js b/android/cli/locales/uk.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/uk.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/locales/zh.js b/android/cli/locales/zh.js deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/android/cli/locales/zh.js +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/cli/tests/test-android-manifest.js b/android/cli/tests/test-android-manifest.js index 4318cf248e3..5b466af7fb2 100644 --- a/android/cli/tests/test-android-manifest.js +++ b/android/cli/tests/test-android-manifest.js @@ -5,10 +5,8 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const AndroidManifest = require('../lib/android-manifest'); -const expect = require('chai').expect; +import { AndroidManifest } from '../lib/android-manifest.js'; +import { expect } from 'chai'; describe('AndroidManifest', () => { it('isEmpty()', () => { diff --git a/android/package.json b/android/package.json index ad6f5134f67..a6042dcafa8 100644 --- a/android/package.json +++ b/android/package.json @@ -2,6 +2,7 @@ "name": "titanium-mobile-android", "title": "Android", "description": "Titanium SDK Android", + "type": "module", "keywords": [ "tidev", "titanium", diff --git a/android/titanium/build.gradle b/android/titanium/build.gradle index d92e05cf820..d5b40fad177 100644 --- a/android/titanium/build.gradle +++ b/android/titanium/build.gradle @@ -147,7 +147,7 @@ task updateV8Library() { exec { executable = 'node' workingDir = projectDir - args = ['-e', "require('./libv8-services').updateLibraryThenExit()"] + args = ['libv8-services.js', 'update-library'] } } } @@ -199,13 +199,13 @@ def getChangedFiles() { task snapshotTiCommonFiles() { inputs.dir "${projectDir}/../../common/Resources" inputs.file "${projectDir}/../../build/lib/builder.js" - inputs.file "${projectDir}/../../build/lib/android/index.js" + inputs.file "${projectDir}/../../build/lib/android.js" outputs.file "${projectDir}/../runtime/v8/generated/V8Snapshots.h" doLast { exec { executable = 'node' workingDir = projectDir - args = ['-e', "require('./libv8-services').createSnapshotThenExit()"] + args = ['libv8-services.js', 'create-snapshot'] } } } diff --git a/android/titanium/genBootstrap.js b/android/titanium/genBootstrap.js index f7620e3800e..162d5815eed 100644 --- a/android/titanium/genBootstrap.js +++ b/android/titanium/genBootstrap.js @@ -1,9 +1,9 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs-extra'); -const ejs = require('ejs'); +import path from 'node:path'; +import fs from 'fs-extra'; +import ejs from 'ejs'; +import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const KROLL_DEFAULT = 'org.appcelerator.kroll.annotations.Kroll.DEFAULT'; const TI_MODULE = 'ti.modules.titanium.TitaniumModule'; @@ -128,17 +128,14 @@ async function generateJS(bindings, outDir) { * @param {string} [outDir='../runtime/v8/generated'] path to place the output files * @returns {Promise} */ -async function genBootstrap(outDir = path.join(__dirname, '../runtime/v8/generated')) { +export async function generateBootstrap(outDir = path.join(__dirname, '../runtime/v8/generated')) { const bindings = await loadBindings(); return generateJS(bindings, outDir); } -if (require.main === module) { - genBootstrap().then(() => process.exit(0)) - .catch(e => { - console.error(e); - process.exit(1); - }); +if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) { + generateBootstrap().catch(err => { + console.error(err); + process.exit(1); + }); } - -module.exports = genBootstrap; diff --git a/android/titanium/libv8-services.js b/android/titanium/libv8-services.js index cd737d24ad3..c656e4f4cbd 100644 --- a/android/titanium/libv8-services.js +++ b/android/titanium/libv8-services.js @@ -4,21 +4,21 @@ * Licensed under the terms of the Apache Public License. * Please see the LICENSE included with this distribution for details. */ -/* eslint no-unused-expressions: "off" */ -/* eslint security/detect-child-process: "off" */ -'use strict'; - -const AndroidBuilder = require('../../build/lib/android'); -const Builder = require('../../build/lib/builder'); -const BuildUtils = require('../../build/lib/utils'); -const util = require('util'); -const ejs = require('ejs'); -const child_process = require('child_process'); + +import { AndroidBuilder } from '../../build/lib/android.js'; +import { Builder } from '../../build/lib/builder.js'; +import * as BuildUtils from '../../build/lib/utils.js'; +import util from 'node:util'; +import ejs from 'ejs'; +import child_process from 'node:child_process'; +import fs from 'fs-extra'; +import path from 'node:path'; +import request from 'request-promise-native'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const exec = util.promisify(child_process.exec); const execFile = util.promisify(child_process.execFile); -const fs = require('fs-extra'); -const path = require('path'); -const request = require('request-promise-native'); // Determine if we're running on a Windows machine. const isWindows = process.platform === 'win32'; @@ -150,7 +150,7 @@ async function createSnapshot() { const mainBuilder = new Builder(options, [ 'android' ]); await mainBuilder.ensureGitHash(); const androidBuilder = new AndroidBuilder({ - sdkVersion: require('../../package.json').version, + sdkVersion: fs.readJsonSync(path.join(__dirname, '../../package.json')).version, gitHash: options.gitHash, timestamp: options.timestamp }); @@ -265,42 +265,28 @@ async function updateLibrary() { return fs.copy(tmpExtractDir, installedLibV8DirPath); } -/** - * Does a transpile/polyfill/rollup of our "titanium_mobile/common/Resources" JS files. - * Will then generate a C++ header file providing a V8 snapshot of the rolled-up JS for fast startup times. - * - * Will exit the process when the async operation ends. Intended to be called from the command line. - */ -function createSnapshotThenExit() { - exitWhenDone(createSnapshot()); -} - -/** - * Checks if the V8 library referenced by the "titanium_mobile/android/package.json" file is installed. - * If not, then this function will automatically download/install it. Function will do nothing if already installed. - * - * Will exit the process when the async operation ends. Intended to be called from the command line. - */ -function updateLibraryThenExit() { - exitWhenDone(updateLibrary()); -} - -/** - * Exits the process when the given promise's operation ends. - * @param {Promise} promise The promise to be monitored. Cannot be null/undefined. - */ -function exitWhenDone(promise) { - promise - .then(() => process.exit(0)) - .catch((err) => { +if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) { + if (process.argv[2] === 'create-snapshot') { + /** + * Does a transpile/polyfill/rollup of our "titanium_mobile/common/Resources" JS files. + * Will then generate a C++ header file providing a V8 snapshot of the rolled-up JS for fast startup times. + * + * Will exit the process when the async operation ends. Intended to be called from the command line. + */ + createSnapshot().catch(err => { console.error(err); process.exit(1); }); + } else if (process.argv[2] === 'updateLibrary') { + /** + * Checks if the V8 library referenced by the "titanium_mobile/android/package.json" file is installed. + * If not, then this function will automatically download/install it. Function will do nothing if alredy installed. + * + * Will exit the process when the async operation ends. Intended to be called from the command line. + */ + updateLibrary().catch(err => { + console.error(err); + process.exit(1); + }); + } } - -module.exports = { - createSnapshot, - createSnapshotThenExit, - updateLibrary, - updateLibraryThenExit -}; diff --git a/android/titanium/prebuild.js b/android/titanium/prebuild.js index 07a62e2f2b3..cb193d43096 100644 --- a/android/titanium/prebuild.js +++ b/android/titanium/prebuild.js @@ -5,15 +5,16 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const util = require('util'); -const exec = util.promisify(require('child_process').exec); // eslint-disable-line security/detect-child-process -const fs = require('fs-extra'); -const path = require('path'); -const ejs = require('ejs'); -const generateBootstrap = require('./genBootstrap'); - +import util from 'node:util'; +import child_process from 'node:child_process'; +import fs from 'fs-extra'; +import path from 'node:path'; +import ejs from 'ejs'; +import { generateBootstrap } from './genBootstrap.js'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const exec = util.promisify(child_process.exec); const runtimeV8DirPath = path.join(__dirname, '..', 'runtime', 'v8'); // Determine if we're running on a Windows machine. @@ -60,13 +61,13 @@ async function gperf(workingDirPath, inputFilePath, outputFilePath) { * @param {string} outputDir directory to place the generated 'ti.kernel.js' file */ async function generateTiKernel(outputDir) { - const Builder = require('../../build/lib/builder'); - const Android = require('../../build/lib/android'); + const { Builder } = await import('../../build/lib/builder.js'); + const { AndroidBuilder } = await import('../../build/lib/android.js'); const options = { }; const builder = new Builder(options, [ 'android' ]); await builder.ensureGitHash(); - const android = new Android({ - sdkVersion: require('../../package.json').version, + const android = new AndroidBuilder({ + sdkVersion: fs.readJsonSync(path.join(__dirname, '../../package.json')).version, gitHash: options.gitHash, timestamp: options.timestamp }); @@ -181,18 +182,12 @@ async function generateSourceCode() { return replaceFileIfDifferent(tempFilePath, headerFilePath); } -/** Executes the pre-build step. */ -async function main() { +if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) { console.log('Running Titanium "prebuild.js" script.'); - // Generate C/C++ source files with JS files embedded in them and from gperf templates. - return generateSourceCode(); -} -if (require.main === module) { - main() - .then(() => process.exit(0)) - .catch((err) => { - console.error(err); - process.exit(1); - }); + // Generate C/C++ source files with JS files embedded in them and from gperf templates. + generateSourceCode().catch(err => { + console.error(err); + process.exit(1); + }); } diff --git a/build/lib/android/index.js b/build/lib/android.js similarity index 91% rename from build/lib/android/index.js rename to build/lib/android.js index 5e6a731b2ba..29f02faeb0b 100644 --- a/build/lib/android/index.js +++ b/build/lib/android.js @@ -1,27 +1,23 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs-extra'); -const utils = require('../utils'); -const { spawn } = require('child_process'); // eslint-disable-line security/detect-child-process -const copyFile = utils.copyFile; -const copyFiles = utils.copyFiles; -const copyAndModifyFile = utils.copyAndModifyFile; -const globCopy = utils.globCopy; +import path from 'node:path'; +import fs from 'fs-extra'; +import { copyFile, copyFiles, copyAndModifyFile, globCopy } from './utils.js'; +import { spawn } from 'child_process'; +import { fileURLToPath } from 'node:url'; // Determine if we're running on a Windows machine. const isWindows = (process.platform === 'win32'); -const ROOT_DIR = path.join(__dirname, '..', '..', '..'); -const TITANIUM_ANDROID_PATH = path.join(__dirname, '..', '..', '..', 'android'); -const DIST_ANDROID_PATH = path.join(__dirname, '..', '..', '..', 'dist', 'android'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT_DIR = path.join(__dirname, '..', '..'); +const TITANIUM_ANDROID_PATH = path.join(ROOT_DIR, 'android'); +const DIST_ANDROID_PATH = path.join(ROOT_DIR, 'dist', 'android'); const GRADLEW_FILE_PATH = path.join(TITANIUM_ANDROID_PATH, isWindows ? 'gradlew.bat' : 'gradlew'); // On CI server, use plain output to avoid nasty progress bars filling up logs // But on local dev, use the nice UI const GRADLE_CONSOLE_MODE = (process.env.TRAVIS || process.env.JENKINS || process.env.CI) ? 'plain' : 'rich'; const V8_STRING_VERSION_REGEXP = /(\d+)\.(\d+)\.\d+\.\d+/; -class Android { +export class AndroidBuilder { /** * @param {Object} options options object * @param {String} options.androidSdk path to the Android SDK to build with @@ -40,7 +36,7 @@ class Android { } babelOptions() { - const v8Version = require(path.join(ROOT_DIR, 'android', 'package.json')).v8.version; // eslint-disable-line security/detect-non-literal-require + const v8Version = fs.readJsonSync(path.join(ROOT_DIR, 'android', 'package.json')).v8.version; const v8VersionGroup = v8Version.match(V8_STRING_VERSION_REGEXP); const version = parseInt(v8VersionGroup[1] + v8VersionGroup[2]); @@ -130,7 +126,7 @@ class Android { await fs.mkdirs(ZIP_HEADER_INCLUDE_PATH); await globCopy('**/*.h', path.join(TITANIUM_ANDROID_PATH, 'runtime', 'v8', 'src', 'native'), ZIP_HEADER_INCLUDE_PATH); await globCopy('**/*.h', path.join(TITANIUM_ANDROID_PATH, 'runtime', 'v8', 'generated'), ZIP_HEADER_INCLUDE_PATH); - const v8Props = require(path.join(TITANIUM_ANDROID_PATH, 'package.json')).v8; // eslint-disable-line security/detect-non-literal-require + const v8Props = fs.readJsonSync(path.join(TITANIUM_ANDROID_PATH, 'package.json')).v8; // eslint-disable-line security/detect-non-literal-require const LIBV8_INCLUDE_PATH = path.join(DIST_ANDROID_PATH, 'libv8', v8Props.version, v8Props.mode, 'include'); await globCopy('**/*.h', LIBV8_INCLUDE_PATH, ZIP_HEADER_INCLUDE_PATH); @@ -260,4 +256,4 @@ async function createLocalPropertiesFile(sdkPath) { await fs.writeFile(filePath, fileLines.join('\n') + '\n'); } -module.exports = Android; +export default AndroidBuilder; diff --git a/build/lib/builder.js b/build/lib/builder.js index 38b652761fe..f2b22688ffa 100644 --- a/build/lib/builder.js +++ b/build/lib/builder.js @@ -1,17 +1,17 @@ -'use strict'; - -const os = require('os'); -const path = require('path'); -const fs = require('fs-extra'); -const rollup = require('rollup').rollup; -const { babel } = require('@rollup/plugin-babel'); -const { nodeResolve } = require('@rollup/plugin-node-resolve'); -const commonjs = require('@rollup/plugin-commonjs'); - -const git = require('./git'); -const utils = require('./utils'); -const Packager = require('./packager'); - +import os from 'node:os'; +import path from 'node:path'; +import fs from 'fs-extra'; +import { rollup } from 'rollup'; +import { babel } from '@rollup/plugin-babel'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import { getHash } from './git.js'; +import { installSDK, installSDKFromZipFile, timestamp } from './utils.js'; +import { Packager } from './packager.js'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.join(__dirname, '..', '..'); const DIST_DIR = path.join(ROOT_DIR, 'dist'); const TMP_DIR = path.join(DIST_DIR, 'tmp'); @@ -43,6 +43,8 @@ function determineBabelOptions(babelOptions) { const transform = options.transform || {}; delete options.transform; + const require = createRequire(import.meta.url); + return { presets: [ [ '@babel/env', options ] ], plugins: [ [ require.resolve('babel-plugin-transform-titanium'), transform ] ], @@ -58,7 +60,7 @@ function determineBabelOptions(babelOptions) { * @returns {String} */ function getCorejsVersion() { - const packageLock = require('../../package-lock'); + const packageLock = fs.readJsonSync(path.join(__dirname, '../../package-lock.json')); if (packageLock.dependencies && packageLock.dependencies['core-js']) { const { version } = packageLock.dependencies['core-js']; return version; @@ -66,7 +68,7 @@ function getCorejsVersion() { throw new Error('Could not lookup core-js version in package-lock file.'); } -class Builder { +export class Builder { /** * @param {object} options command line options @@ -97,7 +99,7 @@ class Builder { } this.options = options; - this.options.timestamp = utils.timestamp(); + this.options.timestamp = timestamp(); this.options.onlyFailedTests = options.onlyFailed || false; this.options.versionTag = options.versionTag || options.sdkVersion; } @@ -105,7 +107,7 @@ class Builder { async clean() { // TODO: Clean platforms in parallel for (const p of this.platforms) { - const Platform = require(`./${p}`); // eslint-disable-line security/detect-non-literal-require + const { default: Platform } = await import(`./${p}.js`); const platform = new Platform(this.options); await platform.clean(); } @@ -114,7 +116,7 @@ class Builder { } async test() { - const { runTests, outputMultipleResults } = require('./test'); + const { runTests, outputMultipleResults } = await import('./test/index.js'); const results = await runTests(this.platforms, this.options); return outputMultipleResults(results); } @@ -123,7 +125,7 @@ class Builder { if (this.options.gitHash) { return; } - const hash = await git.getHash(ROOT_DIR); + const hash = await getHash(ROOT_DIR); this.options.gitHash = hash || 'n/a'; } @@ -202,7 +204,7 @@ class Builder { // TODO: build platforms in parallel for (const item of this.platforms) { - const Platform = require(`./${item}`); // eslint-disable-line security/detect-non-literal-require + const { default: Platform } = await import(`./${item}.js`); const platform = new Platform(this.options); await this.transpile(item, platform.babelOptions()); await platform.build(); @@ -240,7 +242,7 @@ class Builder { return; } - const Documentation = require('./docs'); + const { Documentation } = await import('./docs.js'); const docs = new Documentation(DIST_DIR); return docs.generate(); } @@ -249,11 +251,9 @@ class Builder { if (zipfile) { // Assume we have explicitly said to install this zipfile (from CLI command) zipfile = path.resolve(process.cwd(), zipfile); - return utils.installSDKFromZipFile(zipfile, this.options.select); + return installSDKFromZipFile(zipfile, this.options.select); } // Otherwise use fuzzier logic that tries to install local dev built version - return utils.installSDK(this.options.versionTag, this.options.symlink); + return installSDK(this.options.versionTag, this.options.symlink); } } - -module.exports = Builder; diff --git a/build/lib/docs.js b/build/lib/docs.js index 6b10a836e11..091b489d3b2 100644 --- a/build/lib/docs.js +++ b/build/lib/docs.js @@ -1,11 +1,12 @@ -'use strict'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const spawn = require('child_process').spawn; // eslint-disable-line security/detect-child-process -const path = require('path'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.join(__dirname, '../..'); const DOC_DIR = path.join(ROOT_DIR, 'apidoc'); -class Documentation { +export class Documentation { /** * @param {string} outputDir output directory for generated documentation * @constructor @@ -55,4 +56,3 @@ class Documentation { ]); } } -module.exports = Documentation; diff --git a/build/lib/git.js b/build/lib/git.js index 69b665e3948..9ecdf8061b5 100644 --- a/build/lib/git.js +++ b/build/lib/git.js @@ -1,14 +1,14 @@ -'use strict'; +import { promisify } from 'util'; +import child_process from 'child_process'; -const promisify = require('util').promisify; -const exec = promisify(require('child_process').exec); // eslint-disable-line security/detect-child-process +const exec = promisify(child_process.exec); /** * Get the short (10-character) SHA hash of HEAD in the supplied working directory. * @param {string} cwd current working directory path * @returns {Promise} sha of current git commit for HEAD */ -async function getHash(cwd) { +export async function getHash(cwd) { const { stdout } = await exec('git rev-parse --short=10 --no-color HEAD', { cwd }); return stdout.trim(); // drop leading 'commit ', just take 10-character sha } @@ -19,8 +19,6 @@ async function getHash(cwd) { * @param {string} file the file to discard * @returns {Promise} sha of current git commit for HEAD */ -function discardLocalChange(cwd, file) { +export function discardLocalChange(cwd, file) { return exec(`git checkout -- ${file}`, { cwd }); } - -module.exports = { getHash, discardLocalChange }; diff --git a/build/lib/ios.js b/build/lib/ios.js index fa5f5b71cca..9796213bff8 100644 --- a/build/lib/ios.js +++ b/build/lib/ios.js @@ -1,19 +1,17 @@ -'use strict'; +import path from 'node:path'; +import fs from 'fs-extra'; +import { promisify } from 'node:util'; +import glob from 'glob'; +import { spawn } from 'node:child_process'; // eslint-disable-line security/detect-child-process +import { copyFiles, copyAndModifyFile } from './utils.js'; -const path = require('path'); -const fs = require('fs-extra'); -const utils = require('./utils'); -const promisify = require('util').promisify; -const glob = promisify(require('glob')); -const spawn = require('child_process').spawn; // eslint-disable-line security/detect-child-process -const copyFiles = utils.copyFiles; -const copyAndModifyFile = utils.copyAndModifyFile; +const globPromise = promisify(glob); const ROOT_DIR = path.join(__dirname, '../..'); const IOS_ROOT = path.join(ROOT_DIR, 'iphone'); const IOS_LIB = path.join(IOS_ROOT, 'lib'); -class IOS { +export class IOS { /** * @param {Object} options options object * @param {String} options.sdkVersion version of Titanium SDK @@ -29,7 +27,7 @@ class IOS { babelOptions() { // eslint-disable-next-line security/detect-non-literal-require - const { minIosVersion } = require(path.join(ROOT_DIR, 'iphone/package.json')); + const { minIosVersion } = fs.readJsonSync(path.join(ROOT_DIR, 'iphone/package.json')); return { targets: { @@ -114,7 +112,7 @@ class IOS { // Create redirecting headers in iphone/include/ pointing to iphone/Classes/ headers // TODO: Use map and Promise.all to run these in parallel - const classesHeaders = await glob('**/*.h', { cwd: path.join(IOS_ROOT, 'Classes') }); + const classesHeaders = await globPromise('**/*.h', { cwd: path.join(IOS_ROOT, 'Classes') }); for (const classHeader of classesHeaders) { let depth = 1; if (classHeader.includes(path.sep)) { // there's a sub-directory @@ -173,4 +171,4 @@ class IOS { } } -module.exports = IOS; +export default IOS; diff --git a/build/lib/packager.js b/build/lib/packager.js index edde2010c2a..9a1d13b0583 100644 --- a/build/lib/packager.js +++ b/build/lib/packager.js @@ -1,16 +1,20 @@ -'use strict'; - -const promisify = require('util').promisify; -const path = require('path'); -const os = require('os'); -const exec = promisify(require('child_process').exec); // eslint-disable-line security/detect-child-process -const fs = require('fs-extra'); -const packageJSON = require('../../package.json'); -const utils = require('./utils'); -const copyFile = utils.copyFile; -const copyFiles = utils.copyFiles; - +import path from 'node:path'; +import os from 'node:os'; +import { exec } from 'child_process'; +import fs from 'fs-extra'; +import { + cachedDownloadPath, + cacheUnzip, + copyFile, + copyFiles, + downloadURL, + unzip +} from './utils.js'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.join(__dirname, '../..'); +const packageJSON = fs.readJsonSync(path.join(ROOT_DIR, 'package.json')); const TMP_DIR = path.join(ROOT_DIR, 'dist', 'tmp'); const SUPPORT_DIR = path.join(ROOT_DIR, 'support'); @@ -45,7 +49,7 @@ async function zip(cwd, filename) { return copyFile(outputFolder, destFolder, path.basename(filename)); } -class Packager { +export class Packager { /** * @param {String} outputDir path to place the temp files and zipfile * @param {String} targetOS 'win32', 'linux', or 'osx' @@ -134,7 +138,7 @@ class Packager { */ async packageNodeModules() { console.log('Copying production npm dependencies'); - const moduleCopier = require('../../cli/lib/module-copier'); + const moduleCopier = await import('../../cli/lib/module-copier.js'); // Copy node_modules/ await moduleCopier.execute(this.srcDir, this.zipSDKDir); @@ -163,7 +167,7 @@ class Packager { } // Include 'ti.cloak' - return utils.unzip(path.join(ROOT_DIR, 'support', 'ti.cloak.zip'), path.join(this.zipSDKDir, 'node_modules')); + return unzip(path.join(ROOT_DIR, 'support', 'ti.cloak.zip'), path.join(this.zipSDKDir, 'node_modules')); } /** @@ -233,8 +237,7 @@ class Packager { let modules = []; // module objects holding url/integrity // Read modules.json, grab the object for each supportedPlatform - // eslint-disable-next-line security/detect-non-literal-require - const modulesJSON = require(path.join(SUPPORT_DIR, 'module', 'packaged', 'modules.json')); + const modulesJSON = fs.readJsonSync(path.join(SUPPORT_DIR, 'module', 'packaged', 'modules.json')); supportedPlatforms.forEach(platform => { const modulesForPlatform = modulesJSON[platform]; if (modulesForPlatform) { @@ -273,11 +276,11 @@ class Packager { async handleModule(m) { // download (with caching based on integrity hash) - const zipFile = await utils.downloadURL(m.url, m.integrity, { progress: false }); + const zipFile = await downloadURL(m.url, m.integrity, { progress: false }); // then unzip to temp dir (again with caching based on inut integrity hash) - const tmpZipPath = utils.cachedDownloadPath(m.url); + const tmpZipPath = cachedDownloadPath(m.url); const tmpOutDir = tmpZipPath.substring(0, tmpZipPath.length - '.zip'.length); // drop .zip - await utils.cacheUnzip(zipFile, m.integrity, tmpOutDir); + await cacheUnzip(zipFile, m.integrity, tmpOutDir); // then copy from tmp dir over to this.zipDir // Might have to tweak this a bit! probably want to copy some subdir console.log(`Copying ${tmpOutDir} to ${this.zipDir}`); @@ -315,7 +318,7 @@ class Packager { async zipPlatforms() { // TODO: do in parallel? for (const p of this.platforms) { - const Platform = require(`./${p}`); // eslint-disable-line security/detect-non-literal-require + const { default: Platform } = await import(`./${p}.js`); await new Platform({ sdkVersion: this.version, versionTag: this.versionTag, @@ -341,5 +344,3 @@ class Packager { return fs.remove(this.zipDir); } } - -module.exports = Packager; diff --git a/build/lib/test/generator.js b/build/lib/test/generator.js index 0dc878943b6..f444ab8b8a3 100644 --- a/build/lib/test/generator.js +++ b/build/lib/test/generator.js @@ -1,11 +1,13 @@ /** * Generate unit test suite for a type from apidoc yml */ -const path = require('path'); -const fs = require('fs-extra'); -const yaml = require('js-yaml'); -const ejs = require('ejs'); +import path from 'node:path'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import ejs from 'ejs'; +import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.join(__dirname, '../../..'); const APIDOC_DIR = path.join(ROOT_DIR, 'apidoc'); const TEST_TEMPLATE = path.join(__dirname, 'test.js.ejs'); @@ -96,7 +98,7 @@ async function expandWildcard(constants) { * Generate a unit test given an input apidoc yml file * @param {string[]} args program args */ -async function main(args) { +export async function main(args) { const fileName = args.shift(); const filePath = path.resolve(process.cwd(), fileName); @@ -114,5 +116,3 @@ async function main(args) { } console.log(`Created tests at: ${created}`); } - -module.exports = main; diff --git a/build/lib/test/index.js b/build/lib/test/index.js index 1e8be98a9d6..34351d4288c 100644 --- a/build/lib/test/index.js +++ b/build/lib/test/index.js @@ -1,11 +1,11 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs-extra'); +import path from 'node:path'; +import fs from 'fs-extra'; +import { fileURLToPath } from 'node:url'; +import { test, outputResults } from './test.js'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.join(__dirname, '../../..'); const LOCAL_TESTS = path.join(ROOT_DIR, 'tests'); -const { test, outputResults } = require('./test'); /** * Runs our unit testing script against the supplied platforms for the currently select SDK in `ti` cli. @@ -19,7 +19,7 @@ const { test, outputResults } = require('./test'); * @param {string} [program.onlyFailedTests] boolean * @returns {Promise} returns an object whose keys are platform names */ -async function runTests(platforms, program) { +export async function runTests(platforms, program) { const snapshotDir = path.join(LOCAL_TESTS, 'Resources'); // wipe generated images and diffs from previous run await Promise.all([ @@ -34,7 +34,7 @@ async function runTests(platforms, program) { * @param {object} results Dictionary of test results to be outputted. * @returns {Promise} */ -async function outputMultipleResults(results) { +export async function outputMultipleResults(results) { const platforms = Object.keys(results); for (const p of platforms) { console.log(); @@ -44,5 +44,3 @@ async function outputMultipleResults(results) { await outputResults(results[p].results); } } - -module.exports = { runTests, outputMultipleResults }; diff --git a/build/lib/test/test-test.js b/build/lib/test/test-test.js index 198909d6a7b..bf500b24304 100644 --- a/build/lib/test/test-test.js +++ b/build/lib/test/test-test.js @@ -5,12 +5,13 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; +import { handleBuild } from './test.js'; +import { expect } from 'chai'; +import fs from 'fs-extra'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const { handleBuild } = require('./test'); -const expect = require('chai').expect; -const fs = require('fs-extra'); -const path = require('path'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); describe('test.handleBuild', function () { this.slow(750); diff --git a/build/lib/test/test.js b/build/lib/test/test.js index beb51094541..379fa720655 100644 --- a/build/lib/test/test.js +++ b/build/lib/test/test.js @@ -3,20 +3,26 @@ * Licensed under the terms of the Apache Public License. * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const path = require('path'); -const fs = require('fs-extra'); -const colors = require('colors'); // eslint-disable-line no-unused-vars -const ejs = require('ejs'); -const StreamSplitter = require('stream-splitter'); -const spawn = require('child_process').spawn; // eslint-disable-line security/detect-child-process + +import path from 'node:path'; +import fs from 'fs-extra'; +import 'colors'; +import ejs from 'ejs'; +import StreamSplitter from 'stream-splitter'; +import child_process, { spawn } from 'node:child_process'; +import { promisify } from 'node:util'; +import stripAnsi from 'strip-ansi'; +import glob from 'glob'; +import { unzip } from '../utils.js'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); const titanium = require.resolve('titanium'); -const { promisify } = require('util'); -const stripAnsi = require('strip-ansi'); -const exec = promisify(require('child_process').exec); // eslint-disable-line security/detect-child-process -const glob = promisify(require('glob')); -const utils = require('../utils'); + +const exec = promisify(child_process.exec); +const globPromise = promisify(glob); const ROOT_DIR = path.join(__dirname, '../../..'); const SOURCE_DIR = path.join(ROOT_DIR, 'tests'); @@ -59,7 +65,7 @@ let showFailedOnly = false; * @param {string} [failedOnly] Show only failed tests * @returns {Promise} */ -async function test(platforms, target, deviceId, deployType, deviceFamily, junitPrefix, snapshotDir = path.join(__dirname, '../../../tests/Resources'), failedOnly) { +export async function test(platforms, target, deviceId, deployType, deviceFamily, junitPrefix, snapshotDir = path.join(__dirname, '../../../tests/Resources'), failedOnly) { showFailedOnly = failedOnly; const snapshotPromises = []; // place to stick commands we've fired off to pull snapshot images console.log(platforms); @@ -157,9 +163,9 @@ async function copyMochaAssets() { (async () => { await fs.copy(path.join(SOURCE_DIR, 'modules'), path.join(PROJECT_DIR, 'modules')); const modulesSourceDir = path.join(SOURCE_DIR, 'modules-source'); - const zipPaths = await glob('*/*/dist/*.zip', { cwd: modulesSourceDir }); + const zipPaths = await globPromise('*/*/dist/*.zip', { cwd: modulesSourceDir }); for (const nextZipPath of zipPaths) { - await utils.unzip(path.join(modulesSourceDir, nextZipPath), PROJECT_DIR); + await unzip(path.join(modulesSourceDir, nextZipPath), PROJECT_DIR); } })(), // platform @@ -770,7 +776,7 @@ class DeviceTestDetails { * @param {Promise[]} snapshotPromises array to hold promises for grabbign generated images * @returns {Promise} */ -async function handleBuild(prc, target, snapshotDir, snapshotPromises) { +export async function handleBuild(prc, target, snapshotDir, snapshotPromises) { return new Promise((resolve, reject) => { const deviceMap = new Map(); let started = false; @@ -963,7 +969,7 @@ async function outputJUnitXML(jsonResults, prefix) { /** * @param {object[]} results test results */ -async function outputResults(results) { +export async function outputResults(results) { const suites = {}; // start @@ -1021,8 +1027,3 @@ async function outputResults(results) { const total = skipped + failures + passes; console.log('%d Total Tests: %d passed, %d failed, %d skipped.', total, passes, failures, skipped); } - -// public API -exports.test = test; -exports.outputResults = outputResults; -exports.handleBuild = handleBuild; diff --git a/build/lib/utils.js b/build/lib/utils.js index e321dfd2421..f44eead95e5 100644 --- a/build/lib/utils.js +++ b/build/lib/utils.js @@ -1,20 +1,22 @@ -'use strict'; - -const util = require('util'); -const promisify = util.promisify; -const path = require('path'); -const fs = require('fs-extra'); -const os = require('os'); +import util from 'node:util'; +import path from 'node:path'; +import fs from 'fs-extra'; +import os from 'node:os'; +import child_process from 'node:child_process'; +import glob from 'glob'; +import appc from 'node-appc'; +import request from 'request'; +import ssri from 'ssri'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); const titanium = require.resolve('titanium'); -const exec = util.promisify(require('child_process').exec); // eslint-disable-line security/detect-child-process - -const glob = promisify(require('glob')); -const appc = require('node-appc'); -const request = require('request'); -const ssri = require('ssri'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const exec = util.promisify(child_process.exec); +const globPromise = util.promisify(glob); const tempDir = os.tmpdir(); -const Utils = {}; function leftpad(str, len, ch) { str = String(str); @@ -29,23 +31,23 @@ function leftpad(str, len, ch) { return str; } -Utils.timestamp = function () { +export function timestamp() { const date = new Date(); return '' + (date.getUTCMonth() + 1) + '/' + date.getUTCDate() + '/' + (date.getUTCFullYear()) + ' ' + leftpad(date.getUTCHours(), 2, '0') + ':' + leftpad(date.getUTCMinutes(), 2, '0'); -}; +} -Utils.copyFile = async function (srcFolder, destFolder, filename) { +export async function copyFile(srcFolder, destFolder, filename) { return fs.copy(path.join(srcFolder, filename), path.join(destFolder, filename)); -}; +} -Utils.copyFiles = async function (srcFolder, destFolder, files) { - return Promise.all(files.map(f => Utils.copyFile(srcFolder, destFolder, f))); -}; +export async function copyFiles(srcFolder, destFolder, files) { + return Promise.all(files.map(f => copyFile(srcFolder, destFolder, f))); +} -Utils.globCopy = async function (pattern, srcFolder, destFolder) { - const files = await glob(pattern, { cwd: srcFolder }); - return Utils.copyFiles(srcFolder, destFolder, files); -}; +export async function globCopy(pattern, srcFolder, destFolder) { + const files = await globPromise(pattern, { cwd: srcFolder }); + return copyFiles(srcFolder, destFolder, files); +} /** * @param {string} srcFolder source directory to copy from @@ -54,7 +56,7 @@ Utils.globCopy = async function (pattern, srcFolder, destFolder) { * @param {object} substitutions a mapping of substitutions to make in the contents while copying * @returns {Promise} */ -Utils.copyAndModifyFile = async function (srcFolder, destFolder, filename, substitutions) { +export async function copyAndModifyFile(srcFolder, destFolder, filename, substitutions) { // FIXME If this is a directory, we need to recurse into directory! // read in src file, modify contents, write to dest folder @@ -67,7 +69,7 @@ Utils.copyAndModifyFile = async function (srcFolder, destFolder, filename, subst } } return fs.writeFile(path.join(destFolder, filename), str); -}; +} /** * @param {string} srcFolder source directory to copy from @@ -76,9 +78,9 @@ Utils.copyAndModifyFile = async function (srcFolder, destFolder, filename, subst * @param {object} substitutions a mapping of substitutions to make in the contents while copying * @returns {Promise} */ -Utils.copyAndModifyFiles = async function (srcFolder, destFolder, files, substitutions) { - return Promise.all(files.map(f => Utils.copyAndModifyFile(srcFolder, destFolder, f, substitutions))); -}; +export async function copyAndModifyFiles(srcFolder, destFolder, files, substitutions) { + return Promise.all(files.map(f => copyAndModifyFile(srcFolder, destFolder, f, substitutions))); +} /** * @param {string} url the URL of a file to download @@ -174,7 +176,7 @@ async function downloadWithIntegrity(url, downloadPath, integrity, options) { * @param {string} url url of file we're caching * @returns {string} cache filepath (basicaly dir under tmp with the url file's basename appended) */ -function cachedDownloadPath(url) { +export function cachedDownloadPath(url) { // Use some consistent name so we can cache files! const cacheDir = path.join(process.env.SDK_BUILD_CACHE_DIR || tempDir, 'timob-build'); fs.ensureDirSync(cacheDir); @@ -183,9 +185,8 @@ function cachedDownloadPath(url) { // Place to download file return path.join(cacheDir, filename); } -Utils.cachedDownloadPath = cachedDownloadPath; -Utils.generateSSRIHashFromURL = async function (url) { +export async function generateSSRIHashFromURL(url) { if (url.startsWith('file://')) { // Generate integrity hash! return ssri.fromStream(fs.createReadStream(url.slice(7))); @@ -195,7 +196,7 @@ Utils.generateSSRIHashFromURL = async function (url) { await fs.remove(downloadPath); const file = await download(url, downloadPath); return ssri.fromStream(fs.createReadStream(file)); -}; +} /** * @param {string} url URL to module zipfile @@ -204,7 +205,7 @@ Utils.generateSSRIHashFromURL = async function (url) { * @param {boolean} [options.progress=true] show progress bar/spinner for download * @returns {Promise} path to file */ -Utils.downloadURL = async function downloadURL(url, integrity, options) { +export async function downloadURL(url, integrity, options) { if (!integrity) { throw new Error('No "integrity" value given for %s, may need to run "node scons.js modules-integrity" to generate new module listing with updated integrity hashes.', url); } @@ -237,16 +238,16 @@ Utils.downloadURL = async function downloadURL(url, integrity, options) { // download and verify integrity return downloadWithIntegrity(url, downloadPath, integrity, options); -}; +} /** * @param {string} zipFile the downloaded file to extract * @param {string} integrity SSRI generated integrity hash for the zip * @param {string} outDir filepath of directory to extract zip to */ -Utils.cacheUnzip = async function (zipFile, integrity, outDir) { - return Utils.cacheExtract(zipFile, integrity, outDir, Utils.unzip); -}; +export async function cacheUnzip(zipFile, integrity, outDir) { + return cacheExtract(zipFile, integrity, outDir, unzip); +} /** * @callback AsyncExtractFunction @@ -261,8 +262,8 @@ Utils.cacheUnzip = async function (zipFile, integrity, outDir) { * @param {string} outDir filepath of directory to extract the input file to * @param {AsyncExtractFunction} extractFunc function to call to extract/manipulate the input file */ -Utils.cacheExtract = async function (inFile, integrity, outDir, extractFunc) { - const { hashElement } = require('folder-hash'); +export async function cacheExtract(inFile, integrity, outDir, extractFunc) { + const { hashElement } = await import('folder-hash'); const exists = await fs.pathExists(outDir); // The integrity hash may contain characters like '/' which we need to convert // see https://en.wikipedia.org/wiki/Base64#Filenames @@ -290,21 +291,21 @@ Utils.cacheExtract = async function (inFile, integrity, outDir, extractFunc) { await extractFunc(inFile, outDir); const hash = await hashElement(outDir); return fs.writeJson(cacheFile, hash); -}; +} /** * @param {string} zipfile zip file to unzip * @param {string} dest destination folder to unzip to * @returns {Promise} */ -Utils.unzip = function unzip(zipfile, dest) { +export function unzip(zipfile, dest) { return util.promisify(appc.zip.unzip)(zipfile, dest, null); -}; +} /** * @returns {string} absolute path to SDK install root */ -Utils.sdkInstallDir = function () { +export function sdkInstallDir() { // TODO: try ti cli first as below? // TODO: Cache value switch (os.platform()) { @@ -318,7 +319,7 @@ Utils.sdkInstallDir = function () { default: return path.join(process.env.HOME, '.titanium'); } -}; +} // /** // * @return {Promise} path to Titanium SDK root dir @@ -344,8 +345,8 @@ Utils.sdkInstallDir = function () { * @param {boolean} [symlinkIfPossible=false] [description] * @returns {Promise} */ -Utils.installSDK = async function (versionTag, symlinkIfPossible = false) { - const dest = Utils.sdkInstallDir(); +export async function installSDK(versionTag, symlinkIfPossible = false) { + const dest = sdkInstallDir(); let osName = os.platform(); if (osName === 'darwin') { @@ -372,29 +373,29 @@ Utils.installSDK = async function (versionTag, symlinkIfPossible = false) { // try the zip const zipfile = path.join(distDir, `mobilesdk-${versionTag}-${osName}.zip`); - return Utils.unzip(zipfile, dest); -}; + return unzip(zipfile, dest); +} /** * @param {string} zipfile path to zipfile to install * @param {boolean} [select=false] select the sdk in ti cli after install? * @returns {Promise} */ -Utils.installSDKFromZipFile = async function (zipfile, select = false) { +export async function installSDKFromZipFile(zipfile, select = false) { const regexp = /mobilesdk-([^-]+)-(osx|win32|linux)\.zip$/; const matches = zipfile.match(regexp); const osName = matches[2]; const versionTag = matches[1]; // wipe existing - const dest = Utils.sdkInstallDir(); + const dest = sdkInstallDir(); await wipeInstalledSDK(dest, osName, versionTag); - await Utils.unzip(zipfile, dest); + await unzip(zipfile, dest); if (select) { return exec(`node "${titanium}" sdk select ${versionTag}`); } -}; +} /** * @param {string} dest base dir of SDK installs @@ -423,7 +424,7 @@ async function wipeInstalledSDK(dest, osName, versionTag) { * Remove all CI SDKs installed. Skip GA releases. * @returns {Promise} */ -Utils.cleanNonGaSDKs = async function cleanNonGaSDKs() { +export async function cleanNonGaSDKs() { const { stdout } = await exec(`node "${titanium}" sdk list -o json`); const out = JSON.parse(stdout); const installedSDKs = out.installed; @@ -446,18 +447,16 @@ Utils.cleanNonGaSDKs = async function cleanNonGaSDKs() { } else { console.log('No GA SDK installed, you might find that your next ti command execution will error'); } -}; +} /** * Removes the global modules and plugins dirs * @param {string} sdkDir filepath to sdk root install directory * @returns {Promise} */ -Utils.cleanupModules = async function cleanupModules(sdkDir) { +export async function cleanupModules(sdkDir) { const moduleDir = path.join(sdkDir, 'modules'); const pluginDir = path.join(sdkDir, 'plugins'); return Promise.all([ moduleDir, pluginDir ].map(dir => fs.remove(dir))); -}; - -module.exports = Utils; +} diff --git a/build/scons b/build/scons index c12d352b884..ace161a52fe 100755 --- a/build/scons +++ b/build/scons @@ -1,4 +1,2 @@ #!/usr/bin/env node -'use strict'; - -require('./scons.js'); +import './scons.js'; diff --git a/build/scons-async.js b/build/scons-async.js index b52f64d1b3e..89b95e41e54 100755 --- a/build/scons-async.js +++ b/build/scons-async.js @@ -1,12 +1,12 @@ #!/usr/bin/env node -'use strict'; -const path = require('path'); -const glob = require('glob'); -const yaml = require('js-yaml'); -const fs = require('fs-extra'); -const promisify = require('util').promisify; -const chalk = require('chalk'); +import path from 'node:path'; +import glob from 'glob'; +import yaml from 'js-yaml'; +import fs from 'fs-extra'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import { fileURLToPath } from 'node:url'; /** * @param {object} m method definition from yml apidocs @@ -71,6 +71,7 @@ async function checkFile(file) { * @returns {object[]} */ async function main() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); const apidocs = path.join(__dirname, '../apidoc'); const files = await promisify(glob)(`${apidocs}/**/*.yml`); const arr = await Promise.all(files.map(f => checkFile(f))); diff --git a/build/scons-build.js b/build/scons-build.js index d43c6c810b5..853065466f2 100755 --- a/build/scons-build.js +++ b/build/scons-build.js @@ -1,8 +1,13 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); program .option('-v, --sdk-version [version]', 'Override the SDK version we report', process.env.PRODUCT_VERSION || version) @@ -10,7 +15,6 @@ program .option('-a, --all', 'Build a ti.main.js file for every target OS') .parse(process.argv); -const Builder = require('./lib/builder'); new Builder(program.opts(), program.args).build() .then(() => process.exit(0)) .catch(err => { diff --git a/build/scons-check-ios-toplevel.js b/build/scons-check-ios-toplevel.js index 55aa8faf34f..845014c244f 100644 --- a/build/scons-check-ios-toplevel.js +++ b/build/scons-check-ios-toplevel.js @@ -1,9 +1,10 @@ #!/usr/bin/env node -'use strict'; -const fs = require('fs-extra'); -const path = require('path'); +import fs from 'fs-extra'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); let topTiModule = path.join(__dirname, '..', 'iphone/TitaniumKit/TitaniumKit/Sources/API/TopTiModule.m'); if (process.argv.length >= 3) { topTiModule = process.argv[2]; diff --git a/build/scons-check-lockfile.js b/build/scons-check-lockfile.js index d06571b28f2..1dfffd4dc82 100755 --- a/build/scons-check-lockfile.js +++ b/build/scons-check-lockfile.js @@ -5,13 +5,15 @@ * given version. This is extremely rare, but could occur if a developer * tried to manually update the lockfile (or a bad merge occurred) */ -'use strict'; -const lockfile = require('../package-lock.json'); +import fs from 'fs-extra'; + +const lockfile = fs.readJsonSync('package-lock.json'); let foundError = false; checkDependencies(lockfile.dependencies); + function checkDependencies(deps) { const packageNames = Object.keys(deps); for (const packageName of packageNames) { diff --git a/build/scons-clean-modules.js b/build/scons-clean-modules.js index 70d8096b9a4..794e539bf24 100755 --- a/build/scons-clean-modules.js +++ b/build/scons-clean-modules.js @@ -1,11 +1,10 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); +import { program } from 'commander'; +import { cleanupModules, sdkInstallDir } from './lib/utils.js'; program.parse(process.argv); -const { cleanupModules, sdkInstallDir } = require('./lib/utils'); const sdkRootDir = sdkInstallDir(); cleanupModules(sdkRootDir).then(() => process.exit(0)) .catch(e => { diff --git a/build/scons-clean-sdks.js b/build/scons-clean-sdks.js index 6243f9d2d1c..ba674f8dbb2 100755 --- a/build/scons-clean-sdks.js +++ b/build/scons-clean-sdks.js @@ -1,11 +1,10 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); +import { program } from 'commander'; +import { cleanNonGaSDKs } from './lib/utils.js'; program.parse(process.argv); -const { cleanNonGaSDKs } = require('./lib/utils'); cleanNonGaSDKs().then(() => process.exit(0)) .catch(e => { console.error(e); diff --git a/build/scons-clean.js b/build/scons-clean.js index c65fbe59005..eea91a00f58 100755 --- a/build/scons-clean.js +++ b/build/scons-clean.js @@ -1,8 +1,13 @@ #!/usr/bin/env node -'use strict'; -const version = require('../package.json').version; -const program = require('commander'); +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); program.option('-v, --sdk-version [version]', 'Override the SDK version we report', process.env.PRODUCT_VERSION || version) .option('-t, --version-tag [tag]', 'Override the SDK version tag we report') @@ -10,7 +15,6 @@ program.option('-v, --sdk-version [version]', 'Override the SDK version we repor .option('-a, --all', 'Clean every OS/platform') .parse(process.argv); -const Builder = require('./lib/builder'); new Builder(program.opts(), program.args).clean() .then(() => process.exit(0)) .catch(err => { diff --git a/build/scons-cleanbuild.js b/build/scons-cleanbuild.js index 0570f55104f..b920f44e640 100644 --- a/build/scons-cleanbuild.js +++ b/build/scons-cleanbuild.js @@ -1,8 +1,14 @@ #!/usr/bin/env node -'use strict'; -const version = require('../package.json').version; -const program = require('commander'); +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); + program .option('-v, --sdk-version [version]', 'Override the SDK version we report', process.env.PRODUCT_VERSION || version) .option('-t, --version-tag [tag]', 'Override the SDK version tag we report') @@ -14,7 +20,6 @@ program .parse(process.argv); async function main(program) { - const Builder = require('./lib/builder'); const builder = new Builder(program.opts(), program.args); await builder.clean(); await builder.build(); diff --git a/build/scons-deprecations.js b/build/scons-deprecations.js index ce3deb21530..4e3495d2d40 100755 --- a/build/scons-deprecations.js +++ b/build/scons-deprecations.js @@ -1,13 +1,13 @@ #!/usr/bin/env node -'use strict'; -const path = require('path'); -const glob = require('glob'); -const yaml = require('js-yaml'); -const fs = require('fs-extra'); -const promisify = require('util').promisify; -const semver = require('semver'); -const chalk = require('chalk'); +import path from 'node:path'; +import glob from 'glob'; +import yaml from 'js-yaml'; +import fs from 'fs-extra'; +import { promisify } from 'node:util'; +import semver from 'semver'; +import chalk from 'chalk'; +import { fileURLToPath } from 'node:url'; // TODO: Extract common code for parsing and visiting all yml apidocs // The code between this and removals is nearly identical @@ -137,6 +137,7 @@ function compareUnremoved(a, b) { * @returns {object[]} */ async function main() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); const apidocs = path.join(__dirname, '../apidoc'); const files = await promisify(glob)(`${apidocs}/**/*.yml`); const arr = await Promise.all(files.map(f => checkFile(f))); diff --git a/build/scons-generate-test.js b/build/scons-generate-test.js index ba3461b6cff..1811fe789a5 100644 --- a/build/scons-generate-test.js +++ b/build/scons-generate-test.js @@ -1,11 +1,16 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; +import { main } from './lib/test/generator.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); + program.version(version).parse(process.argv); -const main = require('./lib/test/generator'); main(program.args) .then(() => process.exit(0)) .catch(err => { diff --git a/build/scons-gradlew.js b/build/scons-gradlew.js index 555dfebfd02..723d0bee5ca 100755 --- a/build/scons-gradlew.js +++ b/build/scons-gradlew.js @@ -1,8 +1,12 @@ #!/usr/bin/env node -'use strict'; -const version = require('../package.json').version; -const program = require('commander'); +import { program } from 'commander'; +import fs from 'fs-extra'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); const argsIndex = process.argv.indexOf('--args'); const mainArgs = (argsIndex >= 0) ? process.argv.slice(0, argsIndex) : process.argv; @@ -14,8 +18,8 @@ program .option('-t, --version-tag [tag]', 'Override the SDK version tag we report') .option('-s, --android-sdk [path]', 'Explicitly set the path to the Android SDK used for building') .option('--args [arguments...]', 'Arguments to be passed to gradlew tool (Must be set last)') - .action((task, options, _command) => { - const AndroidBuilder = require('./lib/android'); + .action(async (task, options, _command) => { + const { AndroidBuilder } = await import('./lib/android.js'); new AndroidBuilder(options).runGradleTask(task, gradlewArgs) .then(() => process.exit(0)) .catch(err => { diff --git a/build/scons-install.js b/build/scons-install.js index 524f50c00d2..c2aa3267568 100755 --- a/build/scons-install.js +++ b/build/scons-install.js @@ -1,8 +1,14 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); + program .option('-v, --sdk-version [version]', 'Override the SDK version we report', process.env.PRODUCT_VERSION || version) .option('-t, --version-tag [tag]', 'Override the SDK version tag we report') @@ -11,7 +17,6 @@ program .parse(process.argv); async function main(program) { - const Builder = require('./lib/builder'); const builder = new Builder(program.opts(), program.args); return builder.install(program.args[0]); } diff --git a/build/scons-missing-tests.js b/build/scons-missing-tests.js index 37a492cee5b..6d24604771b 100644 --- a/build/scons-missing-tests.js +++ b/build/scons-missing-tests.js @@ -1,9 +1,13 @@ #!/usr/bin/env node -'use strict'; -const path = require('path'); -const fs = require('fs-extra'); -const yaml = require('js-yaml'); +import { program } from 'commander'; +import path from 'node:path'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); /** * @@ -73,8 +77,6 @@ async function getTypeName(ymlPath) { return typeNames; } -const program = require('commander'); -const version = require('../package.json').version; program.version(version).parse(process.argv); main(program) .then(() => process.exit(0)) diff --git a/build/scons-modules-integrity.js b/build/scons-modules-integrity.js index 391402eb40b..bbaf897c88a 100644 --- a/build/scons-modules-integrity.js +++ b/build/scons-modules-integrity.js @@ -1,9 +1,11 @@ #!/usr/bin/env node -'use strict'; -const utils = require('./lib/utils'); -const fs = require('fs-extra'); -const path = require('path'); +import { generateSSRIHashFromURL } from './lib/utils.js'; +import fs from 'fs-extra'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const modulesPath = path.join(__dirname, '../support/module/packaged/modules.json'); /** @@ -13,7 +15,7 @@ const modulesPath = path.join(__dirname, '../support/module/packaged/modules.jso * @returns {Promise} */ async function handleModule(moduleObject, moduleName) { - const hash = await utils.generateSSRIHashFromURL(moduleObject.url); + const hash = await generateSSRIHashFromURL(moduleObject.url); // eslint-disable-next-line require-atomic-updates moduleObject.integrity = hash.toString(); return { @@ -43,7 +45,7 @@ async function main() { throw new Error('The modules.json does not exist, aborting ...'); } - const modules = require(modulesPath); // eslint-disable-line security/detect-non-literal-require + const modules = fs.readJsonSync(modulesPath); const platforms = Object.keys(modules); console.log('Attempting to download...'); diff --git a/build/scons-package.js b/build/scons-package.js index 01024b11851..874a3daca3d 100755 --- a/build/scons-package.js +++ b/build/scons-package.js @@ -1,8 +1,14 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); + program .option('-a, --all', 'Build a zipfile for every OS') .option('-v, --sdk-version [version]', 'Override the SDK version we report', process.env.PRODUCT_VERSION || version) @@ -12,7 +18,6 @@ program .parse(process.argv); async function main(program) { - const Builder = require('./lib/builder'); const builder = new Builder(program.opts(), program.args); await builder.generateDocs(); return builder.package(); diff --git a/build/scons-removals.js b/build/scons-removals.js index f4ed70287d4..83812919783 100755 --- a/build/scons-removals.js +++ b/build/scons-removals.js @@ -1,13 +1,13 @@ #!/usr/bin/env node -'use strict'; -const path = require('path'); -const glob = require('glob'); -const yaml = require('js-yaml'); -const fs = require('fs-extra'); -const promisify = require('util').promisify; -const semver = require('semver'); -const chalk = require('chalk'); +import path from 'node:path'; +import glob from 'glob'; +import yaml from 'js-yaml'; +import fs from 'fs-extra'; +import { promisify } from 'node:util'; +import semver from 'semver'; +import chalk from 'chalk'; +import { fileURLToPath } from 'node:url'; /** * @param {object} thing type, property or method from docs @@ -134,6 +134,7 @@ function compareRemoved(a, b) { * @returns {object[]} */ async function main(version) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); const apidocs = path.join(__dirname, '../apidoc'); const files = await promisify(glob)(`${apidocs}/**/*.yml`); const arr = await Promise.all(files.map(f => checkFile(f))); diff --git a/build/scons-ssri.js b/build/scons-ssri.js index 7272b95bf24..bd87a3ca166 100644 --- a/build/scons-ssri.js +++ b/build/scons-ssri.js @@ -1,8 +1,7 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const utils = require('./lib/utils'); +import { program } from 'commander'; +import { generateSSRIHashFromURL } from './lib/utils.js'; program.parse(process.argv); @@ -15,7 +14,7 @@ if (urls.length <= 0) { async function main(urls) { console.log(urls); for (const url of urls) { - const hash = await utils.generateSSRIHashFromURL(url); + const hash = await generateSSRIHashFromURL(url); console.log(JSON.stringify({ url: url, integrity: hash.toString() diff --git a/build/scons-test.js b/build/scons-test.js index 7a0773f6919..0b4f2803ebe 100644 --- a/build/scons-test.js +++ b/build/scons-test.js @@ -1,8 +1,11 @@ #!/usr/bin/env node -'use strict'; -const program = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; +import { Builder } from './lib/builder.js'; + +const { version } = fs.readJsonSync('package.json'); + program .option('-C, --device-id [id]', 'Titanium device id to run the unit tests on. Only valid when there is a target provided') .option('-T, --target [target]', 'Titanium platform target to run the unit tests on. Only valid when there is a single platform provided') @@ -14,7 +17,6 @@ program .parse(process.argv); async function main(program) { - const Builder = require('./lib/builder'); return new Builder(program.opts(), program.args).test(); } diff --git a/build/scons-update-node-deps.js b/build/scons-update-node-deps.js index 87285e540e9..09994d7b652 100755 --- a/build/scons-update-node-deps.js +++ b/build/scons-update-node-deps.js @@ -1,10 +1,9 @@ #!/usr/bin/env node -'use strict'; -const exec = require('child_process').exec, // eslint-disable-line security/detect-child-process - spawn = require('child_process').spawn, // eslint-disable-line security/detect-child-process - fs = require('fs'), - path = require('path'); +import { exec, spawn } from 'node:child_process'; +import fs from 'fs-extra'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; if (parseInt(process.versions.modules) < 46) { console.error('You must run this using Node.js 4.0 or newer. Sorry.'); @@ -32,7 +31,7 @@ function rm(dir, ignore) { }); } -exec('npm -v', function (err, stdout) { +exec('npm -v', async function (err, stdout) { if (err) { console.error(err); process.exit(1); @@ -44,10 +43,11 @@ exec('npm -v', function (err, stdout) { } // make sure we're in the right directory + const __dirname = path.dirname(fileURLToPath(import.meta.url)); let titaniumDir = __dirname; while (1) { const p = path.join(titaniumDir, 'package.json'); - if (fs.existsSync(p) && require(p).name === 'titanium-mobile') { // eslint-disable-line security/detect-non-literal-require + if (fs.existsSync(p) && fs.readJsonSync(p).name === 'titanium-mobile') { break; } titaniumDir = path.dirname(titaniumDir); diff --git a/build/scons-xcode-project-build.js b/build/scons-xcode-project-build.js index cf838655df0..77db43d9dfc 100644 --- a/build/scons-xcode-project-build.js +++ b/build/scons-xcode-project-build.js @@ -1,12 +1,16 @@ #!/usr/bin/env node -'use strict'; - -const program = require('commander'); -const fs = require('fs-extra'); -const path = require('path'); -const IOS = require('./lib/ios'); -const Builder = require('./lib/builder'); -const { i18n } = require('node-titanium-sdk'); + +import { program } from 'commander'; +import fs from 'fs-extra'; +import path from 'node:path'; +import IOS from './lib/ios.js'; +import Builder from './lib/builder.js'; +import { i18n } from 'node-titanium-sdk'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const { version } = fs.readJsonSync(path.join(__dirname, '../package.json')); + program.parse(process.argv); const projectDir = program.args[0]; @@ -49,7 +53,7 @@ async function generateBundle(outputDir) { const builder = new Builder(options, [ 'ios' ]); await builder.ensureGitHash(); const ios = new IOS({ - sdkVersion: require('../package.json').version, + sdkVersion: version, gitHash: options.gitHash, timestamp: options.timestamp }); diff --git a/build/scons-xcode-test.js b/build/scons-xcode-test.js index 5731d48f2d3..83526ce8364 100644 --- a/build/scons-xcode-test.js +++ b/build/scons-xcode-test.js @@ -1,12 +1,11 @@ #!/usr/bin/env node -'use strict'; -const fs = require('fs-extra'); -const path = require('path'); -const promisify = require('util').promisify; -// eslint-disable-next-line security/detect-child-process -const execFile = promisify(require('child_process').execFile); +import fs from 'fs-extra'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TEST_SUITE_DIR = path.join(__dirname, '..', 'tests'); const JS_DIR = path.join(TEST_SUITE_DIR, 'Resources'); const DEST = path.join(__dirname, '..', 'iphone', 'Resources'); @@ -32,7 +31,7 @@ async function main() { } // TODO: We need to run npm install --production in DEST - await execFile('npm', [ 'ci', '--production' ], { cwd: DEST }); + execFileSync('npm', [ 'ci', '--production' ], { cwd: DEST }); } main() diff --git a/build/scons.js b/build/scons.js index 1911472e4f5..e9883292afa 100755 --- a/build/scons.js +++ b/build/scons.js @@ -1,10 +1,11 @@ #!/usr/bin/env node -'use strict'; -const commander = require('commander'); -const version = require('../package.json').version; +import { program } from 'commander'; +import fs from 'fs-extra'; -commander +const { version } = fs.readJsonSync('package.json'); + +program .version(version) .command('check-lockfile', 'Ensures there\'s no mismatch between version string and assumed version from url in our package-lock.json') .command('clean [platforms]', 'clean up build directories for one or more platforms') diff --git a/cli/commands/build.js b/cli/commands/build.js index 40c3c6e91cb..795e9bdfff2 100644 --- a/cli/commands/build.js +++ b/cli/commands/build.js @@ -5,17 +5,16 @@ * See the LICENSE file for more information. */ -'use strict'; - -const appc = require('node-appc'), - fields = require('fields'), - fs = require('fs-extra'), - jsanalyze = require('node-titanium-sdk/lib/jsanalyze'), - path = require('path'), - sprintf = require('sprintf'), - ti = require('node-titanium-sdk'), - tiappxml = require('node-titanium-sdk/lib/tiappxml'), - __ = appc.i18n(__dirname).__; +import appc from 'node-appc'; +import fields from 'fields'; +import fs from 'fs-extra'; +import path from 'node:path'; +import sprintf from 'sprintf'; +import ti from 'node-titanium-sdk'; +import tiappxml from 'node-titanium-sdk/lib/tiappxml.js'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); fields.setup({ formatters: { @@ -32,12 +31,12 @@ fields.setup({ } }); -exports.cliVersion = '>=3.2.1'; -exports.title = __('Build'); -exports.desc = __('builds a project'); -exports.extendedDesc = __('Builds an existing app or module project.'); +export const cliVersion = '>=3.2.1'; +export const title = 'Build'; +export const desc = 'builds a project'; +export const extendedDesc = 'Builds an existing app or module project.'; -exports.config = function config(logger, config, cli) { +export function config(logger, config, cli) { fields.setup({ colors: cli.argv.colors }); // start patching the logger here @@ -52,21 +51,21 @@ exports.config = function config(logger, config, cli) { flags: { 'build-only': { abbr: 'b', - desc: __('only perform the build; if true, does not install or run the app') + desc: 'only perform the build; if true, does not install or run the app' }, force: { abbr: 'f', - desc: __('force a full rebuild') + desc: 'force a full rebuild' }, legacy: { - desc: __('build using the old Python-based builder.py; deprecated') + desc: 'build using the old Python-based builder.py; deprecated' }, 'skip-js-minify': { default: false, - desc: __('bypasses JavaScript minification; %s builds are never minified; only supported for %s and %s', 'simulator'.cyan, 'Android'.cyan, 'iOS'.cyan) + desc: `bypasses JavaScript minification; ${'simulator'.cyan} builds are never minified; only supported for ${'Android'.cyan} and ${'iOS'.cyan}` }, 'source-maps': { - desc: __('generate inline source maps for transpiled JS files') + desc: 'generate inline source maps for transpiled JS files' }, }, options: appc.util.mix({ @@ -87,17 +86,17 @@ exports.config = function config(logger, config, cli) { return platform; }, - desc: __('the target build platform'), - hint: __('platform'), + desc: 'the target build platform', + hint: 'platform', order: 2, prompt: { - label: __('Target platform'), - error: __('Invalid platform'), + label: 'Target platform', + error: 'Invalid platform', validator: function (platform) { if (!platform) { - throw new Error(__('Invalid platform')); + throw new Error('Invalid platform'); } else if (ti.availablePlatforms.indexOf(platform) === -1) { - throw new Error(__('Invalid platform: %s', platform)); + throw new Error(`Invalid platform: ${platform}`); } return true; } @@ -172,12 +171,12 @@ exports.config = function config(logger, config, cli) { return projectDir; }, - desc: __('the directory containing the project'), + desc: 'the directory containing the project', default: process.env.SOURCE_ROOT ? path.join(process.env.SOURCE_ROOT, '..', '..') : '.', order: 1, prompt: function (callback) { callback(fields.file({ - promptLabel: __('Where is the __project directory__?'), + promptLabel: 'Where is the __project directory__?', complete: true, showHidden: true, ignoreDirs: new RegExp(config.get('cli.ignoreDirs')), // eslint-disable-line security/detect-non-literal-regexp @@ -191,7 +190,7 @@ exports.config = function config(logger, config, cli) { let dir = appc.fs.resolvePath(projectDir); if (!fs.existsSync(dir)) { - return callback(new Error(__('Project directory does not exist'))); + return callback(new Error('Project directory does not exist')); } const root = path.resolve('/'); @@ -226,7 +225,7 @@ exports.config = function config(logger, config, cli) { } if (!isFound) { - callback(new Error(__('Invalid project directory "%s" because tiapp.xml or timodule.xml not found', projectDir))); + callback(new Error(`Invalid project directory "${projectDir}" because tiapp.xml or timodule.xml not found`)); return; } callback(null, dir); @@ -241,9 +240,9 @@ exports.config = function config(logger, config, cli) { finished(result); }); }; -}; +} -exports.validate = function validate(logger, config, cli) { +export function validate(logger, config, cli) { // Determine if the project is an app or a module, run appropriate build command if (cli.argv.type === 'module') { @@ -291,41 +290,27 @@ exports.validate = function validate(logger, config, cli) { }); }; } -}; +} -exports.run = function run(logger, config, cli, finished) { +export async function run(logger, config, cli, finished) { const buildFile = cli.argv.type === 'module' ? '_buildModule.js' : '_build.js', platform = ti.resolvePlatform(cli.argv.platform), buildModule = path.join(__dirname, '..', '..', platform, 'cli', 'commands', buildFile); if (!fs.existsSync(buildModule)) { - logger.error(__('Unable to find platform specific build command') + '\n'); - logger.log(__('Your SDK installation may be corrupt. You can reinstall it by running \'%s\'.', (cli.argv.$ + ' sdk install --force --default').cyan) + '\n'); + logger.error('Unable to find platform specific build command\n'); + logger.log(`Your SDK installation may be corrupt. You can reinstall it by running '${(cli.argv.$ + ' sdk install --force --default').cyan}'.\n`); process.exit(1); } - if (config.get('cli.sendAPIUsage', true)) { - cli.on('build.finalize', function (builder) { - const deployType = builder.deployType || cli.argv['deploy-type'] || null; - if (deployType === 'production') { - cli.addAnalyticsEvent('Titanium API Usage', { - platform: platform, - tisdkname: (ti.manifest && ti.manifest.name) || (cli.sdk && cli.sdk.name) || null, - tisdkver: (ti.manifest && ti.manifest.version) || (cli.sdk && cli.sdk.name) || null, - deployType: deployType, - target: builder.target || cli.argv.target || null, - usage: jsanalyze.getAPIUsage() - }, 'ti.apiusage'); - } - }); - } - let counter = 0; - require(buildModule).run(logger, config, cli, function (err) { // eslint-disable-line security/detect-non-literal-require + const { run } = await import(buildModule); + + run(logger, config, cli, function (err) { // eslint-disable-line security/detect-non-literal-require if (!counter++) { const delta = appc.time.prettyDiff(cli.startTime, Date.now()); if (err) { - logger.error(__('An error occurred during build after %s', delta)); + logger.error(`An error occurred during build after ${delta}`); if (err instanceof appc.exception) { err.dump(logger.error); } else if (err !== true) { @@ -340,7 +325,7 @@ exports.run = function run(logger, config, cli, finished) { // eventually all platforms will just show how long the build took since they // are responsible for showing the own logging if (platform !== 'iphone' || cli.argv['build-only']) { - logger.info(__('Project built successfully in %s', delta.cyan) + '\n'); + logger.info(`Project built successfully in ${delta.cyan}\n`); } logger.log.end(); } @@ -348,7 +333,7 @@ exports.run = function run(logger, config, cli, finished) { finished(); } }); -}; +} /** * Monkey-patch the logger object to enable file logging during build @@ -439,26 +424,26 @@ function patchLogger(logger, cli) { logger.log([ new Date().toLocaleString(), '', - styleHeading(__('Operating System')), - ' ' + rpad(__('Name')) + ' = ' + styleValue(osInfo.os), - ' ' + rpad(__('Version')) + ' = ' + styleValue(osInfo.osver), - ' ' + rpad(__('Architecture')) + ' = ' + styleValue(osInfo.ostype), - ' ' + rpad(__('# CPUs')) + ' = ' + styleValue(osInfo.oscpu), - ' ' + rpad(__('Memory')) + ' = ' + styleValue(osInfo.memory), + styleHeading('Operating System'), + ' ' + rpad('Name') + ' = ' + styleValue(osInfo.os), + ' ' + rpad('Version') + ' = ' + styleValue(osInfo.osver), + ' ' + rpad('Architecture') + ' = ' + styleValue(osInfo.ostype), + ' ' + rpad('# CPUs') + ' = ' + styleValue(osInfo.oscpu), + ' ' + rpad('Memory') + ' = ' + styleValue(osInfo.memory), '', - styleHeading(__('Node.js')), - ' ' + rpad(__('Node.js Version')) + ' = ' + styleValue(osInfo.node), - ' ' + rpad(__('npm Version')) + ' = ' + styleValue(osInfo.npm), + styleHeading('Node.js'), + ' ' + rpad('Node.js Version') + ' = ' + styleValue(osInfo.node), + ' ' + rpad('npm Version') + ' = ' + styleValue(osInfo.npm), '', - styleHeading(__('Titanium CLI')), - ' ' + rpad(__('CLI Version')) + ' = ' + styleValue(cli.version), + styleHeading('Titanium CLI'), + ' ' + rpad('CLI Version') + ' = ' + styleValue(cli.version), '', - styleHeading(__('Titanium SDK')), - ' ' + rpad(__('SDK Version')) + ' = ' + styleValue(cli.argv.sdk), - ' ' + rpad(__('SDK Path')) + ' = ' + styleValue(cli.sdk.path), - ' ' + rpad(__('Target Platform')) + ' = ' + styleValue(ti.resolvePlatform(cli.argv.platform)), + styleHeading('Titanium SDK'), + ' ' + rpad('SDK Version') + ' = ' + styleValue(cli.argv.sdk), + ' ' + rpad('SDK Path') + ' = ' + styleValue(cli.sdk.path), + ' ' + rpad('Target Platform') + ' = ' + styleValue(ti.resolvePlatform(cli.argv.platform)), '', - styleHeading(__('Command')), + styleHeading('Command'), ' ' + styleValue(process.argv.join(' ')), '' ].join('\n')); diff --git a/cli/commands/clean.js b/cli/commands/clean.js index 0a155b7ca80..1630f16ef00 100644 --- a/cli/commands/clean.js +++ b/cli/commands/clean.js @@ -5,24 +5,22 @@ * See the LICENSE file for more information. */ -'use strict'; - -const appc = require('node-appc'), - i18n = appc.i18n(__dirname), - __ = i18n.__, - __n = i18n.__n, - ti = require('node-titanium-sdk'), - fs = require('fs-extra'), - path = require('path'), - sprintf = require('sprintf'), - async = require('async'), - tiappxml = require('node-titanium-sdk/lib/tiappxml'), - fields = require('fields'); - -exports.cliVersion = '>=3.2.1'; -exports.desc = __('removes previous build directories'); - -exports.config = function (logger, config, cli) { +import appc from 'node-appc'; +import ti from 'node-titanium-sdk'; +import fs from 'fs-extra'; +import path from 'node:path'; +import sprintf from 'sprintf'; +import async from 'async'; +import tiappxml from 'node-titanium-sdk/lib/tiappxml.js'; +import fields from 'fields'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const cliVersion = '>=3.2.1'; +export const desc = 'removes previous build directories'; + +export function config(logger, config, cli) { // start patching the logger here patchLogger(logger, cli); @@ -37,7 +35,7 @@ exports.config = function (logger, config, cli) { platforms: { // note: --platforms is not required for the clean command abbr: 'p', - desc: __('one or more platforms to clean'), + desc: 'one or more platforms to clean', values: ti.targetPlatforms, skipValueCheck: true // we do our own validation }, @@ -107,12 +105,12 @@ exports.config = function (logger, config, cli) { return projectDir; }, - desc: __('the directory containing the project, otherwise the current working directory'), + desc: 'the directory containing the project, otherwise the current working directory', default: process.env.SOURCE_ROOT ? path.join(process.env.SOURCE_ROOT, '..', '..') : '.', order: 1, prompt: function (callback) { callback(fields.file({ - promptLabel: __('Where is the __project directory__?'), + promptLabel: 'Where is the __project directory__?', complete: true, showHidden: true, ignoreDirs: new RegExp(config.get('cli.ignoreDirs')), // eslint-disable-line security/detect-non-literal-regexp @@ -125,7 +123,7 @@ exports.config = function (logger, config, cli) { let dir = appc.fs.resolvePath(projectDir); if (!fs.existsSync(dir)) { - return callback(new Error(__('Project directory does not exist'))); + return callback(new Error('Project directory does not exist')); } const root = path.resolve('/'); @@ -160,7 +158,7 @@ exports.config = function (logger, config, cli) { } if (!isFound) { - callback(new Error(__('Invalid project directory "%s" because tiapp.xml or timodule.xml not found', projectDir))); + callback(new Error(`Invalid project directory "${projectDir}" because tiapp.xml or timodule.xml not found`)); return; } callback(null, dir); @@ -173,9 +171,9 @@ exports.config = function (logger, config, cli) { finished(result); }); }; -}; +} -exports.validate = function (logger, config, cli) { +export function validate(logger, config, cli) { // Determine if the project is an app or a module, run appropriate clean command if (cli.argv.type === 'module') { @@ -199,8 +197,12 @@ exports.validate = function (logger, config, cli) { platforms = ti.scrubPlatforms(platforms); if (platforms.bad.length) { - logger.error(__n('Invalid platform: %%s', 'Invalid platforms: %%s', platforms.bad.length, platforms.bad.join(', ')) + '\n'); - logger.log(__('Available platforms for SDK version %s:', ti.manifest.sdkVersion) + '\n'); + if (platforms.bad.length === 1) { + logger.error(`Invalid platform: ${platforms.bad.join(', ')}\n`); + } else { + logger.error(`Invalid platforms: ${platforms.bad.join(', ')}\n`); + } + logger.log(`Available platforms for SDK version ${ti.manifest.sdkVersion}:\n`); ti.targetPlatforms.forEach(function (p) { logger.log(' ' + p.cyan); }); @@ -221,14 +223,14 @@ exports.validate = function (logger, config, cli) { }); }; } -}; +} -exports.run = function (logger, config, cli) { +export function run(logger, config, cli) { function done(err) { if (err) { - logger.error(__('Failed to clean project in %s', appc.time.prettyDiff(cli.startTime, Date.now())) + '\n'); + logger.error(`Failed to clean project in ${appc.time.prettyDiff(cli.startTime, Date.now())}\n`); } else { - logger.info(__('Project cleaned successfully in %s', appc.time.prettyDiff(cli.startTime, Date.now())) + '\n'); + logger.info(`Project cleaned successfully in ${appc.time.prettyDiff(cli.startTime, Date.now())}\n`); } } @@ -237,21 +239,22 @@ exports.run = function (logger, config, cli) { const platform = ti.resolvePlatform(cli.argv.platform); const cleanModule = path.join(__dirname, '..', '..', platform, 'cli', 'commands', '_cleanModule.js'); if (!fs.existsSync(cleanModule)) { - logger.error(__('Unable to find platform specific module clean command') + '\n'); - logger.log(__('Your SDK installation may be corrupt. You can reinstall it by running \'%s\'.', (cli.argv.$ + ' sdk install --force --default').cyan) + '\n'); + logger.error('Unable to find platform specific module clean command\n'); + logger.log(`Your SDK installation may be corrupt. You can reinstall it by running '${(cli.argv.$ + ' sdk install --force --default').cyan}'.\n`); process.exit(1); } // Now wrap the actual cleaning of the module (specific to a given platform), // in hooks so a module itself could potentially do additional cleanup itself - cli.fireHook('clean.module.pre', function () { - cli.fireHook('clean.module.' + platform + '.pre', function () { + cli.emit('clean.module.pre', function () { + cli.emit('clean.module.' + platform + '.pre', async function () { // Do the actual cleaning per-sdk _cleanModule command - require(cleanModule).run(logger, config, cli, function (err) { // eslint-disable-line security/detect-non-literal-require + const { run } = await import(cleanModule); + run(logger, config, cli, function (err) { // eslint-disable-line security/detect-non-literal-require const delta = appc.time.prettyDiff(cli.startTime, Date.now()); if (err) { - logger.error(__('An error occurred during clean after %s', delta)); + logger.error(`An error occurred during clean after ${delta}`); if (err instanceof appc.exception) { err.dump(logger.error); } else if (err !== true) { @@ -266,8 +269,8 @@ exports.run = function (logger, config, cli) { logger.log.end(); } - cli.fireHook('clean.module.' + platform + '.post', function () { - cli.fireHook('clean.module.post', function () { + cli.emit('clean.module.' + platform + '.post', function () { + cli.emit('clean.module.post', function () { done(); }); }); @@ -282,24 +285,24 @@ exports.run = function (logger, config, cli) { return function (next) { // scan platform SDK specific clean hooks cli.scanHooks(path.join(__dirname, '..', '..', platform, 'cli', 'hooks')); - cli.fireHook('clean.pre', function () { - cli.fireHook('clean.' + platform + '.pre', function () { + cli.emit('clean.pre', function () { + cli.emit('clean.' + platform + '.pre', function () { var dir = path.join(buildDir, platform); if (appc.fs.exists(dir)) { - logger.debug(__('Deleting %s', dir.cyan)); + logger.debug(`Deleting ${dir.cyan}`); fs.removeSync(dir); } else { - logger.debug(__('Directory does not exist %s', dir.cyan)); + logger.debug(`Directory does not exist ${dir.cyan}`); } dir = path.join(buildDir, 'build_' + platform + '.log'); if (appc.fs.exists(dir)) { - logger.debug(__('Deleting %s', dir.cyan)); + logger.debug(`Deleting ${dir.cyan}`); fs.unlinkSync(dir); } else { - logger.debug(__('Build log does not exist %s', dir.cyan)); + logger.debug(`Build log does not exist ${dir.cyan}`); } - cli.fireHook('clean.' + platform + '.post', function () { - cli.fireHook('clean.post', function () { + cli.emit('clean.' + platform + '.post', function () { + cli.emit('clean.post', function () { next(); }); }); @@ -308,7 +311,7 @@ exports.run = function (logger, config, cli) { }; }), done); } else if (appc.fs.exists(buildDir)) { - logger.debug(__('Deleting all platform build directories')); + logger.debug('Deleting all platform build directories'); // scan platform SDK specific clean hooks if (ti.targetPlatforms) { @@ -317,30 +320,30 @@ exports.run = function (logger, config, cli) { }); } - cli.fireHook('clean.pre', function () { + cli.emit('clean.pre', function () { async.series(fs.readdirSync(buildDir).map(function (dir) { return function (next) { var file = path.join(buildDir, dir); - cli.fireHook('clean.' + dir + '.pre', function () { - logger.debug(__('Deleting %s', file.cyan)); + cli.emit('clean.' + dir + '.pre', function () { + logger.debug(`Deleting ${file.cyan}`); fs.removeSync(file); - cli.fireHook('clean.' + dir + '.post', function () { + cli.emit('clean.' + dir + '.post', function () { next(); }); }); }; }), function () { - cli.fireHook('clean.post', function () { + cli.emit('clean.post', function () { done(); }); }); }); } else { - logger.debug(__('Directory does not exist %s', buildDir.cyan)); + logger.debug(`Directory does not exist ${buildDir.cyan}`); done(); } } -}; +} /** * Monkey-patch the logger object to enable file logging during build @@ -431,26 +434,26 @@ function patchLogger(logger, cli) { logger.log([ new Date().toLocaleString(), '', - styleHeading(__('Operating System')), - ' ' + rpad(__('Name')) + ' = ' + styleValue(osInfo.os), - ' ' + rpad(__('Version')) + ' = ' + styleValue(osInfo.osver), - ' ' + rpad(__('Architecture')) + ' = ' + styleValue(osInfo.ostype), - ' ' + rpad(__('# CPUs')) + ' = ' + styleValue(osInfo.oscpu), - ' ' + rpad(__('Memory')) + ' = ' + styleValue(osInfo.memory), + styleHeading('Operating System'), + ' ' + rpad('Name') + ' = ' + styleValue(osInfo.os), + ' ' + rpad('Version') + ' = ' + styleValue(osInfo.osver), + ' ' + rpad('Architecture') + ' = ' + styleValue(osInfo.ostype), + ' ' + rpad('# CPUs') + ' = ' + styleValue(osInfo.oscpu), + ' ' + rpad('Memory') + ' = ' + styleValue(osInfo.memory), '', - styleHeading(__('Node.js')), - ' ' + rpad(__('Node.js Version')) + ' = ' + styleValue(osInfo.node), - ' ' + rpad(__('npm Version')) + ' = ' + styleValue(osInfo.npm), + styleHeading('Node.js'), + ' ' + rpad('Node.js Version') + ' = ' + styleValue(osInfo.node), + ' ' + rpad('npm Version') + ' = ' + styleValue(osInfo.npm), '', - styleHeading(__('Titanium CLI')), - ' ' + rpad(__('CLI Version')) + ' = ' + styleValue(cli.version), + styleHeading('Titanium CLI'), + ' ' + rpad('CLI Version') + ' = ' + styleValue(cli.version), '', - styleHeading(__('Titanium SDK')), - ' ' + rpad(__('SDK Version')) + ' = ' + styleValue(cli.argv.sdk), - ' ' + rpad(__('SDK Path')) + ' = ' + styleValue(cli.sdk.path), - ' ' + rpad(__('Target Platform')) + ' = ' + styleValue(ti.resolvePlatform(cli.argv.platform)), + styleHeading('Titanium SDK'), + ' ' + rpad('SDK Version') + ' = ' + styleValue(cli.argv.sdk), + ' ' + rpad('SDK Path') + ' = ' + styleValue(cli.sdk.path), + ' ' + rpad('Target Platform') + ' = ' + styleValue(ti.resolvePlatform(cli.argv.platform)), '', - styleHeading(__('Command')), + styleHeading('Command'), ' ' + styleValue(process.argv.join(' ')), '' ].join('\n')); diff --git a/cli/commands/create.js b/cli/commands/create.js index 2b32008b4fa..b7ada72a0db 100644 --- a/cli/commands/create.js +++ b/cli/commands/create.js @@ -11,24 +11,25 @@ * Please see the LICENSE included with this distribution for details. */ -'use strict'; - -const appc = require('node-appc'), - async = require('async'), - fields = require('fields'), - fs = require('fs'), - i18n = appc.i18n(__dirname), - path = require('path'), - ti = require('node-titanium-sdk'), - { execSync } = require('child_process'), - __ = i18n.__; - -exports.cliVersion = '>=3.2.1'; -exports.title = __('Create'); -exports.desc = __('creates a new project'); -exports.extendedDesc = __('Creates a new Titanium application, native module, or Apple Watch™ app.') + '\n\n' - + __('Apple, iPhone, and iPad are registered trademarks of Apple Inc. Apple Watch is a trademark of Apple Inc.') + '\n\n' - + __('Android is a trademark of Google Inc.'); +import appc from 'node-appc'; +import async from 'async'; +import fields from 'fields'; +import fs from 'node:fs'; +import path from 'node:path'; +import ti from 'node-titanium-sdk'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const cliVersion = '>=3.2.1'; +export const title = 'Create'; +export const desc = 'creates a new project'; +export const extendedDesc = `Creates a new Titanium application, native module, or Apple Watch™ app. + +Apple, iPhone, and iPad are registered trademarks of Apple Inc. Apple Watch is a trademark of Apple Inc. + +Android is a trademark of Google Inc.`; /** * Encapsulates the create command's state. @@ -36,164 +37,168 @@ exports.extendedDesc = __('Creates a new Titanium application, native module, or * @classdesc Implements the CLI command interface for the create command. * @constructor */ -function CreateCommand() { - this.creators = {}; -} - -/** - * Defines the create command's CLI configuration. - * @param {Object} logger - The logger instance - * @param {Object} config - The CLI config - * @param {Object} cli - The CLI instance - * @return {Function} - */ -CreateCommand.prototype.config = function config(logger, config, cli) { - this.logger = logger; - this.config = config; - this.cli = cli; - - fields.setup({ colors: cli.argv.colors }); - - return function (finished) { - // find and load the creators - const creatorDir = path.join(__dirname, '..', 'lib', 'creators'), - jsRegExp = /\.js$/, - typeConf = {}; - - async.eachSeries(fs.readdirSync(creatorDir), function (filename, next) { - if (!jsRegExp.test(filename)) { - return next(); - } - - const CreatorConstructor = require(path.join(creatorDir, filename)); // eslint-disable-line security/detect-non-literal-require - const creator = new CreatorConstructor(logger, config, cli); - this.creators[creator.type] = creator; +export class CreateCommand { + constructor() { + this.creators = {}; + } + + /** + * Defines the create command's CLI configuration. + * @param {Object} logger - The logger instance + * @param {Object} config - The CLI config + * @param {Object} cli - The CLI instance + * @return {Function} + */ + config(logger, config, cli) { + this.logger = logger; + this.config = config; + this.cli = cli; + + fields.setup({ colors: cli.argv.colors }); + + return function (finished) { + // find and load the creators + const creatorDir = path.join(__dirname, '..', 'lib', 'creators'), + jsRegExp = /\.js$/, + typeConf = {}; + + async.eachSeries(fs.readdirSync(creatorDir), function (filename, next) { + if (!jsRegExp.test(filename)) { + return next(); + } - try { - if (typeof creator.init === 'function') { - if (creator.init.length > 1) { - typeConf[creator.type] = creator.init(function (conf) { - typeConf[creator.type] = conf; + import(path.join(creatorDir, filename)) + .then(({ default: CreatorConstructor }) => { + const creator = new CreatorConstructor(logger, config, cli); + this.creators[creator.type] = creator; + + try { + if (typeof creator.init === 'function') { + if (creator.init.length > 1) { + typeConf[creator.type] = creator.init(function (conf) { + typeConf[creator.type] = conf; + next(); + }); + return; + } + typeConf[creator.type] = creator.init(); + } + } catch (ex) { + // squeltch + delete this.creators[creator.type]; + } finally { next(); - }); - return; + } + }) + .catch(err => next(err)); + }.bind(this), function () { + cli.createHook('create.config', this, function (callback) { + var conf = { + flags: { + force: { + abbr: 'f', + desc: 'force project creation even if path already exists' + } + }, + options: appc.util.mix({ + type: { + abbr: 't', + default: cli.argv.prompt ? undefined : 'app', + desc: 'the type of project to create', + order: 100, + prompt: function (callback) { + callback(fields.select({ + title: 'What type of project would you like to create?', + promptLabel: 'Select a type by number or name', + default: 'app', + margin: '', + numbered: true, + relistOnError: true, + complete: true, + suggest: false, + options: Object.keys(this.creators) + .map(function (type) { + return { + label: this.creators[type].title || type, + value: type, + order: this.creators[type].titleOrder + }; + }, this) + .sort(function (a, b) { + return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; + }) + })); + }.bind(this), + required: true, + values: Object.keys(this.creators) + } + }, ti.commonOptions(logger, config)), + type: typeConf + }; + + callback(null, conf); + })(function (err, result) { + finished(result); + }); + }.bind(this)); + }.bind(this); + } + + /** + * Performs the project creation including making the project directory and copying + * the project template files. + * @param {Object} logger - The logger instance + * @param {Object} config - The CLI config + * @param {Object} cli - The CLI instance + * @param {Function} finished - A callback to fire when the project has been created + */ + run(logger, config, cli, finished) { + var type = cli.argv.type, + creator = this.creators[type]; + + // load the project type lib + logger.info(`Creating ${type.cyan} project`); + + appc.async.series(this, [ + function (next) { + cli.emit([ + 'create.pre', + 'create.pre.' + type + ], creator, next); + }, + + function (next) { + creator.run(function (err) { + if (err) { + logger.error(err.message || err.toString()); + next(err); + } else { + cli.emit([ + 'create.post.' + type, + 'create.post' + ], creator, next); } - typeConf[creator.type] = creator.init(); - } - } catch (ex) { - // squeltch - delete this.creators[creator.type]; - } finally { - next(); + }); } - }.bind(this), function () { - cli.createHook('create.config', this, function (callback) { - var conf = { - flags: { - force: { - abbr: 'f', - desc: __('force project creation even if path already exists') - } - }, - options: appc.util.mix({ - type: { - abbr: 't', - default: cli.argv.prompt ? undefined : 'app', - desc: __('the type of project to create'), - order: 100, - prompt: function (callback) { - callback(fields.select({ - title: __('What type of project would you like to create?'), - promptLabel: __('Select a type by number or name'), - default: 'app', - margin: '', - numbered: true, - relistOnError: true, - complete: true, - suggest: false, - options: Object.keys(this.creators) - .map(function (type) { - return { - label: this.creators[type].title || type, - value: type, - order: this.creators[type].titleOrder - }; - }, this) - .sort(function (a, b) { - return a.order < b.order ? -1 : a.order > b.order ? 1 : 0; - }) - })); - }.bind(this), - required: true, - values: Object.keys(this.creators) - } - }, ti.commonOptions(logger, config)), - type: typeConf - }; - - callback(null, conf); - })(function (err, result) { - finished(result); - }); - }.bind(this)); - }.bind(this); -}; - -/** - * Performs the project creation including making the project directory and copying - * the project template files. - * @param {Object} logger - The logger instance - * @param {Object} config - The CLI config - * @param {Object} cli - The CLI instance - * @param {Function} finished - A callback to fire when the project has been created - */ -CreateCommand.prototype.run = function run(logger, config, cli, finished) { - var type = cli.argv.type, - creator = this.creators[type]; - - // load the project type lib - logger.info(__('Creating %s project', type.cyan)); - - appc.async.series(this, [ - function (next) { - cli.emit([ - 'create.pre', - 'create.pre.' + type - ], creator, next); - }, - - function (next) { - creator.run(function (err) { + ], function (err) { + cli.emit('create.finalize', creator, function () { if (err) { - logger.error(err.message || err.toString()); - next(err); + logger.error(`Failed to create project after ${appc.time.prettyDiff(cli.startTime, Date.now())}\n`); } else { - cli.emit([ - 'create.post.' + type, - 'create.post' - ], creator, next); + logger.info(`Project created successfully in ${appc.time.prettyDiff(cli.startTime, Date.now())}\n`); } - }); - } - ], function (err) { - cli.emit('create.finalize', creator, function () { - if (err) { - logger.error(__('Failed to create project after %s', appc.time.prettyDiff(cli.startTime, Date.now())) + '\n'); - } else { - logger.info(__('Project created successfully in %s', appc.time.prettyDiff(cli.startTime, Date.now())) + '\n'); - } - if (cli.argv.alloy !== undefined) { - execSync(`alloy new "${path.join(cli.argv['workspace-dir'], cli.argv.name)}"`, { stdio: 'inherit' }); - } + if (cli.argv.alloy !== undefined) { + execSync(`alloy new "${path.join(cli.argv['workspace-dir'], cli.argv.name)}"`, { stdio: 'inherit' }); + } - finished(err); + finished(err); + }); }); - }); -}; + } +} // create the builder instance and expose the public api -(function (createCommand) { - exports.config = createCommand.config.bind(createCommand); - exports.run = createCommand.run.bind(createCommand); -}(new CreateCommand())); +const createCommand = new CreateCommand(); +export const config = createCommand.config.bind(createCommand); +export const run = createCommand.run.bind(createCommand); diff --git a/cli/commands/project.js b/cli/commands/project.js index 45a826f3be7..47329594d04 100644 --- a/cli/commands/project.js +++ b/cli/commands/project.js @@ -4,67 +4,63 @@ * Copyright TiDev, Inc. 04/07/2022-Present All Rights Reserved. * See the LICENSE file for more information. */ -'use strict'; -const path = require('path'), - ti = require('node-titanium-sdk'), - appc = require('node-appc'), - __ = appc.i18n(__dirname).__, - mix = appc.util.mix; +import path from 'node:path'; +import ti from 'node-titanium-sdk'; +import appc from 'node-appc'; -exports.cliVersion = '>=3.2.1'; -exports.desc = __('get and set tiapp.xml settings'); -exports.extendedDesc = [ - __('Get and set tiapp.xml settings.'), - __('Run %s to see all available entries that can be changed.', 'titanium project --project-dir /path/to/project'.cyan), - [ __('When setting the %s entry, it will non-destructively copy each specified ', 'deployment-targets'.cyan), - __('platform\'s default resources into your project\'s Resources folder. For '), - __('example, if your app currently supports %s and you wish to add Android ', 'iphone'.cyan), - __('support, you must specify %s, otherwise only specifying %s will remove ', 'iphone,android'.cyan), - __('support for iPhone.', 'android'.cyan) - ].join('') -].join('\n\n'); +export const cliVersion = '>=3.2.1'; +export const desc = 'get and set tiapp.xml settings'; +export const extendedDesc = `Get and set tiapp.xml settings. -exports.config = function (logger, config) { +Run ${'titanium project --project-dir /path/to/project'.cyan} to see all available entries that can be changed. + +When setting the ${'deployment-targets'.cyan} entry, it will non-destructively copy each specified +platform's default resources into your project's Resources folder. For +example, if your app currently supports ${'iphone'.cyan} and you wish to add Android +support, you must specify ${'iphone,android'.cyan}, otherwise only specifying ${'android'.cyan} will remove +support for iPhone.`; + +export function config(logger, config) { return { skipBanner: true, - options: mix({ + options: Object.assign({ output: { abbr: 'o', default: 'report', - desc: __('output format'), + desc: 'output format', values: [ 'report', 'json', 'text' ] }, 'project-dir': { - desc: __('the directory of the project to analyze'), + desc: 'the directory of the project to analyze', default: '.' }, template: { - desc: __('the name of the project template to use'), + desc: 'the name of the project template to use', default: 'default' } }, ti.commonOptions(logger, config)), args: [ { name: 'key', - desc: __('the key to get or set') + desc: 'the key to get or set' }, { name: 'value', - desc: __('the value to set the specified key') + desc: 'the value to set the specified key' } ] }; -}; +} -exports.validate = function (logger, config, cli) { +export function validate(logger, config, cli) { ti.validateProjectDir(logger, cli, cli.argv, 'project-dir'); // Validate the key, if it exists if (cli.argv._.length > 0) { const key = cli.argv._[0]; if (!/^([A-Za-z_]{1}[A-Za-z0-9-_]*(\.[A-Za-z-_]{1}[A-Za-z0-9-_]*)*)$/.test(key)) { - logger.error(__('Invalid key "%s"', key) + '\n'); + logger.error(`Invalid key "${key}"\n`); process.exit(1); } } @@ -72,9 +68,9 @@ exports.validate = function (logger, config, cli) { return function (finished) { ti.loadPlugins(null, config, cli, cli.argv['project-dir'], finished, cli.argv.output !== 'report' || cli.argv._.length, false); }; -}; +} -exports.run = function (logger, config, cli, finished) { +export function run(logger, config, cli, finished) { var projectDir = cli.argv['project-dir'], tiappPath = path.join(projectDir, 'tiapp.xml'), tiapp = new ti.tiappxml(tiappPath), @@ -88,7 +84,7 @@ exports.run = function (logger, config, cli, finished) { maxlen, sdkPath = cli.sdk.path, templateDir, - propsList = [ 'sdk-version', 'id', 'name', 'version', 'publisher', 'url', 'description', 'copyright', 'icon', 'analytics', 'guid' ], + propsList = [ 'sdk-version', 'id', 'name', 'version', 'publisher', 'url', 'description', 'copyright', 'icon', 'guid' ], deploymentTargets = tiapp['deployment-targets']; args.length === 0 && output === 'report' && logger.banner(); @@ -113,22 +109,22 @@ exports.run = function (logger, config, cli, finished) { } else { // Print the deployment targets - logger.log(__('Deployment Targets:')); + logger.log('Deployment Targets:'); maxlen = Object.keys(deploymentTargets).reduce(function (a, b) { return Math.max(a, b.length); }, 0); for (p in tiapp['deployment-targets']) { - logger.log(' %s = %s', appc.string.rpad(p, maxlen), (deploymentTargets[p] + '').cyan); + logger.log(` ${appc.string.rpad(p, maxlen)} = ${(deploymentTargets[p] + '').cyan}`); } logger.log(); // Print the other properties - logger.log(__('Project Properties:')); + logger.log('Project Properties:'); maxlen = propsList.reduce(function (a, b) { return Math.max(a, b.length); }, 0); propsList.forEach(function (key) { - logger.log(' %s = %s', appc.string.rpad(key, maxlen), String(tiapp[key] || __('not specified')).cyan); + logger.log(` ${appc.string.rpad(key, maxlen)} = ${String(tiapp[key] || 'not specified').cyan}`); }); logger.log(); } @@ -153,12 +149,12 @@ exports.run = function (logger, config, cli, finished) { logger.log(result.join(',')); } else { // Print the deployment targets - logger.log(__('Deployment Targets:')); + logger.log('Deployment Targets:'); maxlen = Object.keys(deploymentTargets).reduce(function (a, b) { return Math.max(a, b.length); }, 0); for (p in tiapp['deployment-targets']) { - logger.log(' %s = %s', appc.string.rpad(p, maxlen), (deploymentTargets[p] + '').cyan); + logger.log(` ${appc.string.rpad(p, maxlen)} = ${(deploymentTargets[p] + '').cyan}`); } logger.log(); } @@ -172,7 +168,7 @@ exports.run = function (logger, config, cli, finished) { if (output === 'json') { logger.log('null'); } else { - logger.error(__('%s is not a valid entry name', key) + '\n'); + logger.error(`${key} is not a valid entry name\n`); } process.exit(1); } @@ -195,8 +191,8 @@ exports.run = function (logger, config, cli, finished) { value = args[1].split(','); value.forEach(function (p) { if (!Object.prototype.hasOwnProperty.call(result, p)) { - logger.error(__('Unsupported deployment target "%s"', p) + '\n'); - logger.log(__('Available deployment targets are:')); + logger.error(`Unsupported deployment target "${p}"\n`); + logger.log('Available deployment targets are:'); Object.keys(result).sort().forEach(function (p) { logger.log(' ' + p.cyan); }); @@ -215,7 +211,7 @@ exports.run = function (logger, config, cli, finished) { // Non-destructively copy over files from /templates/app/