UNPKG

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