@titanium/sdk-manager
Version:
⭐ Axway Amplify module for managing and installing SDK for Appcelerator Titanium SDK Framework
559 lines (457 loc) • 15.1 kB
JavaScript
/* eslint-disable promise/avoid-new */
const sdk = {};
module.exports = sdk;
const fs = require(`fs-extra`);
const path = require(`path`);
const _ = require(`lodash`);
const pluralize = require(`pluralize`);
const request = require(`request`);
const tmp = require(`tmp`);
const http = require(`http`);
const { STATUS_CODES } = http;
const { log } = console;
const highlight = value => value;
sdk.options = require(`./options`);
const locationsModule = require(`./locations`);
sdk.getInstallPaths = locationsModule.getInstallPaths;
sdk.locations = locationsModule.locations;
const legacy = require(`./legacy`);
const { expandPath } = legacy;
const { isDir } = legacy;
const util = require(`./util`);
const { architecture } = util;
const { buildRequestParams } = util;
const { extractZip } = util;
const { fetchJSON } = util;
const { os } = util;
const { version } = util;
/**
* A regex to extract a continuous integration build version and platform from the filename.
* @type {RegExp}
*/
const ciBuildRegExp = /^mobilesdk-(.+)(?:\.v|-)((\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2}))-([^.]+)/;
/**
* A regex to test if a string is a URL or path to a zip file.
* @type {RegExp}
*/
const uriRegExp = /^(https?:\/\/.+)|(?:file:\/\/(.+))$/;
/**
* Retrieves the list of CI branches.
*
* @returns {Promise<object>}
*/
sdk.getBranches = async () => {
const results = await fetchJSON(sdk.options.sdk.urls.branches);
results.branches.sort();
return results;
};
/**
* Retrieves a list of Titanium SDK continuous integration builds.
*
* @param {string} [branch="master"] - The branch to retreive.
* @returns {Promise<object>} Resolves a map of versions to build info.
*/
sdk.getBuilds = ({
branch = `master`,
buildsUrl = `http://builds.appcelerator.com/mobile/<BRANCH>/index.json`,
buildUrl = `http://builds.appcelerator.com/mobile/<BRANCH>/<FILENAME>`,
noLatest = false,
} = {}) => {
if (!_.isString(branch)) {
throw new TypeError(`Expected branch to be a string`);
}
const { urls } = sdk.options.sdk;
return fetchJSON(buildsUrl.replace(/<BRANCH>/, branch))
.then(builds => {
const results = {};
log(`Received ${pluralize(`build`, builds.length, true)}`);
for (const build of builds) {
const { build_type, filename, git_branch, git_revision } = build;
const m = filename && filename.match(ciBuildRegExp);
if (build_type !== `mobile` || !m || !filename.includes(`-${os}`)) {
continue;
}
const name = `${m[1]}.v${m[2]}`;
results[name] = {
version: m[1],
ts: m[2],
githash: git_revision,
date: new Date(`${m[3]}-${m.slice(4, 6).join(`-`)}T${m.slice(6, 9).join(`:`)}.000Z`),
url: buildUrl.replace(/<BRANCH>/, git_branch).replace(/<FILENAME>/, filename),
};
}
return results;
});
};
const cachedSDKs = _.memoize(() => {
const results = [];
for (const dir of sdk.getPaths()) {
if (isDir(dir)) {
for (let sdkDir of fs.readdirSync(dir)) {
sdkDir = path.join(dir, sdkDir);
try {
results.push(sdk.getSdk(sdkDir));
} catch (e) {
// Do nothing
}
}
}
}
return results;
});
sdk.getInstalledSDKs = force => {
force && cachedSDKs.cache.clear();
return cachedSDKs();
};
/**
* Retrieves a map of Titanium SDK versions to release info including the download URL. By default,
* the latest version is added to the map of releases.
*
* @param {boolean} [noLatest=false] - When `true`, it does not determine the 'latest' release.
* @returns {Promise<object>} Resolves a map of versions to URLs.
*/
sdk.getReleases = async ({ releasesUrl = `https://appc-mobilesdk-server.s3-us-west-2.amazonaws.com/releases.json`, noLatest = false } = {}) => {
const { releases } = await fetchJSON(releasesUrl);
const results = {};
const is64 = architecture === `x64`;
const archRE = /64bit/;
for (const release of releases) {
const { build_type, name, url, version: ver } = release;
if (release.os !== os || name !== `mobilesdk`) {
continue;
}
const is64build = archRE.test(build_type);
if (os !== `linux` || (is64 && is64build) || (!is64 && !is64build)) {
results[ver] = {
name: ver,
version: ver.replace(/\.GA.*$/, ``),
url,
};
}
}
if (!noLatest) {
const latest = Object.keys(results).sort(version.rcompare)[0];
if (latest) {
results.latest = results[latest];
}
}
return results;
};
/**
* Returns a list of possible SDK install paths.
*
* @returns {Array.<string>}
*/
sdk.getPaths = () => {
return _.uniq([
..._.castArray(_.get(sdk.options, `sdk.searchPaths`), true).map(p => expandPath(p)),
...sdk.getInstallPaths().map(p => expandPath(p, `mobilesdk`, os)),
]);
};
/**
* Install a Titanium SDK from either a URI or version. A URI may be either a local file, a URL, an
* SDK version, a CI branch, or a CI branch and build hash.
*
* @param {object} [params] - Various parameters.
* @param {Context} [params.downloadDir] - When `uri` is a URL, release, or build, download the SDK
* to this directory.
* @param {string} [params.installDir] - The path to install the SDK. Defaults to the first path in
* the list of Titanium install locations.
* @param {boolean} [params.keep=false] - When `true` and `uri` is a URL, release, or build, and
* `downloadDir` is specified, then the downloaded SDK `.zip` file is not deleted after install.
* @param {boolean} [params.force=false] - When `true`, overwrites an existing Titanium SDK
* installation, otherwise an error is thrown.
* @param {string} [params.uri] - A URI to a local file or remote URL to download.
* @param params.releasesUrl
* @param params.sdkUrl
* @returns {Promise}
*/
sdk.install = async ({
releasesUrl = `https://appc-mobilesdk-server.s3-us-west-2.amazonaws.com/releases.json`,
force = false,
sdkUrl = `latest`,
keep = false,
installDir,
downloadDir,
}) => {
// log('you are here → sdk.install()');
if (!_.isString(sdkUrl)) {
throw new TypeError(`Expected URI to be a string`);
}
const uri = sdkUrl.trim();
const uriMatch = sdkUrl.match(uriRegExp);
let downloadedFile = null;
let file = null;
let downloadUrl = null;
let sdkName;
let titaniumDir;
let sdkDir;
// step 1: determine what the uri is
if (uriMatch && uriMatch[2]) {
file = uriMatch[2];
} else if (sdkUrl && fs.existsSync(sdkUrl)) {
file = sdkUrl;
}
if (file) {
file = expandPath(file);
if (!fs.existsSync(file)) {
throw new Error(`Specified file URI does not exist`);
}
if (!/\.zip$/.test(file)) {
throw new Error(`Specified file URI is not a zip file`);
}
} else {
// we are downloading an sdk
if (uriMatch && uriMatch[1]) {
// we have a http url
downloadUrl = uriMatch[1];
log(`URI is a URL: ${highlight(downloadUrl)}`);
} else {
// we have a version that needs to be resolved to a url
const releases = await sdk.getReleases({ releasesUrl });
let ver = uri;
let release;
if (_.get(releases, `${ver}.GA`)) {
// we have a GA release
release = _.get(releases, `${ver}.GA`);
downloadUrl = release.url;
sdkName = release.version;
log(`URI is a GA release: ${highlight(downloadUrl)}`);
} else if (_.get(releases, `${ver}.RC`)) {
// we have a RC release
release = _.get(releases, `${ver}.RC`);
downloadUrl = release.url;
sdkName = release.version;
log(`URI is an BETA release: ${highlight(downloadUrl)}`);
} else if (_.get(releases, `${ver}.BETA`)) {
// we have a RC release
release = _.get(releases, `${ver}.BETA`);
downloadUrl = release.url;
sdkName = release.version;
log(`URI is an RC release: ${highlight(downloadUrl)}`);
} else if (_.get(releases, `${ver}`)) {
// we found an exact match for release
release = _.get(releases, `${ver}`);
downloadUrl = release.url;
sdkName = release.version;
log(`URI is an exact match for release: ${highlight(downloadUrl)}`);
} else {
// maybe a ci build?
let { branches, defaultBranch } = await sdk.getBranches();
if (ver) {
const m = ver.match(/^([A-Za-z0-9_]+?):(.+)$/);
if (m) {
// uri is a branch:hash combo
const branch = m[1];
log(`URI is a branch:hash combo: ${highlight(ver)}`);
if (!branches.includes(branch)) {
throw new Error(`Invalid branch "${branch}"`);
}
branches = [ branch ];
ver = m[2];
} else if (branches.includes(ver)) {
// uri is a ci branch, default to latest version
log(`URI is a branch: ${highlight(`${ver}:latest`)}`);
branches = [ ver ];
ver = `latest`;
}
}
branches.sort((a, b) => {
// force the default branch to the front
return a === defaultBranch ? -1 : b.localeCompare(a);
});
if (branches.length > 1) {
log(`Scanning ${pluralize(`branch`, branches.length, true)} for ${ver}`);
}
downloadUrl = await branches.reduce((promise, branch) => {
return promise.then(async url => {
if (url) {
return url;
}
const builds = await sdk.getBuilds(branch);
const sortBuilds = (a, b) => {
const r = version.rcompare(builds[a].version, builds[b].version);
return r === 0 ? builds[b].ts.localeCompare(builds[a].ts) : r;
};
// eslint-disable-next-line promise/always-return
for (const name of Object.keys(builds).sort(sortBuilds)) {
if (ver === `latest` || name === ver || builds[name].githash === ver) {
return builds[name].url;
}
}
});
}, Promise.resolve());
}
}
if (!downloadUrl) {
// note: yes, we want `sdkUrl` and not `uri`
throw new Error(`Unable to find any Titanium SDK releases or CI builds that match "${sdkUrl}"`);
}
// step 1.5: download the file
titaniumDir = installDir ? expandPath(installDir) : sdk.getInstallPaths()[0];
if (sdkName) {
sdkDir = path.join(titaniumDir, `mobilesdk`, os, sdkName);
if (!force && isDir(sdkDir)) {
throw new Error(`Titanium SDK "${sdkName}" already exists: ${sdkDir}`);
}
}
downloadedFile = tmp.tmpNameSync({
dir: downloadDir,
prefix: `titaniumsdk-`,
postfix: `.zip`,
});
if (!downloadDir) {
downloadDir = path.dirname(downloadedFile);
keep = false;
}
await fs.mkdirp(downloadDir);
file = await new Promise((resolve, reject) => {
log(`Downloading ${highlight(downloadUrl)} => ${highlight(downloadedFile)}`);
const req = request(buildRequestParams({ url: downloadUrl }));
const out = fs.createWriteStream(downloadedFile);
req.pipe(out);
req.on(`response`, response => {
const { statusCode } = response;
if (statusCode >= 400) {
fs.removeSync(downloadedFile);
return reject(new Error(`${statusCode} ${STATUS_CODES[statusCode]}`));
}
out.on(`close`, () => {
let file = downloadedFile;
const m = downloadUrl.match(/.*\/(.+\.zip)$/);
if (m) {
file = path.join(downloadDir, m[1]);
fs.renameSync(downloadedFile, file);
downloadedFile = file;
}
resolve(file);
});
});
req.once(`error`, reject);
});
}
// step 2: extract the sdk zip file
const sdkDestRegExp = new RegExp(`^mobilesdk[/\\\\]${os}[/\\\\]([^/\\\\]+)`);
const tempDir = tmp.tmpNameSync({ prefix: `titaniumsdk-` });
// const titaniumDir = installDir ? expandPath(installDir) : sdk.getInstallPaths()[0];
let name;
const dest = null;
if (!titaniumDir) {
throw new Error(`Unable to determine the Titanium directory`);
}
log(`Using Titanium directory: ${highlight(titaniumDir)}`);
try {
await extractZip({
dest: tempDir,
file,
onEntry(filename) {
// do a quick check to make sure the destination doesn't exist
const m = !name && filename.match(sdkDestRegExp);
if (m) {
name = m[1];
if (!sdkName) {
sdkDir = path.join(titaniumDir, `mobilesdk`, os, name);
if (!force && isDir(sdkDir)) {
throw new Error(`Titanium SDK "${name}" already exists: ${sdkDir}`);
}
}
}
},
});
if (!name) {
throw new Error(`Zip file does not appear to contain a Titanium SDK`);
}
// step 3: move the sdk files to the dest
let src = path.join(tempDir, `mobilesdk`, os, name);
log(`Moving SDK files: ${highlight(src)} => ${highlight(dest)}`);
await fs.move(src, sdkDir, { overwrite: true });
// install the modules
src = path.join(tempDir, `modules`);
if (isDir(src)) {
const modulesDest = path.join(titaniumDir, `modules`);
for (const platform of fs.readdirSync(src)) {
const srcPlatformDir = path.join(src, platform);
if (!isDir(srcPlatformDir)) {
continue;
}
for (const moduleName of fs.readdirSync(srcPlatformDir)) {
const srcModuleDir = path.join(srcPlatformDir, moduleName);
if (!isDir(srcModuleDir)) {
continue;
}
for (const ver of fs.readdirSync(srcModuleDir)) {
const srcVersionDir = path.join(srcModuleDir, ver);
if (!isDir(srcVersionDir)) {
continue;
}
const destDir = path.join(modulesDest, platform, moduleName, ver);
log(`Moving module files ${highlight(`${platform}/${moduleName}@${ver}`)}: ${highlight(srcVersionDir)} => ${highlight(destDir)}`);
if (!force && isDir(destDir)) {
log(`Module ${highlight(`${platform}/${moduleName}@${ver}`)} already exists, skipping`);
continue;
}
await fs.move(srcVersionDir, destDir, { overwrite: true });
}
}
}
}
} finally {
log(`Removing ${highlight(tempDir)}`);
await fs.remove(tempDir);
}
if (downloadedFile && !keep) {
log(`Removing ${highlight(downloadedFile)}`);
await fs.remove(downloadedFile);
}
return dest;
};
/**
* Deletes an installed Titanium SDK by name or path.
*
* @param {string} nameOrPath - The SDK name or path to uninstall.
* @returns {Promise<Array<TitaniumSDK>>} Resolves an array of versions removed.
* @access private
*/
sdk.uninstall = async nameOrPath => {
if (!nameOrPath || typeof nameOrPath !== `string`) {
throw new TypeError(`Expected an SDK name or path`);
}
const sdks = await sdk.getInstalledSDKs(true);
const results = [];
for (const sdk of sdks) {
if (sdk.name === nameOrPath || sdk.path === nameOrPath) {
results.push(sdk);
log(`Deleting ${highlight(sdk.path)}`);
await fs.remove(sdk.path);
}
}
if (!results.length) {
const err = new Error(`Unable to find any SDKs matching "${nameOrPath}"`);
err.code = `ENOTFOUND`;
throw err;
}
return results;
};
sdk.getSdk = dir => {
if (typeof dir !== `string` || !dir) {
throw new TypeError(`Expected directory to be a valid string`);
}
dir = expandPath(dir);
if (!isDir(dir)) {
throw new Error(`Directory does not exist`);
}
const result = {};
result.name = path.basename(dir);
result.manifest = null;
result.path = dir;
try {
const manifestFile = path.join(dir, `manifest.json`);
result.manifest = JSON.parse(fs.readFileSync(manifestFile));
if (!_.isObject(result.manifest)) {
throw new Error();
}
} catch (e) {
throw new Error(`Directory does not contain a valid manifest.json`);
}
return result;
};