appc-cli-titanium
Version:
Titanium CLI Plugin
1,076 lines (963 loc) • 33.1 kB
JavaScript
/**
* 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;