ionic
Version:
A tool for creating and developing Ionic Framework mobile apps.
623 lines (492 loc) • 16.4 kB
JavaScript
var fs = require('fs'),
request = require('request'),
path = require('path'),
parseUrl = require('url').parse,
argv = require('optimist').argv,
prompt = require('prompt'),
shelljs = require('shelljs/global'),
FormData = require('form-data'),
IonicProject = require('./project'),
IonicStore = require('./store').IonicStore,
Task = require('./task').Task,
IonicUploadTask = require('./upload').IonicTask,
IonicStats = require('./stats').IonicStats,
IonicLoginTask = require('./login').IonicTask,
IonicUtils = require('./utils');
var IonicTask = function() {};
IonicTask.prototype = new Task();
IonicTask.prototype.run = function(ionic) {
var self = this;
self.ionic = ionic;
self.project = null;
try {
self.project = IonicProject.load();
} catch (ex) {
self.ionic.fail(ex.message);
return
}
self.inputFiles = {};
self.inputValues = {};
self.useCmdArgs = false;
IonicStats.t();
if(argv['clear-signing'] || argv.l) {
self.clearSigning();
return;
}
self.loadProject();
this.getCmdLineOptions();
if(self.ionic.hasFailed) return;
var login = new IonicLoginTask();
login.get(self.ionic, function(jar) {
self.jar = jar;
self.loadAppSigning(function(){
self.initPlatforms();
});
});
};
IonicTask.prototype.loadProject = function() {
var appName = this.project.get('name') || "app";
console.log( ('Loading ' + appName + '...').bold.green );
if(argv._.length < 3) {
return this.ionic.fail('No platforms or build mode specified.', 'package');
}
this.mode = argv._[2].toLowerCase();
if(this.mode != 'debug' && this.mode != 'release') {
// ionic package debug android
this.mode = argv._[1].toLowerCase();
this.platforms = argv._.slice(2);
} else {
// ionic package android debug
this.platforms = [ argv._[1].toLowerCase() ];
}
if(this.mode != 'debug' && this.mode != 'release') {
return this.ionic.fail('Package build mode must be "debug" or "release".', 'package');
}
if(this.platforms.length < 1) {
return this.ionic.fail('No platforms specified.', 'package');
}
this.buildStatusEmail = (!argv['no-email'] && !argv.n);
this.loadPlugins();
};
IonicTask.prototype.loadPlugins = function() {
this.plugins = [];
try {
var pluginsPath = resolvePath('./plugins/');
var pluginDirs = fs.readdirSync(pluginsPath);
for(var x=0; x<pluginDirs.length; x++) {
try {
var plugingPackage = resolvePath(pluginsPath + '/' + pluginDirs[x] + '/package.json');
var packageObj = JSON.parse( fs.readFileSync(plugingPackage, { encoding: 'utf8' }) );
if(packageObj.repo || packageObj.name) {
this.plugins.push(packageObj);
}
} catch(e) {}
}
} catch(pluginError) {
return this.ionic.fail('Unable to find plugin directory. Make sure the working directory is an Ionic project.');
}
};
IonicTask.prototype.getCmdLineOptions = function() {
var self = this;
function getCmdArgValue(propertyName, shortName) {
var value = argv[propertyName] || argv[shortName];
if(value) {
self.inputValues[propertyName] = value;
self.useCmdArgs = true;
}
}
function getCmdArgFile(propertyName, shortName) {
var value = argv[propertyName] || argv[shortName];
if(value) {
if(!fileExists(value)) {
return self.ionic.fail("Unable to find file: " + argv[propertyName]);
}
self.inputFiles[propertyName] = value;
self.useCmdArgs = true;
}
}
getCmdArgValue('android-keystore-alias', 'a');
getCmdArgValue('android-keystore-password', 'w');
getCmdArgValue('android-key-password', 'r');
getCmdArgValue('ios-certificate-password', 'd');
getCmdArgFile('android-keystore-file', 'k');
getCmdArgFile('ios-certificate-file', 'c');
getCmdArgFile('ios-profile-file', 'f');
};
IonicTask.prototype.loadAppSigning = function(callback) {
var self = this;
if(self.useCmdArgs) {
// if they used cmd line args, don't bother checking
callback();
return;
}
if(!self.project.get('app_id')) {
// if its the first load we won't have an appId yet
callback();
return;
}
var privateData = new IonicStore(self.project.get('app_id'));
var cck = privateData.get('cck');
if(!cck) {
// if there's no client key don't bother
callback();
return;
}
var options = {
url: self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + self.project.get('app_id') + '/signing',
headers: {
cookie: IonicUtils.transformCookies(self.jar),
cck: cck
},
proxy: process.env.PROXY || process.env.http_proxy || null
};
request(options, function(err, response, body) {
if(err) {
return self.ionic.fail("Error loading app signing info: " + err);
}
if(response.statusCode == 200) {
try {
self.signing = JSON.parse(body);
if(!self.signing) {
return self.ionic.fail("Invalid app signing information");
}
} catch(e) {
return self.ionic.fail("Error parsing app signing: " + e);
}
}
callback();
});
};
IonicTask.prototype.initPlatforms = function() {
var self = this;
if(self.useCmdArgs) {
self.packagePlatforms();
} else {
prompt.override = argv;
prompt.message = '';
prompt.delimiter = '';
prompt.start();
var promptProperties = self.buildPromptProperties();
prompt.get({properties: promptProperties}, function (err, promptResult) {
if(err) {
return self.ionic.fail('Error packaging: ' + err);
}
for(var propertyName in promptResult) {
var promptValue = promptResult[propertyName];
if( !promptValue ) continue;
if(promptProperties[propertyName].isFile) {
self.inputFiles[propertyName] = promptValue;
} else {
self.inputValues[propertyName] = promptValue;
}
}
self.packagePlatforms();
});
}
};
IonicTask.prototype.packagePlatforms = function() {
var self = this;
var upload = new IonicUploadTask();
upload.run(self.ionic, argv, function() {
for(var x=0; x<self.platforms.length; x++) {
var form = self.buildPostRequest(self.platforms[x]);
self.submitPostRequest(form, self.platforms[x]);
}
});
};
IonicTask.prototype.buildPromptProperties = function() {
// Just prompt for some build properties
var promptProperties = {};
for(var x=0; x<this.platforms.length; x++) {
if(this.platforms[x] == 'android') {
// Android debug doesn't require anything
if(this.mode == 'release') {
promptProperties['android-keystore-file']= {
name: 'android-keystore-file',
description: 'Android Keystore File (.keystore):'.yellow.bold,
required: true,
conform: fileExists,
isFile: true
};
promptProperties['android-keystore-alias'] = {
name: 'android-keystore-alias',
description: 'Keystore Alias:'.yellow.bold.yellow.bold,
required: true
};
promptProperties['android-keystore-password'] = {
name: 'android-keystore-password',
description: 'Keystore Password:'.yellow.bold,
hidden: true,
required: true
};
promptProperties['android-key-password'] = {
name: 'android-key-password',
description: 'Key Password (optional):'.yellow.bold,
hidden: true
};
}
} else if(this.platforms[x] == 'ios') {
// iOS
promptProperties['ios-certificate-file'] = {
name: 'ios-certificate-file',
description: 'iOS Certificate File (.p12):'.yellow.bold,
required: true,
conform: fileExists,
isFile: true
};
promptProperties['ios-certificate-password'] = {
name: 'ios-certificate-password',
description: 'Certificate Password:'.yellow.bold,
hidden: true,
required: true
};
promptProperties['ios-profile-file'] = {
name: 'ios-profile-file',
description: 'iOS Mobile Provisioning Profile (.mobileprovision):'.yellow.bold,
required: true,
conform: fileExists,
isFile: true
};
}
}
// Don't prompt for properties we already have stored on the server
if(this.signing){
for (var propertyName in promptProperties) {
if(this.signing['is_valid_' + propertyName.replace(/-/g, '_')]) {
delete promptProperties[propertyName];
}
}
}
return promptProperties;
};
IonicTask.prototype.buildPostRequest = function(platform) {
var csrfmiddlewaretoken = null;
csrfmiddlewaretoken = IonicUtils.retrieveCsrfToken(this.jar)
var form = new FormData();
form.append('build_status_email', this.buildStatusEmail.toString());
form.append('name', this.project.get('name') || '');
form.append('platform', platform);
form.append('build_mode', this.mode);
form.append('csrfmiddlewaretoken', csrfmiddlewaretoken);
for (var i = 0; i < this.plugins.length; i++) {
form.append('plugin_' + i, this.plugins[i].repo ? this.plugins[i].repo : this.plugins[i].name);
if(this.plugins[i].name) {
form.append('plugin_' + i + '_name', this.plugins[i].name);
}
if(this.plugins[i].version) {
form.append('plugin_' + i + '_version', this.plugins[i].version);
}
}
for(var propertyName in this.inputValues) {
form.append(propertyName.replace(/-/g, '_'), this.inputValues[propertyName]);
}
for(propertyName in this.inputFiles) {
var inputFile = this.inputFiles[propertyName];
if( !inputFile ) continue;
try {
inputFile = inputFile.replace(/\\ /g, ' ').trim();
form.append(propertyName.replace(/-/g, '_'), fs.createReadStream( resolvePath(inputFile) ) );
} catch(e) {
return this.ionic.fail("Error loading " + resolvePath(inputFile));
}
}
var configFilePath = resolvePath('./config.xml');
if(fs.existsSync(configFilePath)){
var xmlString = fs.readFileSync(configFilePath, { encoding: 'utf8' });
form.append('config_file', xmlString);
}
return form;
};
IonicTask.prototype.submitPostRequest = function(form, platform) {
var self = this;
var params = parseUrl(self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + self.project.get('app_id') + '/package');
var privateData = new IonicStore(this.project.get('app_id'));
var headersToTransform = {cookie: IonicUtils.transformCookies(self.jar)};
console.log((platform + ' ' + self.mode + ' building...').bold);
if(privateData.get('cck')) {
headersToTransform['cck'] = privateData.get('cck');
}
form.submit({
protocol: params.protocol,
hostname: params.hostname,
port: params.port,
path: params.path,
headers: form.getHeaders(headersToTransform)
},
function(err, response) {
if(err) {
return self.ionic.fail("Error packaging " + platform + ": " + err);
}
response.setEncoding('utf8');
response.on("data", function(data) {
try {
var d = JSON.parse(data);
if(d.errors && d.errors.length) {
for (var j = 0; j < d.errors.length; j++) {
console.log( d.errors[j].bold.error );
}
console.log('');
} else if(d.build_status_url) {
process.stderr.write('.');
setTimeout(function(){
var requestOptions = {
url: d.build_status_url,
headers: {
cookie: IonicUtils.transformCookies(self.jar)
}
};
pingBuildStatus(self.ionic, requestOptions, platform, 1);
}, 5000);
}
if(d.cck) {
var privateData = new IonicStore(self.project.get('app_id'));
privateData.set('cck', d.cck);
privateData.save();
}
if(d.app_id) {
self.project.set('app_id', d.app_id);
}
if(d.name) {
self.project.set('name', d.name);
}
self.project.save();
} catch(e) {
return self.ionic.fail('Error submitPostRequest: ' + e);
}
});
});
};
IonicTask.prototype.clearSigning = function() {
var self = this;
console.log('Clearing app signing and credential information...'.yellow.bold);
var appId = self.project.get('app_id');
if(!appId) {
return self.ionic.fail('App Id is not known');
}
var login = new IonicLoginTask();
login.get(self.ionic, function(jar) {
var options = {
url: self.ionic.IONIC_DASH + self.ionic.IONIC_API + 'app/' + appId + '/signing/clear',
headers: {
cookie: IonicUtils.transformCookies(self.jar)
},
proxy: process.env.PROXY || null
};
request(options, function(err, response, body) {
if(err) {
return self.ionic.fail("Error clearing app signing: " + err);
}
console.log( ('App (' + appId + ') signing and credential information cleared\n').green.bold );
});
});
};
function pingBuildStatus(ionic, requestOptions, platform, attempt) {
process.stderr.clearLine();
process.stderr.cursorTo(0);
for(var x=0; x<attempt; x++) {
process.stderr.write('.');
}
request(requestOptions, function(err, response, body) {
if(err) {
return ionic.fail("\nError pinging build status: " + err);
}
try {
var d = JSON.parse(body);
if(d.errors && d.errors.length) {
for(var x=0; x<d.errors.length; x++) {
console.log( ('\n' + d.errors[x]).bold.red );
}
return;
}
if(d.message) {
console.log( ('\n' + d.message).bold.red );
}
// STATUS_INITIALIZED = 0
// STATUS_QUEUED = 1
// STATUS_BUILDING = 2
// STATUS_SUCCESS = 3
// STATUS_FAILED = 4
if(d.status >= 0 && d.status < 3) {
// still queued/building
attempt++;
if(attempt > 60) {
return ionic.fail("\nUnable to receive build status");
}
setTimeout(function(){
pingBuildStatus(ionic, requestOptions, platform, attempt);
}, 5000);
} else if(d.status == 4) {
console.log("Use 'ionic package --clear-signing' to clear app signing and credential data if needed.".bold.red);
return ionic.fail("Build failed");
} else if(d.status == 3) {
downloadBuildPackage(platform, d);
} else {
return ionic.fail("\nError receiving build status");
}
} catch(e) {
return ionic.fail("\nError pinging build status: " + e);
}
});
}
function downloadBuildPackage(platform, data) {
console.log( ('\n\n' + platform + ' build complete, downloading package...').bold );
var filePath = argv.output;
if(!filePath) {
var dirPath = resolvePath('./packages');
filePath = resolvePath(dirPath + '/' + data.package_filename);
if(!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
}
var file = fs.createWriteStream(filePath);
var proxy = process.env.PROXY || null;
request({ url: data.package_url, proxy: proxy }, function(err, res, body) {
if(res.statusCode !== 200) {
console.error(err);
return;
}
try {
file.write(body);
file.close(function(){
console.log( ('Saved ' + platform + ' package: ' + filePath + '\n').bold.green );
});
} catch(e) {
q.reject(e);
}
}).on('response', function(res){
var ProgressBar = require('progress');
var bar = new ProgressBar('[:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
width: 30,
total: parseInt(res.headers['content-length'], 10)
});
res.on('data', function (chunk) {
try {
bar.tick(chunk.length);
} catch(e){}
});
res.on('end', function () {
console.log('\n');
});
}).on('error', function(err) {
try {
fs.unlink(filePath);
} catch(e) {
console.error(e);
}
console.error(err);
});
}
function fileExists(filePath) {
// check if a file exists with a relative path or absolute path
filePath = filePath.replace(/\\ /g, ' ').trim();
return fs.existsSync(resolvePath(filePath));
}
function resolvePath (p) {
if (p.substr(0, 1) === '~') {
p = process.env.HOME + p.substr(1);
}
return path.resolve(p);
}
exports.IonicTask = IonicTask;