index.js | |
---|---|
tsmTitanium SDK manager This file contains functions for retreiving builds & build metadata from
Appcelerator. Tests are in | var request = require("request");
var async = require("async");
var semver = require("semver");
var fs = require("fs");
var path = require("path");
var util = require("util");
var EventEmitter = require("events").EventEmitter;
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var rimraf = require('rimraf');
var tsm = {};
module.exports = tsm; |
Public API | |
install
Installs a matching SDK to the provided directory. Returns an emitter which
will emit | tsm.install = function (options, done) {
var emitter = options.emitter || new EventEmitter();
tsm.getAllBuilds(options.input, options.os, function (error, builds) {
if (error) return done(error);
if (builds.length === 0) return done(new Error("no matching SDK versions"));
var build = builds.pop();
var total = build.size;
var left = total;
var dest = path.join(options.dir, build.filename);
emitter.emit('chose', build);
var req = request(build.zip);
req.pipe(fs.createWriteStream(dest));
req.on('error', done);
req.on('data', function (buffer) {
left -= buffer.length;
emitter.emit('progress', {
left: left,
done: total - left,
percent: (total-left) / total
});
});
req.on('end', function () {
var dir = path.resolve(options.dir, "..", "..");
emitter.emit('debug', "complete");
emitter.emit('downloaded', {dest: dest, dir: dir});
tsm.unzip(dest, dir, function (er) {
if (er) return done(er);
emitter.emit('extracted', {dest: dest});
fs.unlink(options.dir + "/" + build.filename, function (er) {
if (er) return done(er);
done(null);
emitter.emit('done');
});
});
});
}, emitter);
return emitter;
}; |
remove
Removes sdks matching | tsm.remove = function (options, done) {
var emitter = options.emitter || new EventEmitter();
tsm.findInstalled(options.dir, options.input, function (error, builds) {
if (error) return done(error);
if (builds.length === 0) return done(new Error('no matched builds'));
var dirs = builds.map(function (item) { return item.dir; });
async.forEach(dirs, function (item, callback) {
emitter.emit('deleting', item);
rimraf(item, callback);
}, done);
}, emitter);
return emitter;
}; |
list
The complete solution for listing builds. Returns an EventEmitter which will emit 'log' and 'debug' events. | tsm.list = function (options, done) {
if (typeof options.os !== 'string')
throw new TypeError("options.os is required");
if (options.installed && typeof options.dir != 'string')
throw new TypeError("options.dir is required for options.installed");
var emitter = options.emitter || new EventEmitter();
async.parallel({
installed: function (callback) {
if (!options.installed) callback(null, []);
else {
emitter.emit('debug', "Finding installed SDKs.");
tsm.findInstalled(options.dir, options.input, callback, emitter);
}
},
available: function (callback) {
if (!options.available) callback(null, []);
else {
emitter.emit('debug', "Pulling available SDKs.");
emitter.emit('available', options);
tsm.getAllBuilds(options.input, options.os, callback, emitter);
}
}
}, function (error, data) {
if (error) done(error);
else done(null, tsm.mergeBuilds(data.available, data.installed));
});
return emitter;
}; |
builder
Runs the builder.py script for the specified | tsm.builder = function (options, done) {
var emitter = options.emitter || new EventEmitter();
options.python = options.python || 'python';
tsm.findInstalled(options.dir, options.input, function (error, builds) {
if (error) return done(error);
if (builds.length === 0) return done (new Error('no matched builds'));
var build = builds.pop();
var args = [path.join(build.dir, options.os, 'builder.py')];
var child = spawn('python', args.concat(options.args || []));
emitter.emit('spawned', child);
child.on('exit', function (code) {
if (code === 0 || code === 255)
done();
else {
var er = new Error("process exited with code: " + code);
er.code = code;
done(er);
}
});
if (!options.silent) {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
}, emitter);
return emitter;
}; |
titanium
Runs the titanium.py script | tsm.titanium = function (options, done) {
var emitter = options.emitter || new EventEmitter();
options.python = options.python || 'python';
tsm.findInstalled(options.dir, options.input, function (error, builds) {
if (error) return done(error);
if (builds.length === 0) return done (new Error('no matched builds'));
var build = builds.pop();
var args = [path.join(build.dir, 'titanium.py')];
args = args.concat(options.args || []);
emitter.emit('debug', "spawning: python " + args);
var child = spawn('python', args);
emitter.emit('spawned', child);
child.on('exit', function (code) {
if (code === 0 || code === 255)
done();
else {
var er = new Error("process exited with code: " + code);
er.code = code;
done(er);
}
});
if (!options.silent) {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
}, emitter);
return emitter;
}; |
Helper functions | var branchesURL =
'http://builds.appcelerator.com.s3.amazonaws.com/mobile/branches.json';
var branchURL =
'http://builds.appcelerator.com.s3.amazonaws.com/mobile/$BRANCH/index.json'; |
zipURL needs branch/zipname added to it | var zipURL = 'http://builds.appcelerator.com.s3.amazonaws.com/mobile/'; |
gitCheck
Checks to see if the given input matches the given hash | tsm.gitCheck = function (input, revision) {
if (input && input.length > 1 && revision.indexOf(input) === 0) return true;
else return false;
}; |
getBranches
Gets a list of branches from Appcelerator, formatted as an array of strings | tsm.getBranches = function (done) {
request(branchesURL, function (error, response, body) {
try {
if (error) throw error;
if (response.statusCode != 200)
throw new Error("HTTP " + response.statusCode);
var data = JSON.parse(body).branches;
if (!data) throw new Error("got malformed response from appcelerator");
done(null, data);
} catch (e) { done(e); }
});
}; |
parseDate
Parses dates from the strange way Appcelerator chooses to format them. Returns a date object. | tsm.parseDate = function (dateStr) {
var date = new Date(); |
date parsing code stolen right off of builds.appcelerator.net | date.setFullYear(
dateStr.substring(0,4), dateStr.substring(4,6)-1, dateStr.substring(6,8));
date.setHours(dateStr.substring(8, 10));
date.setMinutes(dateStr.substring(10,12));
date.setSeconds(dateStr.substring(12,14));
return date;
}; |
parseBuildList
Parses a build list from appcelerator, matching only those builds we're interested in. Returns a list of builds formatted like: | |
| tsm.parseBuildList = function (input, os, builds, emitter) {
return builds.filter(function (item) {
if (os && item.filename.indexOf(os) === -1) return false;
var match; |
Parse out version from filename (without date on the end) | match = item.filename.match(/[0-9]*\.[0-9]*\.[0-9]*/);
if (Array.isArray(match)) item.version = match[0]; |
Parse out date from filename | match = item.filename.match(/[0-9]{14}/);
if (Array.isArray(match)) item.date = tsm.parseDate(match[0]);
item.zip = zipURL + item.git_branch + "/" + item.filename;
item.githash = item.git_revision.slice(0, 7); |
Ignore invalid builds. Shouldn't fail unless they change something.. | if (!item.version || !item.date) {
if (emitter) emitter.emit(
'warn',
"couldn't parse version or date from filename: " + item.filename
);
return false;
}
var satisfied;
if (input) satisfied = (
semver.satisfies(item.version, input) ||
tsm.gitCheck(input, item.git_revision)
); |
If there's no input, or if there is input and it was satisfied, this item 'passes' | if (!input || (input && satisfied)) return true;
return false;
});
}; |
getBuilds
Get the list of builds for a particular branch | tsm.getBuilds = function (branch, done) {
var url = branchURL.replace('$BRANCH', branch);
request(url, function (error, response, body) {
try {
if (error) throw error;
if (response.statusCode != 200)
throw new Error("got http " + response.statusCode);
var data = JSON.parse(body);
done(null, data);
}
catch (e) { done(e); }
});
}; |
getAllBuilds
Gets all builds matching the input query. Input can be undefined in which case we return all builds. | tsm.getAllBuilds = function (input, os, done, emitter) {
emitter = emitter || new EventEmitter();
tsm.getBranches(function (error, branches) {
async.reduce(branches, [], function (memo, item, callback) {
emitter.emit('debug', "retrieving builds for branch: " + item);
tsm.getBuilds(item, function (error, builds) {
if (error) callback(error);
else callback(null, memo.concat(builds));
});
}, function (error, result) { |
Sort and return | if (error) return done(error);
result = tsm.parseBuildList(input, os, result);
result.sort(function (a, b) {
return a.date.getTime() - b.date.getTime();
});
done(null, result);
});
});
return emitter;
}; |
parseVersionFile
Parses the text of a version.txt file and returns it | tsm.parseVersionFile = function (data) {
return data.split('\n').reduce(function (memo, item) {
if (!item) return memo; |
remove /r characters which may be present on windows | item = item.replace(/\r/g, '');
item = item.split('=');
memo[item[0]] = item[1];
return memo;
}, {});
}; |
examineDir
Tries to get the version.txt file in a directory. If it isn't found or it doesn't appear to have the correct properties, an error is returned. | tsm.examineDir = function (dir, done) {
fs.readFile(path.join(dir, 'version.txt'), 'utf8', function (error, data) {
if (error) return done(error);
data = tsm.parseVersionFile(data);
if (!data.githash || !data.version || !data.timestamp)
done(new SyntaxError('dir does not appear to contain a valid sdk'));
else done(null, data);
});
}; |
findInstalled
Finds installed versions. Returns an emitter which may emit 'debug' and 'log' events. | tsm.findInstalled = function (dir, input, done, emitter) {
emitter = emitter || new EventEmitter();
fs.readdir(dir, function (error, versions) {
if (error) return done(error);
emitter.emit("debug", "examining directories: " + versions.join(','));
async.reduce(versions, [], function (memo, version, callback) {
tsm.examineDir(path.join(dir, version), function (error, build) {
if (error) return callback(null, memo);
emitter.emit("debug", "got data for version: " + version);
var satisfied;
if (input) satisfied = (
semver.satisfies(build.version, input) ||
tsm.gitCheck(input, build.githash)
);
if (!input || (input && satisfied)) {
emitter.emit("debug", version + " satisfies, returning it");
memo.push({
githash: build.githash,
version: build.version,
date: new Date(build.timestamp),
dir: path.join(dir, version)
});
}
callback(null, memo);
});
}, function (error, data) {
if (error) return done(error);
data.sort(function (a, b) {
return a.date.getTime() - b.date.getTime();
});
done(null, data);
});
});
return emitter;
}; |
mergeBuilds
Merges one list of available builds and one list of installed builds | tsm.mergeBuilds = function (available, installed) { |
Setup a mapping of git hashes to build objects. | var installedByHash = {};
installed.forEach(function (build) {
installedByHash[build.githash] = build;
}); |
We'll loop over the available builds and delete builds from the installedByHash object when we encounter them. That way, any builds not present in the installed list but present in the available list will still be in the installedByHash object at the end and we can simply drop those on the end of the available list and return it. | available = available.map(function (build) {
if (installedByHash[build.githash]) build.installed = true;
else build.installed = false;
delete installedByHash[build.githash];
return build;
});
Object.keys(installedByHash).forEach(function (hash) {
var build = installedByHash[hash];
build.installed = true;
available.push(build);
});
available.sort(function (a, b) {
return a.date.getTime() - b.date.getTime();
});
return available;
};
var oldexec = exec;
exec = function (what, callback) {
oldexec(what, callback);
}; |
unzip
Extracts some zip. Should work on windows and any platform that has unzip. | tsm.unzip = function (zip, output, done) {
if (process.platform == 'win32')
exec("\"" + __dirname + "\\7za.exe\" x -y \"" + zip + "\" -o\"" + output + "\"", done);
else
exec("unzip -oqq '" + zip + "' -d '" + output + "'", done);
};
|