@titanium/appcelerator
Version:
⭐ Axway Amplify command-line (CLI) tool for installing Appcelerator Titanium SDK
549 lines (489 loc) • 17.7 kB
JavaScript
// jscs:disable jsDoc
/**
* 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.
*/
/**
* download and install the appcelerator package
*/
var fs = require('fs'),
path = require('path'),
download = require('./download'),
util = require('./util'),
errorlib = require('./error'),
tar = require('tar'),
chalk = require('chalk'),
debug = require('debug')('appc:install'),
{ exec, spawn } = require('child_process'), // eslint-disable-line security/detect-child-process
semver = require('semver'),
checkPlatform = require('npm-install-checks').checkPlatform;
/**
* tar gunzip
* @param {string} sourceFile - The source tar.gz file to extract
* @param {string} destination - The destination to extract to
* @param {function} callback - Function to call one complete
*/
function targz(sourceFile, destination, callback) {
debug('targz source=%s, dest=%s', sourceFile, destination);
tar.x({
file: sourceFile,
cwd: destination
}, function (err) {
callback(err);
});
}
/**
* run the pre-flight check to check env for specific things we need
* @param {object} opts - Various options
* @param {function} callback - Function to call when complete
* @returns {undefined}
*/
function preflight(opts, callback) {
var isWindows = util.isWindows();
debug('preflight checks, is this windows? %d', isWindows);
// don't allow running this as root (defeats the purpose of writing to the user-writable directory)
if (!isWindows && (process.env.USER === 'root' || process.getuid() === 0)) {
if (process.env.SUDO_USER) {
debug('sudo user detected %s', process.env.SUDO_USER);
return callback(errorlib.createError('com.appcelerator.install.installer.sudo', process.env.SUDO_USER));
}
debug('root user detected');
return callback(errorlib.createError('com.appcelerator.install.installer.user.root'));
} else if (!isWindows && (process.env.USERNAME === 'root' && process.env.SUDO_USER)) {
// don't allow running as sudo from another user account.
debug('root user detected %s', process.env.SUDO_USER);
return callback(errorlib.createError('com.appcelerator.install.installer.user.sudo.user', process.env.SUDO_USER));
}
// check and make sure we actually have a home directory
var homedir = util.getHomeDir();
debug('home directory located at %s', homedir);
if (!fs.existsSync(homedir)) {
var envname = process.env.HOME ? 'HOME' : 'USERPROFILE';
debug('cannot find the home directory');
return callback(errorlib.createError('com.appcelerator.install.installer.missing.homedir', homedir, chalk.yellow('$' + envname)));
}
// make sure the user home directory its writable
var error = util.checkDirectory(homedir, 'home');
if (error) {
debug('home directory isn\'t writable');
return callback(error);
}
// make sure the install directory its writable
var installDir = util.getInstallDir();
error = util.checkDirectory(installDir, 'install');
if (error) {
debug('install directory isn\'t writable %s', installDir);
return callback(error);
}
// check parent directory to make sure owned by the user
error = util.checkDirectory(path.dirname(installDir), 'appcelerator');
if (error) {
debug('install directory isn\'t writable %s', path.dirname(installDir));
return callback(error);
}
switch (process.platform) {
case 'darwin':
// must have Xcode tools to compile so let's check that
return exec('xcode-select -p', function (err, _stdout) {
var exitCode = err && err.code;
if (exitCode === 2) {
debug('xcode-select says CLI tools not installed');
// this means we don't have Xcode CLI tools, prompt to install it
// you do this by trying to invoke gcc which will automatically install
exec('gcc', function (_err, _stdout) {
return callback(errorlib.createError('com.appcelerator.install.preflight.missing.xcode.clitools'));
});
} else {
callback();
}
});
}
callback();
}
/**
* tar gunzip our package into dir
* @param {boolean} quiet - Whether to output logs during extraction
* @param {string} filename - Path to file to extract
* @param {string} dir - Location to extract to
* @param {function} callback - Function to call when complete
* @param {number} attempts - Number of attempts to make to extract
* @returns {undefined}
*/
function extract(quiet, filename, dir, callback, attempts) {
debug('calling extract on %s, dir=%s', filename, dir);
attempts = attempts || 0;
if (!quiet) {
util.waitMessage('Installing ');
}
util.ensureDir(dir);
var error = util.checkDirectory(dir, 'install');
if (error) {
debug('extract error %s', error);
return callback(new Error(error));
}
targz(filename, dir, function (_err) {
// let errors fail through and attempt to do it again. we seem to have
// failures ocassionally on extraction
var pkg = path.join(dir, 'package', 'package.json');
if (fs.existsSync(pkg)) {
util.okMessage();
return callback(null, filename, dir);
} else {
debug('after extraction, package.json not found at %s', pkg);
if (attempts < 3) {
// reset the line since it will be in the Installing... spinner state
util.resetLine();
// delete the directory since stale directories cause issues
util.rmdirSyncRecursive(dir);
// console.log('extraction failed, attempting again',attempts+1);
extract(quiet, filename, dir, callback, attempts + 1);
} else {
debug('extract failed after %d attempts', attempts);
callback(errorlib.createError('com.appcelerator.install.installer.extraction.failed'));
}
}
});
}
/**
* find all native compiled modules. the publish command detected any npm modules that had a native
* compiled library and marked it by creating an empty file .nativecompiled during tar.gz. we are going
* to find all those specific modules and then re-install using npm so that they can be properly compiled
* on the target platform during install.
* @param {string} dir - Directory to search
* @param {string[]} check - Array of directories that have been checked already
* @returns {string[]}
*/
function findAllNativeModules(dir, check) {
var dirs = [];
fs.readdirSync(dir).forEach(function (name) {
if (name === '.nativecompiled' && dirs.indexOf(dir) === -1 && (!check || check.indexOf(dir) < 0)) {
dirs.push(dir);
}
var fn = path.join(dir, name);
if (fs.existsSync(fn)) {
try {
var isDir = fs.statSync(fn).isDirectory();
if (isDir) {
dirs = dirs.concat(findAllNativeModules(fn, dirs));
}
} catch (e) {
// ignore this. just means we're trying to follow a
// bad symlink
debug('findAllNativeModules encountered a likely symlink issue at %s, error was %o', fn, e);
}
}
});
return dirs;
}
/**
* run npm install on all compiled native modules so that they will be
* correctly compiled for the installed platform (vs. the platform we used to upload)
* @param {string} dir - Directory to run npm install in
* @param {string} cliVersion - Version of the CLI we're installing in
* @param {function} callback - Function to call when done
*/
function compileNativeModules(dir, cliVersion, callback) {
debug('compileNativeModules %s', dir);
process.nextTick(function () {
// Strip off any prerelease suffixes on our cliVersion
const cleanVersion = semver.coerce(cliVersion);
// If 7.1.0 CLI or higher then we can use npm rebuild
if (semver.gte(cleanVersion, '7.1.0')) {
util.waitMessage('Compiling platform native modules');
var cmd = 'npm rebuild';
var rebuildDir = path.join(dir, 'package');
let child;
let stderr = '';
let stdout = '';
const rebuildOpts = { cwd: rebuildDir, env: { ...process.env, APPCD_SKIP_POSTINSTALL: 1 } };
debug('spawn: %s in dir %s', cmd, rebuildDir);
if (/^win/.test(process.platform)) {
child = spawn(process.env.comspec, [ '/c', 'npm' ].concat([ 'rebuild' ]), rebuildOpts);
} else {
child = spawn('npm', [ 'rebuild' ], rebuildOpts);
}
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.on('exit', (code) => {
if (code) {
util.infoMessage('Failed to rebuild native modules. Please contact Appcelerator Support at support@appcelerator.com.');
debug('error during %s', cmd);
debug('stdout: %s', stdout);
debug('stderr: %s', stderr);
} else {
util.okMessage();
}
callback();
});
} else {
// For pre-7.1.0 CLIs we need to still use the full install due to missing deps
var dirs = findAllNativeModules(dir),
finished = 0;
if (dirs.length) {
util.waitMessage('Compiling platform native modules\n');
// run them serially so we don't run into npm lock issues
function doNext() {
var dir = dirs[finished++];
if (dir) {
var name = path.basename(dir),
todir = path.dirname(dir),
todirname = path.basename(path.dirname(todir)),
installdir = path.join(dir, '..', '..'),
shouldInstall = true,
version;
/* jshint -W083 */
if (fs.existsSync(dir)) {
var pkg = path.join(dir, 'package.json');
if (fs.existsSync(pkg)) {
// make sure we install the exact version
var pkgcontents = JSON.parse(fs.readFileSync(pkg));
version = pkgcontents.version;
debug('found version %s', version);
version = '@' + version;
try {
checkPlatform(pkgcontents);
} catch (err) {
debug('module %s is not supported on the current os %s, not installing it', name, process.platform);
shouldInstall = false;
}
}
debug('rmdir %s', dir);
util.rmdirSyncRecursive(dir);
}
if (shouldInstall) {
var cmd = 'npm install ' + name + version + ' --production';
debug('exec: %s in dir %s', cmd, installdir);
util.waitMessage('└ ' + chalk.cyan(todirname + '/' + name) + ' ');
exec(cmd, { cwd: installdir }, function (err, stdout, stderr) {
if (err) {
util.infoMessage('Failed to install ' + name + version + '; it may not support your current OS.');
debug('error during %s, was: %o', cmd, err);
debug('stdout: %s', stdout);
debug('stderr: %s', stderr);
doNext();
} else {
util.okMessage();
doNext();
}
});
} else {
doNext();
}
} else {
callback();
}
}
doNext();
} else {
debug('none found');
callback();
}
}
});
}
/**
* start the install process
* @param {object} opts - configuration options
* @param {function} callback - Function to call when done
*/
function start(opts, callback) {
// do our pre-flight checks
preflight(opts, function (err) {
// if we have pre-flight check failure, handle special
if (err) {
console.error(chalk.red('\n' + (err && err.message || String(err))));
process.exit(1);
}
if (!opts.setup && !opts.quiet && (opts.banner === undefined || opts.banner)) {
util.infoMessage(chalk.blue.underline.bold('Before you can continue, the latest Appcelerator software update needs to be downloaded.'));
console.log();
}
callback(null, true);
});
}
/**
* run setup
* @param {string} installBin - Path to the installation binary
* @param {object} opts - Configuration options
* @param {function} cb - Function to call when doe
*/
function runSetup(installBin, opts, _cb) {
var run = require('./index').run,
found = util.parseArgs(opts);
debug('runSetup called, found is %o', found);
// if we didn't pass in anything or we explicitly called setup
// then run it
if (found.length === 0 || (found[0] === 'setup')) {
var saved = process.argv.splice(2);
process.argv[2] = 'setup';
process.argv.length = 3;
debug('calling run with %s', installBin);
run(installBin, util.mergeOptsToArgs([ '--no-banner' ], opts));
} else {
// otherwise, we've called a different command and we should just run
// it instead and skip the setup
run(installBin, util.mergeOptsToArgs([ '--no-banner' ], opts));
}
}
/**
* run the install
* @param {string} installDir - Path to the installation directory
* @param {object} opts - Configuration options
* @param {function} cb - Function to call when doe
*/
function install(installDir, opts, cb) {
start(opts, function (_err, result) {
if (!result) {
util.stopSpinner();
if (!opts.quiet) {
console.log('Cancelled!');
}
process.exit(1);
}
// determine our registry url
var wantVersion = opts.version || '',
url = util.makeURL(opts, '/api/appc/install/' + wantVersion),
bin = wantVersion && util.getInstallBinary(opts, wantVersion);
debug('install, wantVersion: %s, url: %s, bin: %s', wantVersion, url, bin);
// if already installed the version we're looking for, then we just need to continue
if (bin && !opts.force) {
debug('bin is setup and not force');
if (!opts.quiet) {
util.infoMessage('Version ' + chalk.green(wantVersion) + ' already installed.');
}
return cb && cb(null, installDir, wantVersion, bin);
}
// check if setup command help is requested and bail out before the
// download if we already have a version installed
if (opts.setup && (opts.h || opts.help)) {
var installBin = util.getInstallBinary();
if (installBin) {
return runSetup(installBin, opts, cb);
}
}
// download the package
download.start(opts.quiet, opts.banner, !!opts.force, url, wantVersion, function (err, filename, version, installBin) {
if (err) {
util.fail(err);
}
// we mark it as failed in case it gets interuppted before finishing
var failed = true;
// use this since below we are going to overwrite which might be null
var installationDir = path.join(installDir, version);
var sigIntFn, exitFn, pendingAbort;
debug('after download, installationDir %s', installationDir);
function createCleanup(name) {
return function (exit) {
if (failed) {
var pkg = path.join(installationDir, 'package', 'package.json');
if (fs.existsSync(pkg)) {
fs.unlinkSync(pkg);
}
}
// if exit, go ahead and exit with exitcode
if (name === 'exit') {
try {
process.removeListener('exit', exitFn);
} catch (e) {
// this is OK
}
if (pendingAbort) {
process.exit(exit);
}
} else if (failed) {
// if failed and a SIGINT, force an exit
pendingAbort = true;
util.abortMessage('Install');
}
try {
process.removeListener('SIGINT', sigIntFn);
} catch (e) {
// this is OK
}
};
}
// we need to hook and listen for an interruption and remove our package
// in case the install is interrupted, we don't want a package that is partly installed
process.on('SIGINT', (sigIntFn = createCleanup('SIGINT')));
process.on('exit', (exitFn = createCleanup('exit')));
util.stopSpinner();
// ensure that we have our installation path
installDir = util.ensureDir(path.join(installDir, version));
// we already have it installed, just return
if (installBin) {
debug('installBin already found, returning %s', installBin);
failed = false;
createCleanup()();
if (!opts.quiet) {
util.infoMessage('Version ' + chalk.green(version) + ' already installed.');
}
if (opts.setup) {
util.writeVersion(version);
return runSetup(installBin, opts, cb);
}
return cb && cb(null, installDir, version, installBin);
}
// add an install flag to indicate we're doing an install
var installTag = util.getInstallTag();
fs.writeFileSync(installTag, version);
function cleanupInstall() {
if (fs.existsSync(installTag)) {
fs.unlinkSync(installTag);
}
}
// extract it
extract(opts.quiet, filename, installDir, function (err, filename, dir) {
if (err) {
cleanupInstall();
util.fail(err);
}
// compile any native modules found
compileNativeModules(dir, version, function (err) {
if (err) {
cleanupInstall();
util.fail(err);
}
// point at the right version that we just downloaded
installBin = util.getInstallBinary(opts, version);
debug('after compileNativeModules, installBin is %s', installBin);
// mark it as completed so we know we completed OK
failed = false;
// make the new version active
if (opts.setup || opts.use) {
util.writeVersion(version);
}
// remove up install tag file
cleanupInstall();
// if this is a setup, then run the setup after the install
if (opts.setup) {
debug('after compileNativeModules, setup is set');
return runSetup(installBin, opts, cb);
}
// write current process versions to package dir
util.writeVersions(installDir);
util.restartDaemon(version, installBin, opts.quiet);
util.installPlugins(version, installBin, opts.quiet);
if (!opts.quiet) {
util.infoMessage(chalk.green.bold('Installed!!'));
}
// if this is a use we don't run, we just return
if (opts.use) {
return cb && cb(null, installDir, version, installBin);
}
debug('running %s', installBin);
// run it
require('./index').run(installBin, [ '--no-banner' ]);
});
});
});
});
}
module.exports = install;