UNPKG

17.5 kBJavaScriptView Raw
1// jscs:disable jsDoc
2/**
3 * This code is closed source and Confidential and Proprietary to
4 * Appcelerator, Inc. All Rights Reserved. This code MUST not be
5 * modified, copied or otherwise redistributed without express
6 * written permission of Appcelerator. This file is licensed as
7 * part of the Appcelerator Platform and governed under the terms
8 * of the Appcelerator license agreement.
9 */
10/**
11 * download and install the appcelerator package
12 */
13var fs = require('fs'),
14 path = require('path'),
15 download = require('./download'),
16 util = require('./util'),
17 errorlib = require('./error'),
18 tar = require('tar'),
19 chalk = require('chalk'),
20 debug = require('debug')('appc:install'),
21 { exec, spawn } = require('child_process'), // eslint-disable-line security/detect-child-process
22 semver = require('semver'),
23 checkPlatform = require('npm-install-checks').checkPlatform;
24
25/**
26 * tar gunzip
27 * @param {string} sourceFile - The source tar.gz file to extract
28 * @param {string} destination - The destination to extract to
29 * @param {function} callback - Function to call one complete
30 */
31function targz(sourceFile, destination, callback) {
32 debug('targz source=%s, dest=%s', sourceFile, destination);
33 tar.x({
34 file: sourceFile,
35 cwd: destination
36 }, function (err) {
37 callback(err);
38 });
39}
40
41/**
42 * run the pre-flight check to check env for specific things we need
43 * @param {object} opts - Various options
44 * @param {function} callback - Function to call when complete
45 * @returns {undefined}
46 */
47function preflight(opts, callback) {
48
49 var isWindows = util.isWindows();
50 debug('preflight checks, is this windows? %d', isWindows);
51
52 // don't allow running this as root (defeats the purpose of writing to the user-writable directory)
53 if (!isWindows && (process.env.USER === 'root' || process.getuid() === 0)) {
54 if (process.env.SUDO_USER) {
55 debug('sudo user detected %s', process.env.SUDO_USER);
56 return callback(errorlib.createError('com.appcelerator.install.installer.sudo', process.env.SUDO_USER));
57 }
58 debug('root user detected');
59 return callback(errorlib.createError('com.appcelerator.install.installer.user.root'));
60 } else if (!isWindows && (process.env.USERNAME === 'root' && process.env.SUDO_USER)) {
61 // don't allow running as sudo from another user account.
62 debug('root user detected %s', process.env.SUDO_USER);
63 return callback(errorlib.createError('com.appcelerator.install.installer.user.sudo.user', process.env.SUDO_USER));
64 }
65
66 // check and make sure we actually have a home directory
67 var homedir = util.getHomeDir();
68 debug('home directory located at %s', homedir);
69 if (!fs.existsSync(homedir)) {
70 var envname = process.env.HOME ? 'HOME' : 'USERPROFILE';
71 debug('cannot find the home directory');
72 return callback(errorlib.createError('com.appcelerator.install.installer.missing.homedir', homedir, chalk.yellow('$' + envname)));
73 }
74
75 // make sure the user home directory its writable
76 var error = util.checkDirectory(homedir, 'home');
77 if (error) {
78 debug('home directory isn\'t writable');
79 return callback(error);
80 }
81
82 // make sure the install directory its writable
83 var installDir = util.getInstallDir();
84 error = util.checkDirectory(installDir, 'install');
85 if (error) {
86 debug('install directory isn\'t writable %s', installDir);
87 return callback(error);
88 }
89
90 // check parent directory to make sure owned by the user
91 error = util.checkDirectory(path.dirname(installDir), 'appcelerator');
92 if (error) {
93 debug('install directory isn\'t writable %s', path.dirname(installDir));
94 return callback(error);
95 }
96
97 switch (process.platform) {
98 case 'darwin':
99 // must have Xcode tools to compile so let's check that
100 return exec('xcode-select -p', function (err, _stdout) {
101 var exitCode = err && err.code;
102 if (exitCode === 2) {
103 debug('xcode-select says CLI tools not installed');
104 // this means we don't have Xcode CLI tools, prompt to install it
105 // you do this by trying to invoke gcc which will automatically install
106 exec('gcc', function (_err, _stdout) {
107 return callback(errorlib.createError('com.appcelerator.install.preflight.missing.xcode.clitools'));
108 });
109 } else {
110 callback();
111 }
112 });
113 }
114
115 callback();
116}
117
118/**
119 * tar gunzip our package into dir
120 * @param {boolean} quiet - Whether to output logs during extraction
121 * @param {string} filename - Path to file to extract
122 * @param {string} dir - Location to extract to
123 * @param {function} callback - Function to call when complete
124 * @param {number} attempts - Number of attempts to make to extract
125 * @returns {undefined}
126 */
127function extract(quiet, filename, dir, callback, attempts) {
128 debug('calling extract on %s, dir=%s', filename, dir);
129 attempts = attempts || 0;
130 if (!quiet) {
131 util.waitMessage('Installing ');
132 }
133 util.ensureDir(dir);
134 var error = util.checkDirectory(dir, 'install');
135 if (error) {
136 debug('extract error %s', error);
137 return callback(new Error(error));
138 }
139 targz(filename, dir, function (_err) {
140 // let errors fail through and attempt to do it again. we seem to have
141 // failures ocassionally on extraction
142 var pkg = path.join(dir, 'package', 'package.json');
143 if (fs.existsSync(pkg)) {
144 util.okMessage();
145 return callback(null, filename, dir);
146 } else {
147 debug('after extraction, package.json not found at %s', pkg);
148 if (attempts < 3) {
149 // reset the line since it will be in the Installing... spinner state
150 util.resetLine();
151 // delete the directory since stale directories cause issues
152 util.rmdirSyncRecursive(dir);
153 // console.log('extraction failed, attempting again',attempts+1);
154 extract(quiet, filename, dir, callback, attempts + 1);
155 } else {
156 debug('extract failed after %d attempts', attempts);
157 callback(errorlib.createError('com.appcelerator.install.installer.extraction.failed'));
158 }
159 }
160 });
161}
162
163/**
164 * find all native compiled modules. the publish command detected any npm modules that had a native
165 * compiled library and marked it by creating an empty file .nativecompiled during tar.gz. we are going
166 * to find all those specific modules and then re-install using npm so that they can be properly compiled
167 * on the target platform during install.
168 * @param {string} dir - Directory to search
169 * @param {string[]} check - Array of directories that have been checked already
170 * @returns {string[]}
171 */
172function findAllNativeModules(dir, check) {
173 var dirs = [];
174 fs.readdirSync(dir).forEach(function (name) {
175 if (name === '.nativecompiled' && dirs.indexOf(dir) === -1 && (!check || check.indexOf(dir) < 0)) {
176 dirs.push(dir);
177 }
178 var fn = path.join(dir, name);
179 if (fs.existsSync(fn)) {
180 try {
181 var isDir = fs.statSync(fn).isDirectory();
182 if (isDir) {
183 dirs = dirs.concat(findAllNativeModules(fn, dirs));
184 }
185 } catch (e) {
186 // ignore this. just means we're trying to follow a
187 // bad symlink
188 debug('findAllNativeModules encountered a likely symlink issue at %s, error was %o', fn, e);
189 }
190 }
191 });
192 return dirs;
193}
194
195/**
196 * run npm install on all compiled native modules so that they will be
197 * correctly compiled for the installed platform (vs. the platform we used to upload)
198 * @param {string} dir - Directory to run npm install in
199 * @param {string} cliVersion - Version of the CLI we're installing in
200 * @param {function} callback - Function to call when done
201 */
202function compileNativeModules(dir, cliVersion, callback) {
203 debug('compileNativeModules %s', dir);
204 process.nextTick(function () {
205 // Strip off any prerelease suffixes on our cliVersion
206 const cleanVersion = semver.coerce(cliVersion);
207 // If 7.1.0 CLI or higher then we can use npm rebuild
208 if (semver.gte(cleanVersion, '7.1.0')) {
209 util.waitMessage('Compiling platform native modules');
210 var cmd = 'npm rebuild';
211 var rebuildDir = path.join(dir, 'package');
212 let child;
213 let stderr = '';
214 let stdout = '';
215 const rebuildOpts = { cwd: rebuildDir };
216 debug('spawn: %s in dir %s', cmd, rebuildDir);
217
218 if (/^win/.test(process.platform)) {
219 child = spawn(process.env.comspec, [ '/c', 'npm' ].concat([ 'rebuild' ]), rebuildOpts);
220 } else {
221 child = spawn('npm', [ 'rebuild' ], rebuildOpts);
222 }
223
224 child.stderr.on('data', (chunk) => {
225 stderr += chunk.toString();
226 });
227
228 child.stdout.on('data', (chunk) => {
229 stdout += chunk.toString();
230 });
231
232 child.on('exit', (code) => {
233 if (code) {
234 util.infoMessage('Failed to rebuild native modules. Please contact Appcelerator Support at support@appcelerator.com.');
235 debug('error during %s', cmd);
236 debug('stdout: %s', stdout);
237 debug('stderr: %s', stderr);
238 } else {
239 util.okMessage();
240 }
241 callback();
242 });
243 } else {
244 // For pre-7.1.0 CLIs we need to still use the full install due to missing deps
245 var dirs = findAllNativeModules(dir),
246 finished = 0;
247 if (dirs.length) {
248 util.waitMessage('Compiling platform native modules\n');
249 // run them serially so we don't run into npm lock issues
250 function doNext() {
251 var dir = dirs[finished++];
252 if (dir) {
253 var name = path.basename(dir),
254 todir = path.dirname(dir),
255 todirname = path.basename(path.dirname(todir)),
256 installdir = path.join(dir, '..', '..'),
257 shouldInstall = true,
258 version;
259 /* jshint -W083 */
260 if (fs.existsSync(dir)) {
261 var pkg = path.join(dir, 'package.json');
262 if (fs.existsSync(pkg)) {
263 // make sure we install the exact version
264 var pkgcontents = JSON.parse(fs.readFileSync(pkg));
265 version = pkgcontents.version;
266 debug('found version %s', version);
267 version = '@' + version;
268 checkPlatform(pkgcontents, false, function (err) {
269 if (err) {
270 debug('module %s is not supported on the current os %s, not installing it', name, process.platform);
271 shouldInstall = false;
272 }
273 });
274 }
275 debug('rmdir %s', dir);
276 util.rmdirSyncRecursive(dir);
277 }
278 if (shouldInstall) {
279 var cmd = 'npm install ' + name + version + ' --production';
280 debug('exec: %s in dir %s', cmd, installdir);
281 util.waitMessage('└ ' + chalk.cyan(todirname + '/' + name) + ' ');
282 exec(cmd, { cwd: installdir }, function (err, stdout, stderr) {
283 if (err) {
284 util.infoMessage('Failed to install ' + name + version + '; it may not support your current OS.');
285 debug('error during %s, was: %o', cmd, err);
286 debug('stdout: %s', stdout);
287 debug('stderr: %s', stderr);
288 doNext();
289 } else {
290 util.okMessage();
291 doNext();
292 }
293 });
294 } else {
295 doNext();
296 }
297 } else {
298 callback();
299 }
300 }
301 doNext();
302 } else {
303 debug('none found');
304 callback();
305 }
306 }
307 });
308}
309
310/**
311 * start the install process
312 * @param {object} opts - configuration options
313 * @param {function} callback - Function to call when done
314 */
315function start(opts, callback) {
316
317 // do our pre-flight checks
318 preflight(opts, function (err) {
319
320 // if we have pre-flight check failure, handle special
321 if (err) {
322 console.error(chalk.red('\n' + (err && err.message || String(err))));
323 process.exit(1);
324 }
325
326 if (!opts.setup && !opts.quiet && (opts.banner === undefined || opts.banner)) {
327 util.infoMessage(chalk.blue.underline.bold('Before you can continue, the latest Appcelerator software update needs to be downloaded.'));
328 console.log();
329 }
330
331 callback(null, true);
332 });
333}
334
335/**
336 * run setup
337 * @param {string} installBin - Path to the installation binary
338 * @param {object} opts - Configuration options
339 * @param {function} cb - Function to call when doe
340 */
341function runSetup(installBin, opts, _cb) {
342 var run = require('./index').run,
343 found = util.parseArgs(opts);
344
345 debug('runSetup called, found is %o', found);
346
347 // if we didn't pass in anything or we explicitly called setup
348 // then run it
349 if (found.length === 0 || (found[0] === 'setup')) {
350 var saved = process.argv.splice(2);
351 process.argv[2] = 'setup';
352 process.argv.length = 3;
353 debug('calling run with %s', installBin);
354 run(installBin, util.mergeOptsToArgs([ '--no-banner' ], opts));
355 } else {
356 // otherwise, we've called a different command and we should just run
357 // it instead and skip the setup
358 run(installBin, util.mergeOptsToArgs([ '--no-banner' ], opts));
359 }
360}
361
362/**
363 * run the install
364 * @param {string} installDir - Path to the installation directory
365 * @param {object} opts - Configuration options
366 * @param {function} cb - Function to call when doe
367 */
368function install(installDir, opts, cb) {
369
370 start(opts, function (_err, result) {
371 if (!result) {
372 util.stopSpinner();
373 if (!opts.quiet) {
374 console.log('Cancelled!');
375 }
376 process.exit(1);
377 }
378
379 // determine our registry url
380 var wantVersion = opts.version || '',
381 url = util.makeURL(opts, '/api/appc/install/' + wantVersion),
382 bin = wantVersion && util.getInstallBinary(opts, wantVersion);
383
384 debug('install, wantVersion: %s, url: %s, bin: %s', wantVersion, url, bin);
385
386 // if already installed the version we're looking for, then we just need to continue
387 if (bin && !opts.force) {
388 debug('bin is setup and not force');
389 if (!opts.quiet) {
390 util.infoMessage('Version ' + chalk.green(wantVersion) + ' already installed.');
391 }
392 return cb && cb(null, installDir, wantVersion, bin);
393 }
394
395 // check if setup command help is requested and bail out before the
396 // download if we already have a version installed
397 if (opts.setup && (opts.h || opts.help)) {
398 var installBin = util.getInstallBinary();
399 if (installBin) {
400 return runSetup(installBin, opts, cb);
401 }
402 }
403
404 // download the package
405 download.start(opts.quiet, opts.banner, !!opts.force, url, wantVersion, function (err, filename, version, installBin) {
406 if (err) {
407 util.fail(err);
408 }
409
410 // we mark it as failed in case it gets interuppted before finishing
411 var failed = true;
412
413 // use this since below we are going to overwrite which might be null
414 var installationDir = path.join(installDir, version);
415
416 var sigIntFn, exitFn, pendingAbort;
417
418 debug('after download, installationDir %s', installationDir);
419
420 function createCleanup(name) {
421 return function (exit) {
422 if (failed) {
423 var pkg = path.join(installationDir, 'package', 'package.json');
424 if (fs.existsSync(pkg)) {
425 fs.unlinkSync(pkg);
426 }
427 }
428 // if exit, go ahead and exit with exitcode
429 if (name === 'exit') {
430 try {
431 process.removeListener('exit', exitFn);
432 } catch (e) {
433 // this is OK
434 }
435 if (pendingAbort) {
436 process.exit(exit);
437 }
438 } else if (failed) {
439 // if failed and a SIGINT, force an exit
440 pendingAbort = true;
441 util.abortMessage('Install');
442 }
443 try {
444 process.removeListener('SIGINT', sigIntFn);
445 } catch (e) {
446 // this is OK
447 }
448 };
449 }
450
451 // we need to hook and listen for an interruption and remove our package
452 // in case the install is interrupted, we don't want a package that is partly installed
453 process.on('SIGINT', (sigIntFn = createCleanup('SIGINT')));
454 process.on('exit', (exitFn = createCleanup('exit')));
455
456 util.stopSpinner();
457
458 // ensure that we have our installation path
459 installDir = util.ensureDir(path.join(installDir, version));
460
461 // we already have it installed, just return
462 if (installBin) {
463 debug('installBin already found, returning %s', installBin);
464 failed = false;
465 createCleanup()();
466 if (!opts.quiet) {
467 util.infoMessage('Version ' + chalk.green(version) + ' already installed.');
468 }
469 if (opts.setup) {
470 util.writeVersion(version);
471 return runSetup(installBin, opts, cb);
472 }
473 return cb && cb(null, installDir, version, installBin);
474 }
475
476 // add an install flag to indicate we're doing an install
477 var installTag = util.getInstallTag();
478 fs.writeFileSync(installTag, version);
479
480 function cleanupInstall() {
481 if (fs.existsSync(installTag)) {
482 fs.unlinkSync(installTag);
483 }
484 }
485
486 // extract it
487 extract(opts.quiet, filename, installDir, function (err, filename, dir) {
488 if (err) {
489 cleanupInstall();
490 util.fail(err);
491 }
492
493 // compile any native modules found
494 compileNativeModules(dir, version, function (err) {
495
496 if (err) {
497 cleanupInstall();
498 util.fail(err);
499 }
500
501 // point at the right version that we just downloaded
502 installBin = util.getInstallBinary(opts, version);
503
504 debug('after compileNativeModules, installBin is %s', installBin);
505
506 // mark it as completed so we know we completed OK
507 failed = false;
508
509 // make the new version active
510 if (opts.setup || opts.use) {
511 util.writeVersion(version);
512 }
513
514 // remove up install tag file
515 cleanupInstall();
516
517 // if this is a setup, then run the setup after the install
518 if (opts.setup) {
519 debug('after compileNativeModules, setup is set');
520 return runSetup(installBin, opts, cb);
521 }
522
523 if (!opts.quiet) {
524 util.infoMessage(chalk.green.bold('Installed!!'));
525 }
526
527 // write current process versions to package dir
528 util.writeVersions(installDir);
529
530 // if this is a use we don't run, we just return
531 if (opts.use) {
532 util.killDaemon(version, installBin);
533 return cb && cb(null, installDir, version, installBin);
534 }
535
536 debug('running %s', installBin);
537
538 // run it
539 require('./index').run(installBin, [ '--no-banner' ]);
540 });
541 });
542 });
543 });
544}
545
546module.exports = install;