blob: 4572f5219aef254722dbb3df0a79e440b72e5390 [file] [log] [blame]
var path = require('path'),
fs = require('fs'),
et = require('elementtree'),
shell= require('shelljs'),
config_changes = require('./util/config-changes'),
xml_helpers = require('./util/xml-helpers'),
action_stack = require('./util/action-stack'),
dependencies = require('./util/dependencies'),
underscore = require('underscore'),
Q = require('q'),
plugins = require('./util/plugins'),
underscore = require('underscore'),
events = require('./events'),
platform_modules = require('./platforms'),
plugman = require('../plugman');
// possible options: cli_variables, www_dir
// Returns a promise.
module.exports = function(platform, project_dir, id, plugins_dir, options) {
options = options || {};
options.is_top_level = true;
plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
// Allow path to file to grab an ID
var xml_path = path.join(id, 'plugin.xml');
if ( fs.existsSync(xml_path) ) {
var plugin_et = xml_helpers.parseElementtreeSync(xml_path),
id = plugin_et._root.attrib['id'];
}
return module.exports.uninstallPlatform(platform, project_dir, id, plugins_dir, options)
.then(function() {
return module.exports.uninstallPlugin(id, plugins_dir, options);
});
}
// Returns a promise.
module.exports.uninstallPlatform = function(platform, project_dir, id, plugins_dir, options) {
options = options || {};
options.is_top_level = true;
plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
if (!platform_modules[platform]) {
return Q.reject(new Error(platform + " not supported."));
}
var plugin_dir = path.join(plugins_dir, id);
if (!fs.existsSync(plugin_dir)) {
return Q.reject(new Error('Plugin "' + id + '" not found. Already uninstalled?'));
}
var current_stack = new action_stack();
return runUninstallPlatform(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
};
// Returns a promise.
module.exports.uninstallPlugin = function(id, plugins_dir, options) {
options = options || {};
var plugin_dir = path.join(plugins_dir, id);
// @tests - important this event is checked spec/uninstall.spec.js
events.emit('log', 'Removing "'+ id +'"');
// If already removed, skip.
if ( !fs.existsSync(plugin_dir) ) {
events.emit('verbose', 'Plugin "'+ id +'" already removed ('+ plugin_dir +')');
return Q();
}
var xml_path = path.join(plugin_dir, 'plugin.xml')
, plugin_et = xml_helpers.parseElementtreeSync(xml_path);
var doDelete = function(id) {
var plugin_dir = path.join(plugins_dir, id);
if ( !fs.existsSync(plugin_dir) ) {
events.emit('verbose', 'Plugin "'+ id +'" already removed ('+ plugin_dir +')');
return Q();
}
shell.rm('-rf', plugin_dir);
events.emit('verbose', 'Deleted "'+ id +'"');
};
// We've now lost the metadata for the plugins that have been uninstalled, so we can't use that info.
// Instead, we list all dependencies of the target plugin, and check the remaining metadata to see if
// anything depends on them, or if they're listed as top-level.
// If neither, they can be deleted.
var top_plugin_id = id;
// Recursively remove plugins which were installed as dependents (that are not top-level)
// optional?
var recursive = true;
var toDelete = recursive ? plugin_et.findall('dependency') : [];
toDelete = toDelete && toDelete.length ? toDelete.map(function(p) { return p.attrib.id; }) : [];
toDelete.push(top_plugin_id);
// Okay, now we check if any of these are depended on, or top-level.
// Find the installed platforms by whether they have a metadata file.
var platforms = Object.keys(platform_modules).filter(function(platform) {
return fs.existsSync(path.join(plugins_dir, platform + '.json'));
});
// Can have missing plugins on some platforms when not supported..
var dependList = {};
platforms.forEach(function(platform) {
var depsInfo = dependencies.generate_dependency_info(plugins_dir, platform);
var tlps = depsInfo.top_level_plugins,
deps, i;
// Top-level deps must always be explicitely asked to remove by user
tlps.forEach(function(plugin_id){
if(top_plugin_id == plugin_id)
return;
var i = toDelete.indexOf(plugin_id);
if(i >= 0)
toDelete.splice(i, 1);
});
toDelete.forEach(function(plugin) {
deps = dependencies.dependents(plugin, depsInfo);
var i = deps.indexOf(top_plugin_id);
if(i >= 0)
deps.splice(i, 1); // remove current/top-level plugin as blocking uninstall
if(deps.length) {
dependList[plugin] = deps.join(', ');
}
});
});
var i, plugin_id, msg;
for(i in toDelete) {
plugin_id = toDelete[i];
if( dependList[plugin_id] ) {
msg = '"' + plugin_id + '" is required by ('+ dependList[plugin_id] + ')';
if(options.force) {
events.emit('log', msg +' but forcing removal.');
} else {
// @tests - error and event message is checked spec/uninstall.spec.js
msg += ' and cannot be removed (hint: use -f or --force)';
if(plugin_id == top_plugin_id) {
return Q.reject( new Error(msg) );
} else {
events.emit('warn', msg +' and cannot be removed (hint: use -f or --force)');
continue;
}
}
}
doDelete(plugin_id);
}
return Q();
};
// possible options: cli_variables, www_dir, is_top_level
// Returns a promise
function runUninstallPlatform(actions, platform, project_dir, plugin_dir, plugins_dir, options) {
options = options || {};
var xml_path = path.join(plugin_dir, 'plugin.xml');
var plugin_et = xml_helpers.parseElementtreeSync(xml_path);
var plugin_id = plugin_et._root.attrib['id'];
// Deps info can be passed recusively
var depsInfo = options.depsInfo || dependencies.generate_dependency_info(plugins_dir, platform, 'remove');
// Check that this plugin has no dependents.
var dependents = dependencies.dependents(plugin_id, depsInfo, platform);
if(options.is_top_level && dependents && dependents.length > 0) {
var msg = "The plugin '"+ plugin_id +"' is required by (" + dependents.join(', ') + ")";
if(options.force) {
events.emit("info", msg + " but forcing removal");
} else {
return Q.reject( new Error(msg + ", skipping uninstallation.") );
}
}
// Check how many dangling dependencies this plugin has.
var deps = depsInfo.graph.getChain(plugin_id);
var danglers = dependencies.danglers(plugin_id, depsInfo, platform);
var promise;
if (deps && deps.length && danglers && danglers.length) {
// @tests - important this event is checked spec/uninstall.spec.js
events.emit('log', 'Uninstalling ' + danglers.length + ' dependent plugins.');
promise = Q.all(
danglers.map(function(dangler) {
var dependent_path = dependencies.resolvePath(dangler, plugins_dir);
var opts = underscore.extend({}, options, {
is_top_level: depsInfo.top_level_plugins.indexOf(dangler) > -1,
depsInfo: depsInfo
});
return runUninstallPlatform(actions, platform, project_dir, dependent_path, plugins_dir, opts);
})
);
} else {
promise = Q();
}
return promise.then(function() {
return handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, options.www_dir, plugins_dir, plugin_dir, options.is_top_level);
});
}
// Returns a promise.
function handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, www_dir, plugins_dir, plugin_dir, is_top_level) {
var platform_modules = require('./platforms');
var handler = platform_modules[platform];
var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
www_dir = www_dir || handler.www_dir(project_dir);
events.emit('log', 'Uninstalling ' + plugin_id + ' from ' + platform);
var assets = plugin_et.findall('./asset');
if (platformTag) {
var sourceFiles = platformTag.findall('./source-file'),
headerFiles = platformTag.findall('./header-file'),
libFiles = platformTag.findall('./lib-file'),
resourceFiles = platformTag.findall('./resource-file');
frameworkFiles = platformTag.findall('./framework[@custom="true"]');
assets = assets.concat(platformTag.findall('./asset'));
// queue up native stuff
sourceFiles && sourceFiles.forEach(function(source) {
actions.push(actions.createAction(handler["source-file"].uninstall,
[source, project_dir, plugin_id],
handler["source-file"].install,
[source, plugin_dir, project_dir, plugin_id]));
});
headerFiles && headerFiles.forEach(function(header) {
actions.push(actions.createAction(handler["header-file"].uninstall,
[header, project_dir, plugin_id],
handler["header-file"].install,
[header, plugin_dir, project_dir, plugin_id]));
});
resourceFiles && resourceFiles.forEach(function(resource) {
actions.push(actions.createAction(handler["resource-file"].uninstall,
[resource, project_dir, plugin_id],
handler["resource-file"].install,
[resource, plugin_dir, project_dir]));
});
// CB-5238 custom frameworks only
frameworkFiles && frameworkFiles.forEach(function(framework) {
actions.push(actions.createAction(handler["framework"].uninstall,
[framework, project_dir, plugin_id],
handler["framework"].install,
[framework, plugin_dir, project_dir]));
});
libFiles && libFiles.forEach(function(source) {
actions.push(actions.createAction(handler["lib-file"].uninstall,
[source, project_dir, plugin_id],
handler["lib-file"].install,
[source, plugin_dir, project_dir, plugin_id]));
});
}
// queue up asset installation
var common = require('./platforms/common');
assets && assets.forEach(function(asset) {
actions.push(actions.createAction(common.asset.uninstall, [asset, www_dir, plugin_id], common.asset.install, [asset, plugin_dir, www_dir]));
});
// run through the action stack
return actions.process(platform, project_dir)
.then(function() {
// WIN!
events.emit('verbose', plugin_id + ' uninstalled from ' + platform + '.');
// queue up the plugin so prepare can remove the config changes
config_changes.add_uninstalled_plugin_to_prepare_queue(plugins_dir, plugin_id, platform, is_top_level);
// call prepare after a successful uninstall
plugman.prepare(project_dir, platform, plugins_dir, www_dir, is_top_level);
});
}