UNPKG

tsm

Version:

A manager for Titanium SDK versions

553 lines (467 loc) 17.1 kB
// ## tsm // Titanium SDK manager // // This file contains functions for retreiving builds & build metadata from // Appcelerator. Tests are in `test.js`. 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 // * `options` object // * `dir` output directory // * `input` git hash or version to match // * `os` os to match, should be 'osx' 'win32' 'linux' // * `done` callback, called with `(error)` // // Installs a matching SDK to the provided directory. Returns an emitter which // will emit `progress` events like `{left: 38413, done: 5893, percent: 0.34}`, // `debug` events and `log` events. 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 // * `options` object // * `dir` directory to find sdks // * `input` git hash or version to match // * `done` callback called with `(error)` // // Removes sdks matching `input`. Returns an emitter which may emit `debug` // and/or `log` events. 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 // * `options` object, can have the following properties: // * `os` string *required* os of builds, one of 'osx' 'linux' 'win32' // * `installed` boolean, whether or not to include installed builds // * `available` boolean, whether or not to include uninstalled builds // * `dir` string, directory to look for installed builds // * `input` string, git hash or version to match // * `done` callback called on completion with `(error, builds)` // // 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 // * `options` object // * `dir` directory to find sdks // * `input` git hash or version to match // * `os` 'android' or 'iphone' // * `args` array of arguments to pass to builder.py // * `python` string, optional, path of python // * `silent` boolean; if true, output will be supressed // * `done` callback to call when completed or upon error // // Runs the builder.py script for the specified `os`. 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 // * `options` object // * `dir` directory to find sdks // * `input` git hash or version to match // * `args` array of arguments to pass to titanium.py // * `python` string, optional, path of python // * `silent` boolean; if true, output will be supressed // * `done` callback to call when completed or upon error // // 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 // * `input` some partial hash // * `revision` some git revision hash // // 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 // * `done` callback to be called when complete, passed `(error, data)` // // 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 // * `dateStr` string to parse date out of // // 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 // * `input` version or git hash to match against builds, can be falsy // * `os` should be 'linux' or 'osx' or 'win32' // * `builds` array of builds to parse // // Parses a build list from appcelerator, matching only those builds we're // interested in. Returns a list of builds formatted like: // [ // { // date: date object corresponding to build date, // version: version string like '2.2.0', // zip: zip url, // git_revision: git hash, // githash: short git hash (first 7 chars), // sha1: sha1 hash of file, // size: size in bytes, // git_branch: git branch name, // build_type: mobile or desktop, // filename: basename ie 'mobilesdk-2.2.0-version.zip', // build_url: jenkins url // }, // ... // ] 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 // * `branch` branch to get builds for // * `done` callback function called with `(error, builds)` // // 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 // * `input` git hash or version to match // * `os` os to match, should be 'linux' or 'osx' or 'win32' // * `done` callback function, called with `(error, builds)` // // 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 // * `data` version file text // // 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 // * `dir` directory to examine // * `done` callback called with `(error, data)` // // 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 // * `dir` directory to examine // * `input` git hash or version to match // * `done` callback function, called with `(error, builds)` // // 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 // * `available` uninstalled but available builds // * `installed` builds assumed to already be on the user's system // // 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 // * `zip` zip file to extract // * `output` output directory // * `done` callback // // 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); };