transaction based install with android handler refactored
Project: http://git-wip-us.apache.org/repos/asf/cordova-plugman/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugman/commit/5e1b502e Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugman/tree/5e1b502e Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugman/diff/5e1b502e Branch: refs/heads/master Commit: 5e1b502ecb548d049b50262766dfa4f88f2210a6 Parents: e036b56 Author: Fil Maj <[email protected]> Authored: Mon Apr 22 16:07:35 2013 -0700 Committer: Fil Maj <[email protected]> Committed: Mon Apr 22 16:07:35 2013 -0700 ---------------------------------------------------------------------- spec/install.spec.js | 10 +- spec/uninstall.spec.js | 14 ++- src/fetch.js | 5 +- src/install.js | 131 ++++++++++++--------- src/platforms/android.js | 251 +++++++++++++---------------------------- src/platforms/common.js | 36 ++++++- 6 files changed, 208 insertions(+), 239 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/spec/install.spec.js ---------------------------------------------------------------------- diff --git a/spec/install.spec.js b/spec/install.spec.js index 88c2ca4..9b93a39 100644 --- a/spec/install.spec.js +++ b/spec/install.spec.js @@ -33,10 +33,11 @@ describe('install', function() { shell.cp('-rf', dummyplugin, plugins_dir); }); - it('should call fetch if provided plugin cannot be resolved locally'); - it('should generate an array of transactions required to run an installation'); - it('should pass transaction log to appropriate platform handler\'s install'); - + it('should call fetch if provided plugin cannot be resolved locally', function() { + }); + // TODO: possibly test how diff platform transaction logs are created + it('should generate an array of transactions required to run an installation and pass into appropriate platform handler\'s install method', function() { + }); it('should call prepare after a successful install', function() { shell.cp('-rf', android_one_project, temp); var s = spyOn(plugman, 'prepare'); @@ -58,5 +59,6 @@ describe('install', function() { }).toThrow('Variable(s) missing: API_KEY'); }); it('should handle a failed install by passing completed transactions into appropriate handler\'s uninstall method'); + it('should throw if plugin is already installed into project'); }); }); http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/spec/uninstall.spec.js ---------------------------------------------------------------------- diff --git a/spec/uninstall.spec.js b/spec/uninstall.spec.js index c0fd48a..d95e89b 100644 --- a/spec/uninstall.spec.js +++ b/spec/uninstall.spec.js @@ -30,8 +30,15 @@ describe('uninstall', function() { }); describe('success', function() { - it('should generate an array of transactions required to run an uninstallation'); - it('should pass transaction log to appropriate platform handler\'s uninstall'); + beforeEach(function() { + shell.cp('-rf', android_one_project, temp); + shell.cp('-rf', dummyplugin, plugins_dir); + install('android', temp, 'DummyPlugin', plugins_dir, {}); + }); + + // TODO: possibly test how diff platform transaction logs are created + it('should generate and pass uninstall transaction log to appropriate platform handler\'s uninstall', function() { + }); }); describe('failure', function() { @@ -40,9 +47,6 @@ describe('uninstall', function() { shell.cp('-rf', dummyplugin, plugins_dir); install('android', temp, 'DummyPlugin', plugins_dir, {}); }); - afterEach(function() { - shell.rm('-rf', temp); - }); it('should throw if platform is unrecognized', function() { expect(function() { uninstall('atari', temp, 'SomePlugin', plugins_dir, {}); http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/src/fetch.js ---------------------------------------------------------------------- diff --git a/src/fetch.js b/src/fetch.js index 08ddf77..d20cf08 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -16,8 +16,11 @@ module.exports = function fetchPlugin(plugin_dir, plugins_dir, link, callback) { } else { plugins.clonePluginGitRepo(plugin_dir, plugins_dir, callback); } - } else { // Copy from the local filesystem. + } else { + // Copy from the local filesystem. var dest = path.join(plugins_dir, path.basename(plugin_dir)); + // TODO: throw if local cant be resolved + // TODO: if local cant be resolved, query remote service. shell.rm('-rf', dest); if (link) { http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/src/install.js ---------------------------------------------------------------------- diff --git a/src/install.js b/src/install.js index d246b15..f3cb776 100644 --- a/src/install.js +++ b/src/install.js @@ -1,16 +1,46 @@ var path = require('path'), fs = require('fs'), et = require('elementtree'), + fetch= require('./fetch'), platform_modules = require('./platforms'); +module.exports = function installPlugin(platform, project_dir, name, plugins_dir, cli_variables, callback) { + if (!platform_modules[platform]) { + var err = new Error(platform + " not supported."); + if (callback) { + callback(err); + return; + } + else throw err; + } + + var plugin_dir = path.join(plugins_dir, name); + + // Check that the plugin has already been fetched. + if (!fs.existsSync(plugin_dir)) { + // if plugin doesnt exist, use fetch to get it. + fetch(name, plugins_dir, false, function(err) { + if (err) { + callback(err); + } else { + runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback); + } + }); + } else { + runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback); + } +}; + function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback) { var xml_path = path.join(plugin_dir, 'plugin.xml') , xml_text = fs.readFileSync(xml_path, 'utf-8') , plugin_et = new et.ElementTree(et.XML(xml_text)) , filtered_variables = {} , info; - - // checking preferences + var name = plugin_et.findall('name').text; + var plugin_id = plugin_et._root.attrib['id']; + + // checking preferences, if certain variables are not provided, we should throw. prefs = plugin_et.findall('./preference') || []; prefs = prefs.concat(plugin_et.findall('./platform[@name="'+platform+'"]/preference')); var missing_vars = []; @@ -30,65 +60,54 @@ function runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variable else throw err; } - // Log out plugin INFO element contents in case additional install steps are necessary - if((info = plugin_et.find('./platform[@name="'+platform+'"]/info'))) { - console.log(info.text); - } - - // run the platform-specific function - try { - platform_modules[platform].handleInstall(project_dir, plugin_dir, plugin_et, filtered_variables); - require('./../plugman').prepare(project_dir, platform, plugins_dir); + var platformTag = plugin_et.find('./platform[@name="'+platform+'"]'); + if (!platformTag) { + // Either this plugin doesn't support this platform, or it's a JS-only plugin. + // Either way, return now. if (callback) callback(); - } catch(e) { - var err; - try { - platform_modules[platform].forceUninstall(project_dir, plugin_dir, plugin_et, filtered_variables); - err = new Error('Error during installation of plugin, reverted all changes. Install error: ' + e.message + '\n' + e.stack + '\n----'); - } catch(etwo) { - err = new Error('Error during installation of plugin, reverting changes also caused issues! Reversion probably incomplete. Install error: ' + e.message + '\n' + e.stack + ', reversion error: ' + etwo.message + '\n' + etwo.stack + '\n----'); - } - if (callback) callback(err); - else throw err; + return; } -} + var handler = platform_modules[platform]; -module.exports = function installPlugin(platform, project_dir, name, plugins_dir, cli_variables, callback) { - if (!platform_modules[platform]) { - var err = new Error(platform + " not supported."); - if (callback) { - callback(err); - return; - } - else throw err; - } + // TODO: check if platform has plugin installed already. - var plugin_dir = path.join(plugins_dir, name); - var plugin_xml_path = path.join(plugin_dir, 'plugin.xml'); + // parse plugin.xml into transactions + var txs = []; + var sourceFiles = platformTag.findall('./source-file'), + libFiles = platformTag.findall('./library-file'), + headerFiles = platformTag.findall('./header-file'), + resourceFiles = platformTag.findall('./resource-file'), + assets = plugin_et.findall('./asset'), + frameworks = platformTag.findall('./framework'), + configChanges = platformTag.findall('./config-file'); - // Check that the plugin has already been fetched. - if (!fs.existsSync(plugin_dir) || !fs.existsSync(plugin_xml_path)) { - // try querying the plugin database - plugins.getPluginInfo(plugin_dir, - function(err, plugin_info) { - if (err) { - var err_obj = new Error('install failed. plugin.xml at "' + plugin_xml_path + '" not found and couldnt query remote server for information about the plugin because ' + err.message); - if (callback) callback(err_obj); - else throw err_obj; + txs = txs.concat(sourceFiles, libFiles, headerFiles, resourceFiles, frameworks, configChanges, assets); + + // pass platform-specific transactions into install + handler.install(txs, plugin_id, project_dir, plugin_dir, filtered_variables, function(err) { + if (err) { + // FAIL + handler.uninstall(err.transactions.executed, plugin_id, project_dir, plugin_dir, function(superr) { + var issue = ''; + if (superr) { + // Even reversion failed. super fail. + issue = 'Install failed, then reversion of installation failed. Sorry :(. Instalation issue: ' + err.message + ', reversion issue: ' + superr.message; } else { - plugins.clonePluginGitRepo(plugin_info.url, plugins_dir, function(err, plugin_dir) { - if (err) { - var err_obj = new Error('install failed. plugin.xml at "' + plugin_xml_path + '" not found and couldnt clone plugin repo because ' + err.message); - if (callback) callback(err_obj); - else throw err_obj; - } else { - runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback); - } - }); + issue = 'Install failed, plugin reversion successful so you should be good to go. Installation issue: ' + err.message; } + if (callback) callback(issue); + else console.error(issue); + }); + } else { + // WIN! + // call prepare after a successful install + require('./../plugman').prepare(project_dir, platform, plugins_dir); + + // Log out plugin INFO element contents in case additional install steps are necessary + if((info = platformTag.findAll('./info'))) { + console.log(info.text); } - ); - } else { - runInstall(platform, project_dir, plugin_dir, plugins_dir, cli_variables, callback); - } -}; + if (callback) callback(); + } + }); +} http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/src/platforms/android.js ---------------------------------------------------------------------- diff --git a/src/platforms/android.js b/src/platforms/android.js index 90a828a..0914165 100644 --- a/src/platforms/android.js +++ b/src/platforms/android.js @@ -22,6 +22,7 @@ var fs = require('fs') // use existsSync in 0.6.x , util = require('util') , shell = require('shelljs') , et = require('elementtree') + , common = require('./common') , getConfigChanges = require(path.join(__dirname, '..', 'util', 'config-changes')) , searchAndReplace = require(path.join(__dirname, '..', 'util', 'search-and-replace')) , xml_helpers = require(path.join(__dirname, '..', 'util', 'xml-helpers')) @@ -29,158 +30,100 @@ var fs = require('fs') // use existsSync in 0.6.x , sourceDir = 'src'; module.exports = { - handleInstall:function(project_dir, plugin_dir, plugin_et, variables) { - handlePlugin('install', project_dir, plugin_dir, plugin_et, variables); + install:function(transactions, plugin_id, project_dir, plugin_dir, variables, callback) { + handlePlugin('install', plugin_id, transactions, project_dir, plugin_dir, variables, callback); }, - handleUninstall:function(project_dir, plugin_dir, plugin_et, variables) { - handlePlugin('uninstall', project_dir, plugin_dir, plugin_et, variables); - }, - forceInstall:function(project_dir, plugin_dir, plugin_et, variables) { - handlePlugin('force-install', project_dir, plugin_dir, plugin_et, variables); - }, - forceUninstall:function(project_dir, plugin_dir, plugin_et, variables) { - handlePlugin('force-uninstall', project_dir, plugin_dir, plugin_et, variables); + uninstall:function(transactions, plugin_id, project_dir, plugin_dir, callback) { + handlePlugin('uninstall', plugin_id, transactions, project_dir, plugin_dir, null, callback); }, www_dir:function(project_dir) { return path.join(project_dir, 'assets', 'www'); } }; -function handlePlugin(action, project_dir, plugin_dir, plugin_et, variables) { - var plugin_id = plugin_et._root.attrib['id'] - , version = plugin_et._root.attrib['version'] - , external_hosts = [] - , i = 0 - , PACKAGE_NAME = androidPackageName(project_dir) - - - var platformTag = plugin_et.find('./platform[@name="android"]'); - if (!platformTag) { - // Either this plugin doesn't support this platform, or it's a JS-only plugin. - // Either way, return now. - return; - } - - var sourceFiles = platformTag.findall('./source-file') - , libFiles = platformTag.findall('./library-file') - , configChanges = getConfigChanges(platformTag); - - variables = variables || {} - - // get config.xml filename - var config_xml_filename = 'res/xml/config.xml'; - if(fs.existsSync(path.resolve(project_dir, 'res/xml/plugins.xml'))) { - config_xml_filename = 'res/xml/plugins.xml'; - } - - // collision detection - if(action.match(/force-/) == null) { - if(action == "install" && pluginInstalled(plugin_et, project_dir, config_xml_filename)) { - throw new Error("Plugin "+plugin_id+" already installed"); - } else if(action == "uninstall" && !pluginInstalled(plugin_et, project_dir, config_xml_filename)) { - throw new Error("Plugin "+plugin_id+" not installed"); - } - } else { - action = action.replace('force-', ''); - } +function handlePlugin(action, plugin_id, txs, project_dir, plugin_dir, variables, callback) { + var PACKAGE_NAME = androidPackageName(project_dir); + variables = variables || {}; + // TODO: adding access tags? + // TODO: move this to prepare? + /* var root = et.Element("config-file"); - root.attrib['parent'] = '.' - plugin_et.findall('./access').forEach(function (tag) { + root.attrib['parent'] = '.'; + plugin_et.findall('./access').forEach(function (tag) { root.append(tag); }); - - if (root.len()) { - (configChanges[config_xml_filename]) ? - configChanges[config_xml_filename].push(root) : - configChanges[config_xml_filename] = [root]; - } - - // find which config-files we're interested in - Object.keys(configChanges).forEach(function (configFile) { - if (!fs.existsSync(path.resolve(project_dir, configFile))) { - delete configChanges[configFile]; - } - }); - - // move source files - sourceFiles.forEach(function (sourceFile) { - - var srcDir = path.resolve(project_dir, - sourceFile.attrib['target-dir']) - , destFile = path.resolve(srcDir, - path.basename(sourceFile.attrib['src'])); - - if (action == 'install') { - shell.mkdir('-p', srcDir); - var srcFile = srcPath(plugin_dir, sourceFile.attrib['src']); - shell.cp(srcFile, destFile); - } else { - fs.unlinkSync(destFile); - // check if directory is empty - var curDir = srcDir; - while(curDir !== path.join(project_dir, 'src')) { - if(fs.readdirSync(curDir).length == 0) { - fs.rmdirSync(curDir); - curDir = path.resolve(path.join(curDir, '..')); - } else { - // directory not empty...do nothing + */ + var completed = []; + console.log(txs); + return; + while(txs.length) { + var mod = txs.shift(); + try { + switch(mod.tag.toLowerCase()) { + case 'source-file': + var destFile = path.join(mod.attrib['target-dir'], path.basename(mod.attrib['src'])); + + if (action == 'install') { + common.straightCopy(plugin_dir, mod.attrib['src'], project_dir, destFile); + } else { + common.deleteJava(project_dir, destFile); + } + break; + case 'library-file': + var destFile = path.join(mod.attrib['target-dir'], path.basename(mod.attrib['src'])); + + if (action == 'install') { + common.straightCopy(plugin_dir, mod.attrib['src'], project_dir, destFile); + } else { + fs.unlinkSync(path.resolve(project_dir, destFile)); + } + break; + case 'config-file': + // Only modify config files that exist. + var config_file = path.resolve(project_dir, mod.attrib['target']); + if (fs.existsSync(config_file)) { + var xmlDoc = xml_helpers.parseElementtreeSync(config_file); + var selector = mod.attrib["parent"]; + var children = mod.findall('*'); + + if( action == 'install') { + if (!xml_helpers.graftXML(xmlDoc, children, selector)) { + throw new Error('failed to add config-file children to "' + filename + '"'); + } + } else { + if (!xml_helpers.pruneXML(xmlDoc, children, selector)) { + throw new Error('failed to remove config-file children from "' + filename + '"'); + } + } + + var output = xmlDoc.write({indent: 4}); + fs.writeFileSync(config_file, output); + } + break; + case 'asset': + if (action == 'uninstall') { + var target = mod.attrib.target; + shell.rm('-rf', path.resolve(module.exports.www_dir(), target)); + shell.rm('-rf', path.resolve(module.exports.www_dir(), 'plugins', plugin_id)); + } + break; + default: + throw new Error('Unrecognized plugin.xml element/action in android installer: ' + mod.tag); break; - } - } - } - }) - - // move library files - libFiles.forEach(function (libFile) { - var libDir = path.resolve(project_dir, - libFile.attrib['target-dir']) - - if (action == 'install') { - shell.mkdir('-p', libDir); - var src = path.resolve(plugin_dir, - libFile.attrib['src']), - dest = path.resolve(libDir, - path.basename(libFile.attrib['src'])); - - shell.cp(src, dest); - } else { - var destFile = path.resolve(libDir, - path.basename(libFile.attrib['src'])); - - fs.unlinkSync(destFile); - // check if directory is empty - var files = fs.readdirSync(libDir); - if(files.length == 0) { - shell.rm('-rf', libDir); } + } catch(e) { + // propagate error up and provide completed tx log + e.transactions = { + executed:completed, + incomplete:txs.unshift(mod) + }; + if (callback) callback(e); + else throw e; + return; } - }) - - // edit configuration files - Object.keys(configChanges).forEach(function (filename) { - var filepath = path.resolve(project_dir, filename), - xmlDoc = xml_helpers.parseElementtreeSync(filepath), - output; - - configChanges[filename].forEach(function (configNode) { - var selector = configNode.attrib["parent"], - children = configNode.findall('*'); - - if( action == 'install') { - if (!xml_helpers.graftXML(xmlDoc, children, selector)) { - throw new Error('failed to add children to ' + filename); - } - } else { - if (!xml_helpers.pruneXML(xmlDoc, children, selector)) { - throw new Error('failed to remove children from' + filename); - } - } - }); - - output = xmlDoc.write({indent: 4}); - fs.writeFileSync(filepath, output); - }); + completed.push(mod); + } if (action == 'install') { variables['PACKAGE_NAME'] = androidPackageName(project_dir); @@ -188,25 +131,6 @@ function handlePlugin(action, project_dir, plugin_dir, plugin_et, variables) { searchAndReplace(path.resolve(project_dir, 'AndroidManifest.xml'), variables); } - // Remove all assets and JS modules installed by this plugin. - if (action == 'uninstall') { - var assets = plugin_et.findall('./asset'); - assets && assets.forEach(function(asset) { - var target = asset.attrib.target; - shell.rm('-rf', path.join(project_dir, assetsDir, target)); - }); - - shell.rm('-rf', path.join(project_dir, assetsDir, 'plugins', plugin_id)); - } -} - -// TODO: resolvePath, and should be used everywhere (even across platform impls) -function srcPath(pluginPath, filename) { - var file = path.resolve(pluginPath, filename); - if (!fs.existsSync(file)) { - throw new Error('Path "' + file + '" does not exist.'); - } - return file; } // reads the package name out of the Android Manifest file @@ -218,16 +142,3 @@ function androidPackageName(project_dir) { return mDoc._root.attrib['package']; } - -function pluginInstalled(plugin_et, project_dir, config_xml_filename) { - var tag_xpath = util.format('./platform[@name="android"]/config-file[@target="%s"]/plugin', config_xml_filename); - - var plugin_tag = plugin_et.find(tag_xpath); - if (!plugin_tag) { - return false; - } - var plugin_name = plugin_tag.attrib.name; - - return (fs.readFileSync(path.resolve(project_dir, config_xml_filename), 'utf8') - .match(new RegExp(plugin_name, "g")) != null); -} http://git-wip-us.apache.org/repos/asf/cordova-plugman/blob/5e1b502e/src/platforms/common.js ---------------------------------------------------------------------- diff --git a/src/platforms/common.js b/src/platforms/common.js index cd95d84..523b38a 100644 --- a/src/platforms/common.js +++ b/src/platforms/common.js @@ -1,12 +1,42 @@ - +var shell = require('shelljs'), + path = require('path'), + fs = require('fs'); module.exports = { // helper for resolving source paths from plugin.xml // throws File Not Found - resolveSrcPath:function() { + resolveSrcPath:function(plugin_dir, relative_path) { + var full_path = path.resolve(plugin_dir, relative_path); + if (!fs.existsSync(full_path)) throw new Error('"' + full_path + '" not found!'); + else return full_path; }, // helper for resolving target paths from plugin.xml into a cordova project // throws File Exists - resolveTargetPath:function() { + resolveTargetPath:function(project_dir, relative_path) { + var full_path = path.resolve(project_dir, relative_path); + if (fs.existsSync(full_path)) throw new Error('"' + full_path + '" already exists!'); + else return full_path; + }, + // Many times we simply need to copy shit over, knowing if a source path doesnt exist or if a target path already exists + straightCopy:function(plugin_dir, src, project_dir, dest) { + src = module.exports.resolveSrcPath(plugin_dir, src); + dest = module.exports.resolveTargetPath(project_dir, dest); + shell.mkdir('-p', path.dirname(dest)); + shell.cp('-r', src, dest); + }, + // Sometimes we want to remove some java, and prune any unnecessary empty directories + deleteJava:function(project_dir, destFile) { + fs.unlinkSync(destFile); + // check if directory is empty + var curDir = path.resolve(project_dir, path.basename(destFile)); + while(curDir !== path.resolve(project_dir, 'src')) { + if(fs.readdirSync(curDir).length == 0) { + fs.rmdirSync(curDir); + curDir = path.resolve(curDir, '..'); + } else { + // directory not empty...do nothing + break; + } + } } };
