UNPKG

appc-cli-titanium

Version:
1,076 lines (963 loc) 33.1 kB
/** * This code is closed source and Confidential and Proprietary to * Appcelerator, Inc. All Rights Reserved. This code MUST not be * modified, copied or otherwise redistributed without express * written permission of Appcelerator. This file is licensed as * part of the Appcelerator Platform and governed under the terms * of the Appcelerator license agreement. */ var child_process = require('child_process'), // eslint-disable-line security/detect-child-process spawn = child_process.spawn, exec = child_process.exec, path = require('path'), fs = require('fs-extra'), _ = require('lodash'), tiappxml = require('tiapp.xml'), urllib = require('url'), format = require('util').format, async = require('async'), platformSdk = require('appc-platform-sdk'), PacProxyAgent = require('pac-proxy-agent'), nodeAppc = require('node-appc'), AdmZip = require('adm-zip'), url = require('url'), tmpdir = require('os').tmpdir(); var IS_WIN = /^win/.test(process.platform); function cleanArgs(args) { // remove some appc-cli specific args so they don't get passed to ti var newargs = []; for (var c = 0; c < args.length; c++) { var arg = args[c]; if (/^--(register|session|pluginPaths)$/.test(arg)) { c++; // skip value too continue; } newargs.push(arg); } return newargs; } function getTiBinary() { return path.resolve(path.dirname(require.resolve('titanium')) + '/../bin/titanium'); } /** * return the latest Titanium SDK release (from the network) */ function getLatestTiSDK(appc, callback) { launchTi(appc, [ 'sdk', '-r', '-o', 'json', '--no-banner', '--no-prompt' ], {}, function (err, ti) { if (err) { return callback(err); } var stdout = ''; ti.stdout.on('data', function (buf) { buf = String(buf).replace(/\s$/, ''); appc.log.trace(buf); stdout += buf; }); ti.on('close', function (err) { if (err) { return callback(err); } var json = JSON.parse(stdout || '{}'); if (json && json.releases) { var first = Object.keys(json.releases)[0]; return callback(null, first, first); } }); }); } /** * get titanium sdk information */ function getTiSdk(appc, callback) { var child = spawn(process.execPath, [ getTiBinary(), 'sdk', '-o', 'json' ]), json = ''; child.stdout.on('data', function (data) { json += data; }); child.on('error', function (err) { callback(err); }); child.on('close', function (code) { callback(null, JSON.parse(json)); }); } /** * get titanium information */ function getTiInfo(appc, callback) { var child = spawn(process.execPath, [ getTiBinary(), 'info', '-o', 'json' ]), json = ''; child.stdout.on('data', function (data) { json += data; }); child.on('error', function (err) { callback(err); }); child.on('close', function () { callback(null, JSON.parse(json)); }); } /** * check to make sure we have titanium sdk installed */ function checkForTi(appc, callback) { getTiSdk(appc, function (err, result) { if (err) { return callback(err); } callback(null, result.activeSDK); }); } /** * gets the titanium (default) installation path */ function getTiDefaultPath(appc, callback) { getTiSdk(appc, function (err, result) { if (err) { return callback(err); } var path = null; result.installLocations.forEach(function (location) { if (fs.existsSync(location)) { path = location; } }); appc.log.debug('getTiDefaultPath', path); callback(null, path); }); } /** * get the titanium (default) installation info * @param {Function} callback(err, result) callback with installed ti information */ function getInstalledTiSdkInfo(appc, callback) { exports.getTiSdk(appc, function (err, result) { if (err) { return callback(err); } var path; result.installLocations.forEach(function (location) { if (fs.existsSync(location)) { path = location; } }); if (!path && result.defaultInstallLocation) { path = result.defaultInstallLocation; } var sdkResult = { sdkPath: path, activeSDK: result.activeSDK }; appc.log.trace('getInstalledTiSdkInfo', sdkResult); callback(null, sdkResult); }); } /** * install the latest titanium sdk */ function installTi(appc, callback) { appc.spinner.stop(); // don't run while we're doing install since it has a progress bar appc.log.trace([ process.execPath ].concat([ getTiBinary(), 'sdk', 'install', '--default', '--no-prompt', '--no-banner' ])); var child = spawn(process.execPath, [ getTiBinary(), 'sdk', 'install', '--default', '--no-prompt', '--no-banner' ], { stdio: 'inherit' }); child.on('close', callback); child.on('error', callback); } /** * get the titanium config object */ function getTiConfig(opts, callback) { var config = require(path.join(require.resolve('titanium'), '..', 'config.js')); // eslint-disable-line security/detect-non-literal-require config.load(opts['config-file']); return callback(null, config); } /** * kill all the simulator processes */ function killSimulator() { try { if (process.platform === 'darwin') { exec('ps -ef | grep Simulator | grep -v grep | awk -F " " \'{print $2}\' | sort -nr | xargs kill'); } } catch (E) { } } /** * launch titanium but first check to make sure we have an SDK installed */ function launchTi(appc, args, opts, callback, config) { config = _.defaults(config || {}, { skipDownloadCheck: false }); args = _.isString(args) ? [ args ] : args; if (config.skipDownloadCheck) { return proceed(); } checkForTi(appc, function (err, sdk) { if (err) { return callback(err); } if (!sdk) { appc.log.info('No Mobile SDK found, downloading ... '); return installTi(appc, function (err) { if (err) { return callback(err); } return launchTi(appc, args, opts, callback); }); } proceed(); }); function proceed() { var bin = getTiBinary(), cmd, newargs, ti; if (process.platform === 'win32') { cmd = 'cmd'; newargs = [ '/s', '/c', 'node', bin ].concat(cleanArgs(args)); } else { cmd = process.execPath; newargs = [ bin ].concat(cleanArgs(args)); } ti = spawn(cmd, newargs, opts); [ 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGABRT', 'exit' ].forEach(function (name) { process.on(name, function (code) { appc.log.trace('Sending exit signal to titanium process: ', ti.pid); ti.kill('SIGKILL'); if (name === 'exit') { process.exit(); } }); }); callback(null, ti); } } /** * detect if we're online */ function isOnline(url, callback) { var host = urllib.parse(url).hostname, isIP = /^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/.test(host); if (isIP && /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/.test(host)) { // if a local IP address, assume its true since we're probably in dev or test return callback(true); } // do a quick DNS resolve to validate if we're online or not require('dns').lookup(host, function (err, ip) { var online = (!err && ip && ip.length) || (err && err.code !== 'ENOTFOUND'); return callback(!!online); }); } /** * send a build verification request to generate a security signing key * and validate that the developer can perform the build */ function buildVerify(logger, config, tiapp, modules, deploytype, callback) { var opts = config.opts, ti = tiappxml.parse(tiapp), safePropName = /^(ti|appc)(\.|-)/, props = ti.doc.documentElement.getElementsByTagName('property'); for (var c = 0; c < props.length; c++) { var prop = props[c].getAttribute('name'); // remove all properties that don't match our safePropName from a // security standpoint. we don't want to inadvertantly transmit // properties likes keys or user-configured stuff that is meant to // be secure/private. this will only allow ti/appc config if (!safePropName.test(prop)) { ti.setProperty(prop, '***'); // mask it } } // convert back to a string tiapp = ti.toString(); // the body var json = { username: opts.session.username, session: opts.session.session, ipaddress: opts.session.ipaddress, fingerprint: opts.session.fingerprint, fingerprint_description: opts.session.fingerprint_description, guid: ti.guid, appid: ti.id, name: ti.name, org_id: opts.session.org_id, deploytype: deploytype, tiappxml: tiapp, modules: (modules || []).map(function (m) { return _.omit(m, 'bindings'); }) }; // the request var data = { json: json, gzip: true, followAllRedirects: true, url: urllib.resolve(opts.server, '/api/v1/auth/build-verify') }; if (process.env.APPC_CONFIG_PAC_FILE) { data.agent = new PacProxyAgent('pac+' + process.env.APPC_CONFIG_PAC_FILE); } else if (process.env.APPC_CONFIG_PROXY) { data.proxy = process.env.APPC_CONFIG_PROXY; } if (process.env.APPC_CONFIG_CAFILE) { data.ca = fs.readFileSync(process.env.APPC_CONFIG_CAFILE, 'utf8'); } if (process.env.APPC_CONFIG_STRICTSSL === 'false') { data.strictSSL = false; } // load appc so we can use request bundled with it var appc = require(config.filename); // eslint-disable-line security/detect-non-literal-require appc.log.level && appc.log.level(logger.level); // create our build cache file var sha = sha1(JSON.stringify(appc.lodash.pick(json, 'username', 'session', 'fingerprint', 'guid', 'appid', 'org_id', 'deploytype'))); var buildTmp = path.join(tmpdir, 'build-' + sha + '.json'); log(logger.trace, 'offline build file %s', buildTmp); isOnline(data.url, function (online) { log(logger.trace, 'online %d', online); // always default to needing to build var needsBuild = true; if (fs.existsSync(buildTmp) && deploytype === 'development') { var stat = fs.statSync(buildTmp); var lastBuild = (Date.now() - stat.mtime.getTime()); // only re-verify every day as long as we have a valid file needsBuild = lastBuild > (1000 * 60 * 60 * 24); // force rebuild for offline, so check online status here if (!needsBuild && online) { let obj = JSON.parse(fs.readFileSync(buildTmp)); obj.skipped = true; log(logger.trace, 'optimizing build, last build was %d sec ago', lastBuild / 1000); return callback(null, obj); } else { log(logger.trace, 'last build was %d sec ago, need a re-verify and re-build', lastBuild / 1000); } } // offline, check and see if we can still continue if (!online) { if (deploytype === 'production') { return callback(new Error('You are offline and trying to do a production build. You must be online to continue.')); } if (fs.existsSync(buildTmp)) { let obj = JSON.parse(fs.readFileSync(buildTmp)); let age = Date.now() - obj.timestamp; obj.offline = true; obj.skipped = true; log(logger.trace, 'offline file %j, age=%d', obj, age); if (age >= 259200000) { let days = Math.round(age / 259200000), s = days > 1 ? 's' : ''; log(logger.trace, 'offline in days %d', days); return callback(new Error('You are offline and haven\'t built this app for ' + days + ' day' + s + '. You must be online to build this application again.')); } return callback(null, obj); } return callback(new Error('You are offline but this is the first time you are building this application with this configuration. You must be online once with this build configuration and then you can do subsequent builds offline.')); } log(logger.trace, 'sending request %j', data); // Workaround to make sure appc.registry.handleResponse can be called #CLI-462 if (appc && appc.registry && typeof appc.registry.handleResponse !== 'function') { return callback(new Error('could not load appc.registry.handleResponse')); } appc.request.post(data, function (err, resp, body) { appc.registry.handleResponse(err, resp, body, function (err, result) { if (err && result === 'com.appcelerator.platform.app.notregistered') { // this is OK, just means we haven't registered the app with platform return callback(); } log(logger.trace, 'result from /build-verify=> %j, err=%j', result, err); if (result && result.success) { result.url = urllib.resolve(opts.server, '/api/v1/app/verify'); result.username = json.username; result.timestamp = Date.now(); fs.writeFileSync(buildTmp, JSON.stringify(result)); result.skipped = false; callback(null, result); } else { callback(new Error(result && result.error || err && err.message || 'Error fetching build verification')); } }); }); }); } /** * send a build update notification to record the sha of the build */ function buildUpdate(logger, config, json, result, callback) { var opts = config.opts, // load appc so we can use request bundled with it appc = require(config.filename), // eslint-disable-line security/detect-non-literal-require data = { json: appc.lodash.merge({ session: opts.session.session }, json), followAllRedirects: true, gzip: true, url: urllib.resolve(opts.server, '/api/v1/auth/build-update') }; log(logger.trace, 'sending build-update %j', data); // Workaround to make sure appc.registry.handleResponse can be called #CLI-462 if (appc && appc.registry && typeof appc.registry.handleResponse !== 'function') { return callback(new Error('could not load appc.registry.handleResponse')); } if (process.env.APPC_CONFIG_PROXY) { data.proxy = process.env.APPC_CONFIG_PROXY; } appc.request.post(data, function (err, resp, body) { appc.registry.handleResponse(err, resp, body, function (err, result) { log(logger.trace, 'result from /build-update=> %j, err=%j', result, err); if (result && result.success) { callback(null, result); } else { callback(new Error(result && result.error || err && err.message || 'Error updating build sha settings')); } }); }); } function sha1(value) { var crypto = require('crypto'); var sha = crypto.createHash('sha1'); sha.update(value); return sha.digest('hex'); } function Decycler() { var cache = []; return function (key, value) { if (key === '__cached_trace__') { return; } if (typeof value === 'object' && value !== null) { if (cache.indexOf(value) !== -1) { // Circular reference found, discard key return; } // Store value in our collection cache.push(value); } return value; }; } function log(logger) { var args = Array.prototype.slice.call(arguments, 1); var oldStringify = JSON.stringify; JSON.stringify = function (obj) { return oldStringify(obj, Decycler()); }; var msg = format.apply(null, args); JSON.stringify = oldStringify; logger(msg); } // need to make sure our instructions are appropriate for new CLI function patchTiCommand(str) { return str.replace(/(\s|"|'|^)(ti|titanium)(\s+\w+)/g, '$1appc ti$3'); } /** * Checks if Titanium module exist (with platform and module version) * @param {object} tiModule titanium module * @param {string} tiModule.id titanium module id * @param {object} tiModule.platforms titanium module platforms * @param {string} tiDefaultPath current titanium sdk path * @param {Function} cb(err, results) callback with installed module information */ function checkForTiModule(appc, tiModule, tiDefaultPath, cb) { if (typeof tiModule !== 'object') { return cb(new Error('Invalid type of tiModule')); } var moduleRoot = path.join(tiDefaultPath, 'modules'), response = {}; // 1) iterate through platforms async.each(tiModule.platforms, function (platform, next) { var moduleForPlatformPath = path.join(moduleRoot, platform, tiModule.id, tiModule.version); // 2) Check if module exists if (fs.existsSync(moduleForPlatformPath)) { appc.log.debug('Module Found', tiModule.id, platform, tiModule.version); response[platform] = tiModule.version; } else { response[platform] = null; } next(); }, function () { cb(null, response); }); } /** * Checks if Titanium plugin exist (with platform and module version) * @param {object} tiPlugin titanium module * @param {string} tiPlugin.id titanium module id * @param {object} tiPlugin.platforms titanium module platforms * @param {string} tiDefaultPath current titanium sdk path * @param {Function} cb(err, results) callback with installed module information */ function checkForTiPlugin(appc, tiPlugin, tiDefaultPath, cb) { if (typeof tiPlugin !== 'object') { return cb(new Error('Invalid type of tiModule')); } var pluginRoot = path.join(tiDefaultPath, 'plugins'), response = {}; if (tiPlugin.partner_name !== 'soasta') { return cb(new Error('Plugin \'' + tiPlugin.partner_name + '\' not supported yet')); } var pluginPath = path.join(pluginRoot, 'com.appcelerator.test'); // 1) check if plugin exists if (!fs.existsSync(pluginPath)) { return cb(null, response); } // 2) check versions var versionPath = path.join(pluginPath, 'versions'); if (!fs.existsSync(versionPath)) { return cb(null, response); } if (tiPlugin.version) { var compatibleVersionPath = path.join(pluginPath, 'versions', tiPlugin.version); if (fs.existsSync(compatibleVersionPath)) { response[tiPlugin.partner_name] = tiPlugin.version; return cb(null, response); } } var latestVersion = getLatestInstalledModVersion(versionPath); appc.log.debug('Plugin found', tiPlugin.id, latestVersion); response[tiPlugin.partner_name] = latestVersion; cb(null, response); } /** * installs module in the defined sdk path * @param {object} tiModuleObject titanium module object * @param {string} tiDefaultPath current titanium sdk path * @param {Function} cb(err, result) callback regarding installation */ function installTiModule(appc, tiModuleObject, tiDefaultPath, cb) { var moduleDownloadUrl = url.parse(tiModuleObject.url); return downloadAndExtract(appc, { url: moduleDownloadUrl, extractPath: tiDefaultPath, name: tiModuleObject.id }, cb); } /** * installs plugin in the defined sdk path * @param {object} tiPluginObject titanium plugin object * @param {string} tiDefaultPath current titanium sdk path * @param {Function} cb(err, result) callback regarding installation */ function installTiPlugin(appc, tiPluginObject, tiDefaultPath, cb) { var pluginDownloadUrl = url.parse(tiPluginObject.url), pluginsDir, pluginPath, versionPath, newVersionPath; if (tiPluginObject.partner_name !== 'soasta') { return cb(new Error('Plugin \'' + tiPluginObject.partner_name + '\' not supported yet')); } pluginsDir = path.join(tiDefaultPath, 'plugins'); pluginPath = path.join(pluginsDir, 'com.appcelerator.test'); versionPath = path.join(pluginPath, 'versions'); newVersionPath = path.join(versionPath, tiPluginObject.version); // 1) check if pluginsDir exists (if not create one) if (!fs.existsSync(pluginsDir)) { fs.mkdirSync(pluginsDir); } // 2) check if pluginPath exists (if not create one) if (!fs.existsSync(pluginPath)) { // create path and plugin.py fs.mkdirSync(pluginPath); fs.mkdirSync(versionPath); fs.openSync(path.join(pluginPath, 'plugin.py'), 'w'); } if (!fs.existsSync(versionPath)) { fs.mkdirSync(versionPath); } if (!fs.existsSync(newVersionPath)) { fs.mkdirSync(newVersionPath); } return downloadAndExtract(appc, { url: pluginDownloadUrl, extractPath: newVersionPath, name: tiPluginObject.partner_name }, cb); } /** * Download from url and extract to directory * @param {object} opts Options * @param {string} opts.url Download Url * @param {string} opts.extractPath Extract Path * @param {string} opts.name Name * @param {Function} cb(err) Callback */ function downloadAndExtract(appc, opts, cb) { // create a temp file var tmpPath = path.join(tmpdir, Date.now().toString()); function cleanUp() { fs.unlink(tmpPath, function () {}); } appc.request(opts) .on('error', function (err) { cleanUp(); cb(err); }) .pipe(fs.createWriteStream(tmpPath)) .on('close', function () { var zip = new AdmZip(tmpPath); appc.log.debug('Extracting contents from ', opts.url); zip.extractAllTo(opts.extractPath, true); var iphonedir = path.join(opts.extractPath, 'modules', 'iphone'); if (IS_WIN && fs.existsSync(iphonedir)) { fs.removeSync(iphonedir); } var msg = 'Finished Downloading & Installing ' + (opts.name === 'soasta' ? 'AppC Test' : opts.name); if (appc && appc.opts && appc.opts.output === 'json') { appc.log.info(JSON.stringify({ message: msg })); } else { appc.log.info(msg); } cleanUp(); cb(); }); } /** * Lists available module downloads */ function listTiDownloads(appc, cb) { platformSdk.Auth.createSessionFromID(appc.commands.config.get('sid'), function (err, session) { if (err && Object.keys(err).length !== 0) { appc.log.debug('createSessionFromID', err, session); return cb(err); } platformSdk.createRequest(session, '/api/v1/downloads', 'get', function (err, result) { if (err) { appc.log.debug('/api/v1/downloads', JSON.stringify(err)); return cb(new Error(err)); } appc.log.debug('to download', JSON.stringify(result)); cb(null, result); }); }); } /** * Sync Titanium modules */ function syncTiDownloads(appc, cb) { async.waterfall([ // prevent checking each time function (next) { var lastUpdateCheck = appc.commands.config.get('lastUpdateCheckTiDownloads'), currentTimeStampMS = Date.now(), checkTimeMS = 1000 * 60 * 30; // 30 minutes in ms // make sure lastUpdateCheck is an integer lastUpdateCheck = lastUpdateCheck ? parseInt(lastUpdateCheck) : 0; appc.log.debug('Titanium Downloads Last Checked:', lastUpdateCheck ? lastUpdateCheck : 'Never'); // Will syncTiDownloads if lastUpdateCheck was more than 24 hours ago if (!lastUpdateCheck || lastUpdateCheck && (lastUpdateCheck + checkTimeMS) < currentTimeStampMS) { appc.commands.config.set('lastUpdateCheckTiDownloads', currentTimeStampMS); return next(); } return cb(); }, // get Titanium install path function (next) { exports.getInstalledTiSdkInfo(appc, function (err, tiInfo) { if (err) { return cb(err); } appc.log.debug('active sdk info', tiInfo); return next(null, tiInfo); }); }, // Lists available module downloads function (tiInfo, next) { exports.listTiDownloads(appc, function (err, forDownload) { if (err) { return cb(err); } next(forDownload, tiInfo); }); } // process ], function (forDownload, tiInfo) { if (!tiInfo) { appc.log.debug('no active sdk info.'); return cb(); } var tiDefaultPath = tiInfo.sdkPath; var activeSDK = tiInfo.activeSDK; // handle components (plugin) function handleComponents(cb) { async.each(forDownload.components, function (componentForDownload, done) { var compatibleComponent = getCompatibleModuleDownloadInfo(activeSDK, componentForDownload); if (!compatibleComponent) { return done(); } componentForDownload.version = compatibleComponent.version; componentForDownload.url = compatibleComponent.url; checkForTiPlugin(appc, componentForDownload, tiDefaultPath, function (err, results) { appc.log.debug(componentForDownload.partner_name, 'Latest Version', componentForDownload.version); if (!Object.prototype.hasOwnProperty.call(results, componentForDownload.partner_name) || results[componentForDownload.partner_name] !== componentForDownload.version) { return installTiPlugin(appc, componentForDownload, tiDefaultPath, done); } // already up to date return done(); }); }, cb); } // handle modules function handleModules(cb) { async.each(forDownload.modules, function (moduleForDownload, done) { var compatibleMod = getCompatibleModuleDownloadInfo(activeSDK, moduleForDownload); if (!compatibleMod) { return done(); } moduleForDownload.version = compatibleMod.version; moduleForDownload.url = compatibleMod.url; checkForTiModule(appc, moduleForDownload, tiDefaultPath, function (err, results) { var platformsToUpdate = []; moduleForDownload.platforms.forEach(function (platform) { var p = platform.toLowerCase(); if (IS_WIN && (p === 'iphone' || p === 'ios')) { return; } appc.log.debug(moduleForDownload.id, platform, 'Latest Version', moduleForDownload.version); if (!Object.prototype.hasOwnProperty.call(results, platform) || results[platform] !== moduleForDownload.version) { appc.log.debug('Adding to queue', platform, (!Object.prototype.hasOwnProperty.call(results, platform) ? 'No Version Found' : results[platform])); // add to que platformsToUpdate.push(platform); } // already up to date }); if (platformsToUpdate.length > 0) { return installTiModule(appc, moduleForDownload, tiDefaultPath, done); } return done(); }); }, cb); } // run tasks async.parallel([ handleComponents, handleModules ], function (err) { if (err) { // reset lastUpdateCheckTiDownloads appc.commands.config.set('lastUpdateCheckTiDownloads', 0); } return cb(err, forDownload); }); }); } /** * Compare SDK versions (Less Than) * @param {string} version1 SDK version * @param {string} version2 SDK version * @return {bool} */ function ltSdkVersion(version1, version2) { var RC = /^RC\d*$/i, V = /^v\d+$/i, BETA = /^Beta\d*$/i; /** * Calculates a value for a SDK version * @param {string} version SDK version * @return {int} */ function calculateWeight(version) { // Split the versions in to parts version = (version || '0.0.0').split('.'); var weight = 0; // Itterate through the parts for (var i = 0, max = version.length; i < max; ++i) { var ofset = Math.pow(100, 3 - i), num = version[i]; // check if the num is a number if (!isNaN(num)) { weight += ofset * parseInt(num); // check if num contains v } else if (V.test(num)) { num = num.replace('v', ''); weight += parseFloat('0.00' + num); // check if num contains Beta } else if (BETA.test(num)) { num = num.replace('Beta', ''); weight += parseFloat('0.01' + num); // check if num contains RC } else if (RC.test(num)) { num = num.replace('RC', ''); weight += parseFloat('0.1' + num); // Check if num is equal to GA } else if (num === 'GA') { weight += 1; } } // return weight return weight; } return calculateWeight(version1) < calculateWeight(version2); } /** * Get the list of normalized platform names. * @param {object} opts Options. * @param {array} opts.platforms Supported platforms. * @return {array} */ function getNormalizedPlatforms(opts) { var platforms = (opts.platforms || [ 'android' ]).toString().split(',') .map(function (platform) { if (platform === 'ios' || platform === 'ipad' || platform === 'iphone') { return 'iphone'; } else if (platform !== 'android') { return null; } return platform; }) .filter(function (platform) { return platform !== null; }); if (!IS_WIN && platforms.indexOf('iphone') === -1) { platforms.push('iphone'); } return platforms; } /** * Get the installed versions of the specified SDK module. * @param {string} modPath the module path. * @return {string} */ function getInstalledModVersions(modPath) { if (!fs.existsSync(modPath)) { return; } var versions = _.filter(fs.readdirSync(modPath), function (version) { return /^[0-9]*(\.[0-9]*)+$/.test(version); }); return nodeAppc.version.sort(versions); } /** * Get the latest installed version of the specified SDK module. * @param {string} modPath the module path. * @return {string} */ function getLatestInstalledModVersion(modPath) { if (!fs.existsSync(modPath)) { return; } return getInstalledModVersions(modPath).pop(); } /** * Check the SDK and SDK module is compatible. * @param {string} sdkv the SDK version. * @param {string} modv the module version. * @return {boolean} */ function isSdkModCompatible(sdkv, modv) { var sdk = parseInt((sdkv || '0.0.0').split('.')[0], 10); var mod = parseInt((modv || '0.0.0').split('.')[0], 10); return (sdk < 6 && mod < 2) || (sdk >= 6 && mod >= 2); } /** * Check the SDK and test plugin is compatible. * @param {string} sdkv the SDK version. * @param {string} pluginv the test plugin version. * @return {boolean} */ function isSdkTestPluginCompatible(sdkv, pluginv) { // prior to sdk 6, test plugin has the version format of a.b // sdk 6 compatible test plugin has the version format of a.b.c var sdk = parseInt((sdkv || '0.0.0').split('.')[0], 10); var plugin = (pluginv || '0.0.0').split('.').length; return (sdk < 6 && plugin === 2) || (sdk >= 6 && plugin === 3); } /** * Get the latest installed module version that is compatible with the SDK. * @param {string} sdkv the SDK version. * @param {string} modPath the module path. * @return {string} */ function getCompatibleInstalledModVersion(sdkv, modPath) { var isTestPlugin = modPath && modPath.indexOf('com.appcelerator.test') !== -1; var versions = getInstalledModVersions(modPath).filter(function (v) { return isTestPlugin ? isSdkTestPluginCompatible(sdkv, v) : isSdkModCompatible(sdkv, v); }); return nodeAppc.version.sort(versions).pop(); } /** * Get the module download url that is compatible with current active SDK. * @param {object} sdkv the SDK version. * @param {object} modData the module download data. * @return {object} */ function getCompatibleModuleDownloadInfo(sdkv, modData) { if (!modData || !modData.versions) { return; } return modData.versions.filter(function (mod) { if (mod.sdk_versions) { return nodeAppc.version.satisfies(sdkv, mod.sdk_versions); } return false; }).pop(); } /** * Check if the module/plugin installed is the latest and compatible with current active SDK. * @param {string} addonPath the modules/plugins directory. * @param {string} addonName the module/plugin name. * @param {string} platform the module/plugin plaform. * @param {string} activeSDK the selected titanium sdk version. * @param {string} latestDownloadVersion the latest module/plugin version to download. * @return {string} version. */ function detectAddonVersion(appc, addonPath, addonName, platform, activeSDK, latestDownloadVersion) { appc.log.trace('Checking ', addonPath); var installedVersions = exports.getInstalledModVersions(addonPath); if (!installedVersions || installedVersions.length === 0) { appc.log.debug(addonName + ' is not available ' + (platform || '')); return; } var version; if (!latestDownloadVersion) { // we don't know what is the latest compatible version, let's try our best guess version = exports.getCompatibleInstalledModVersion(activeSDK, addonPath); appc.log.debug('looks like ' + addonName + ' is' + (version ? ' ' : ' not ') + 'installed ' + (platform || '') + ' ' + (version || '')); } else if (installedVersions.indexOf(latestDownloadVersion) !== -1) { // the latest version is already installed version = latestDownloadVersion; appc.log.debug(addonName + ' installed is the latest ' + (platform || '') + ' ' + version); } else { appc.log.debug(addonName + ' is not installed ' + (platform || '')); } return version; } /** * Escape string before using in shell command. * @param {string} str the string to escape. * @return {string} */ function shellEscape(str) { if (/[^A-Za-z0-9_/:=-]/.test(str)) { str = '"' + str.replace(/(["\s'$`\\])/g, '\\$1') + '"'; } return str; } exports.getTiConfig = getTiConfig; exports.cleanArgs = cleanArgs; exports.getTiBinary = getTiBinary; exports.getTiDefaultPath = getTiDefaultPath; exports.checkForTi = checkForTi; exports.launchTi = launchTi; exports.killSimulator = killSimulator; exports.buildVerify = buildVerify; exports.buildUpdate = buildUpdate; exports.getLatestTiSDK = getLatestTiSDK; exports.installTi = installTi; exports.patchTiCommand = patchTiCommand; exports.checkForTiModule = checkForTiModule; exports.checkForTiPlugin = checkForTiPlugin; exports.listTiDownloads = listTiDownloads; exports.syncTiDownloads = syncTiDownloads; exports.ltSdkVersion = ltSdkVersion; exports.installTiModule = installTiModule; exports.installTiPlugin = installTiPlugin; exports.getLatestInstalledModVersion = getLatestInstalledModVersion; exports.getNormalizedPlatforms = getNormalizedPlatforms; exports.getInstalledModVersions = getInstalledModVersions; exports.isSdkModCompatible = isSdkModCompatible; exports.isSdkTestPluginCompatible = isSdkTestPluginCompatible; exports.getCompatibleInstalledModVersion = getCompatibleInstalledModVersion; exports.getCompatibleModuleDownloadInfo = getCompatibleModuleDownloadInfo; exports.getInstalledTiSdkInfo = getInstalledTiSdkInfo; exports.getTiSdk = getTiSdk; exports.getTiInfo = getTiInfo; exports.shellEscape = shellEscape; exports.detectAddonVersion = detectAddonVersion; exports.IS_WIN = IS_WIN; exports.version = nodeAppc.version;