thali
Version:
287 lines (258 loc) • 11.5 kB
JavaScript
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var path = require('path');
var os = require('os');
var https = require('https');
var unzip = require('unzip');
var Promise = require('lie');
var fs = require('fs-extra-promise');
var url = require('url');
var request = require('request');
var FILE_NOT_FOUND = "ENOENT";
var MAGIC_DIRECTORY_NAME_FOR_LOCAL_DEPLOYMENT = "localdev"; // If this file exists in the thaliDontCheckIn directory then
// we will copy the Cordova plugin from a sibling Thali_CordovaPlugin
// project to this Cordova project.
// I tried child-process-promise but it failed without errors and I just don't
// have time to fight with it right now.
function childProcessExecPromise(command, currentWorkingDirectory) {
return new Promise(function(resolve, reject) {
exec(command, { cwd: currentWorkingDirectory }, function(error, stdout, stderr) {
if (error) {
reject(error);
return;
}
// Log output even if command doesn't exit with an error,
// because otherwise useful debugging information might get lost.
if (stdout) console.log(stdout);
if (stderr) console.log(stderr);
resolve();
});
})
}
// Unfortunately the obvious library, request-promise, doesn't handle streams
// well so it would take the multi-megabyte ZIP response file and turn it into
// an in-memory string. So we use this instead.
function httpRequestPromise(method, urlObject) {
if (method != "GET" && method != "HEAD") {
return Promise.reject(new Error("We only support GET or HEAD requests"));
}
return new Promise(function(resolve, reject) {
var httpsRequestOptions = {
host: urlObject.host,
method: method,
path: urlObject.path,
keepAlive: true
};
var req = https.request(httpsRequestOptions, function(res) {
if (res.statusCode != 200) {
reject(new Error("Did not get 200 for " + urlObject.href + ", instead got " + res.statusCode));
return;
}
resolve(res);
}).on('error', function(e) {
reject(new Error("Got error on " + urlObject.href + " - " + e));
});
req.end();
});
}
function getEtagFileLocation(depotName, branchName, directoryToInstallIn) {
return path.join(directoryToInstallIn, "etag-" + depotName + "-" + branchName);
}
function getEtagFromEtagFile(depotName, branchName, directoryToInstallIn) {
var etagFileLocation = getEtagFileLocation(depotName, branchName, directoryToInstallIn);
return fs.readFileAsync(etagFileLocation)
.catch(function(err) {
if (err.code != FILE_NOT_FOUND) {
return Promise.reject(err);
} else {
return Promise.resolve();
}
});
}
function returnEtagFromResponse(httpResponse) {
// The etag value is returned with quotes but when we set the header it adds its
// own quotes so we need to strip those quotes here
return httpResponse && httpResponse.headers && httpResponse.headers.etag &&
httpResponse.headers.etag.substring(1, httpResponse.headers.etag.length - 1);
}
function writeToEtagFile(depotName, branchName, directoryToInstallIn, httpResponse) {
var etag = returnEtagFromResponse(httpResponse);
if (etag == null) {
return Promise.reject(new Error("Did not get ETag header, something is wrong because Github always sends one!"));
}
var etagFileLocation = getEtagFileLocation(depotName, branchName, directoryToInstallIn);
return fs.writeFileAsync(etagFileLocation, etag);
}
function getGitHubZipUrlObject(projectName, depotName, branchName) {
return url.parse("https://codeload.github.com/" + projectName + "/" + depotName + "/zip/" + branchName);
}
/**
* This method is a hack because I'm having trouble getting GitHub to respect if-none-match headers. So instead
* I'm doing a HEAD request and manually checking if the etags match.
*/
function doGitHubEtagsMatch(projectName, depotName, branchName, directoryToInstallIn) {
return getEtagFromEtagFile(depotName, branchName, directoryToInstallIn)
.then(function(etagFromFile) {
if (!etagFromFile) {
return false;
}
return httpRequestPromise("HEAD",
getGitHubZipUrlObject(projectName, depotName, branchName))
.then(function(res) {
var etagFromHeadRequest = returnEtagFromResponse(res);
return etagFromFile == etagFromHeadRequest;
});
});
}
function createUnzippedDirectoryPath(depotName, branchName, directoryToInstallIn) {
return path.join(directoryToInstallIn, depotName + "-" + branchName);
}
function createGitHubZipResponse(depotName, branchName, directoryToInstallIn, directoryUpdated) {
return {
unzipedDirectory: createUnzippedDirectoryPath(depotName, branchName, directoryToInstallIn),
directoryUpdated: directoryUpdated
};
}
function installGitHubZip(projectName, depotName, branchName, directoryToInstallIn) {
var gitHubZipUrlObject = getGitHubZipUrlObject(projectName, depotName, branchName);
return doGitHubEtagsMatch(projectName, depotName, branchName, directoryToInstallIn)
.then(function(doTheEtagsMatch) {
if (doTheEtagsMatch) {
return createGitHubZipResponse(depotName, branchName, directoryToInstallIn, false);
}
console.log('Starting to download Thali Cordova plugin from: ' + gitHubZipUrlObject.href);
return httpRequestPromise("GET", gitHubZipUrlObject)
.then(function(res) {
return new Promise(function(resolve, reject) {
res.pipe(unzip.Extract({ path: directoryToInstallIn}))
.on('close', function() {
resolve();
}).on('error', function(e) {
reject(new Error("Could not extract zip file from " + gitHubZipUrlObject.href + ", error was " + e));
});
}).then(function() {
return writeToEtagFile(depotName, branchName, directoryToInstallIn, res);
}).then(function() {
return createGitHubZipResponse(depotName, branchName, directoryToInstallIn, true);
});
});
});
}
function uninstallPluginsIfNecessary(weAddedPluginsFile, appRootDirectory) {
return fs.readFileAsync(weAddedPluginsFile).catch(function(err) {
if (err) {
if (err.code == FILE_NOT_FOUND) {
return Promise.resolve(false);
}
return Promise.reject(err);
}
return Promise.resolve(true);
}).then(function(doWeNeedToUninstall) {
if (!doWeNeedToUninstall) {
return Promise.resolve();
}
console.log('Trying to remove previously installed Thali Cordova plugin');
var pluginRemoveCommand = 'cordova plugin remove org.thaliproject.p2p';
return childProcessExecPromise(pluginRemoveCommand, appRootDirectory)
.catch(function (err) {
console.log('Ignoring a non-critical error: ' + err);
// Resolve the promise even if plugin removal fails, because it is possible
// that the user has removed the plugin outside of this install script, but there
// is still the left-over file that says this script has added the plugins.
return Promise.resolve();
});
})
}
/**
* This will copy the contents of a Thali_CordovaPlugin local depot to the right directory in the
* current Cordova project so it will be installed. This is used for local development only.
*/
function copyDevelopmentThaliCordovaPluginToProject(appRootDirectory, thaliDontCheckIn, depotName, branchName) {
var targetDirectory = createUnzippedDirectoryPath(depotName, branchName, thaliDontCheckIn);
var sourceDirectory = path.join(appRootDirectory, "../Thali_CordovaPlugin");
return new Promise(function(resolve, reject) {
fs.remove(targetDirectory, function(err) {
if (err) {
reject(new Error("copyDevelopmentThaliCordovaPluginToProject remove failed with " + err));
return;
}
console.log('Copying files from ' + sourceDirectory + ' to ' + targetDirectory);
fs.copy(sourceDirectory, targetDirectory, function (err) {
if (err) {
reject(new Error("copyDevelopmentThaliCordovaPluginToProject failed with" + err));
return;
}
resolve(createGitHubZipResponse(depotName, branchName, thaliDontCheckIn, true));
});
});
});
}
function doesMagicDirectoryNamedExist(thaliDontCheckIn) {
var magicFileLocation = path.join(thaliDontCheckIn, MAGIC_DIRECTORY_NAME_FOR_LOCAL_DEPLOYMENT);
return fs.existsSync(magicFileLocation);
}
function fetchAndInstallJxCoreCordovaPlugin(baseDir, jxCoreVersionNumber) {
return new Promise(function(resolve, reject) {
console.log('Trying to install jxcore-cordova version: ' + jxCoreVersionNumber);
var jxcBin = path.join(__dirname, 'node_modules', 'jxc', 'bin', 'jxc.bin.js');
var jxcInstall = spawn('jx', [jxcBin, 'install', jxCoreVersionNumber], { cwd: baseDir });
jxcInstall.stdout.on('data', function (data) {
console.log(data + '');
});
jxcInstall.stderr.on('data', function (data) {
console.log(data + '');
});
jxcInstall.on('close', function (code) {
if (code === 0) {
resolve();
} else {
reject('jxc install exited with code: ' + code);
}
});
});
}
module.exports = function(callback, appRootDirectory) {
// Get the app root as an argument or from app/www/jxcore/node_modules/thali.
// Passing as argument can be leveraged in local development and testing scenarios.
appRootDirectory = appRootDirectory || path.join(__dirname, '../../../../../');
var thaliDontCheckIn = path.join(appRootDirectory, "thaliDontCheckIn" );
var appScriptsFolder = path.join(appRootDirectory, "plugins/org.thaliproject.p2p/scripts");
var jxCoreVersionNumber = "0.0.8";
var thaliProjectName = "thaliproject";
var thaliDepotName = "Thali_CordovaPlugin";
var thaliBranchName = "npmv2.1.0";
fetchAndInstallJxCoreCordovaPlugin(appRootDirectory, jxCoreVersionNumber)
.then(function () {
if (doesMagicDirectoryNamedExist(thaliDontCheckIn)) {
return copyDevelopmentThaliCordovaPluginToProject(appRootDirectory, thaliDontCheckIn, thaliDepotName, thaliBranchName);
} else {
return installGitHubZip(thaliProjectName, thaliDepotName, thaliBranchName, thaliDontCheckIn);
}
})
.then(function(thaliCordovaPluginUnZipResult) {
if (thaliCordovaPluginUnZipResult.directoryUpdated) {
var weAddedPluginsFile = path.join(thaliDontCheckIn, "weAddedPlugins");
return uninstallPluginsIfNecessary(weAddedPluginsFile, appRootDirectory)
.then(function() {
console.log('Adding Thali Cordova plugin from: ' + thaliCordovaPluginUnZipResult.unzipedDirectory);
return childProcessExecPromise('cordova plugins add ' + thaliCordovaPluginUnZipResult.unzipedDirectory,
appRootDirectory);
}).then(function() {
// The step below is required, because the Android after prepare Cordova hook
// depends on external node modules that need to be installed.
console.log('Running jx npm install in: ' + appScriptsFolder);
return childProcessExecPromise('jx npm install --autoremove "*.gz"', appScriptsFolder);
}).then(function() {
return fs.writeFileAsync(weAddedPluginsFile, "yes");
});
}
})
.then(function() {
// Success
callback();
})
.catch(function(error) {
callback(error, null);
});
};
;