diff --git a/tools/commands-cordova.js b/tools/commands-cordova.js index cfb089a36d..80ad510622 100644 --- a/tools/commands-cordova.js +++ b/tools/commands-cordova.js @@ -122,12 +122,12 @@ var generateCordovaBoilerplate = function (clientDir, options) { }; var fetchCordovaPluginFromShaUrl = - function (urlWithSha, localPluginsDir, pluginName) { + function (urlWithSha, localPluginsDir, pluginName) { var pluginPath = path.join(localPluginsDir, pluginName); var pluginTarballPath = pluginPath + '.tgz'; var execFileSync = require('./utils.js').execFileSync; - var whichCurl = execFileSync('which curl'); + var whichCurl = execFileSync('which', ['curl']); var downloadProcess = null; @@ -252,13 +252,74 @@ cordova.ensureCordovaPlatforms = function (localPath) { return true; }; + +var installPlugin = function (cordovaPath, name, version, settings) { + // XXX do something different for plugins fetched from a url. + var pluginInstallCommand = version ? name + '@' + version : name; + var localPluginsPath = localPluginsPathFromCordovaPath(cordovaPath); + + if (version && utils.isUrlWithSha(version)) { + pluginInstallCommand = + fetchCordovaPluginFromShaUrl(version, localPluginsPath, name); + } + + var additionalArgs = []; + // XXX how do we get settings to work now? Do we require settings to be + // passed every time we add a plugin? + + if (settings && ! _.isObject(settings)) + throw new Error('Meteor.settings.cordova.' + name + + ' is expected to be an object'); + + _.each(settings, function (value, variable) { + additionalArgs.push('--variable'); + additionalArgs.push(variable + '=' + JSON.stringify(value)); + }); + + process.stdout.write('Installing ' + pluginInstallCommand + '\n'); + var execRes = execFileSyncOrThrow(localCordova, + ['plugin', 'add', pluginInstallCommand].concat(additionalArgs), + { cwd: cordovaPath }); + if (! execRes.success) + throw new Error("Failed to install plugin " + name + ": " + execRes.stderr); +}; + +var uninstallPlugin = function (cordovaPath, name) { + try { + execFileSyncOrThrow(localCordova, ['plugin', 'rm', name], + { cwd: cordovaPath }); + } catch (err) { + // Catch when an uninstall fails, because it might just be a dependency + // issue. For example, plugin A depends on plugin B and we try to remove + // plugin B. In this case, we will loop and remove plugin A first. + } +}; + +// Returns the list of installed plugins as a hash from plugin name to version. +var getInstalledPlugins = function (cordovaPath) { + var installedPlugins = {}; + + var pluginsOutput = execFileSyncOrThrow(localCordova, ['plugin', 'list'], + { cwd: cordovaPath }).stdout; + // Check if there are any plugins + if (! pluginsOutput.match(/No plugins added/)) { + _.each(pluginsOutput.split('\n'), function (line) { + line = line.trim(); + if (line === '') + return; + var plugin = line.split(' ')[0]; + var version = line.split(' ')[1]; + installedPlugins[plugin] = version; + }); + } + + return installedPlugins; +}; + // Ensures that the Cordova platforms are synchronized with the app-level // platforms. -// options -// - packagePlugins: the list of plugins required by packages. If not defined, -// we bundle the app to find the required plugins. -cordova.ensureCordovaPlugins = function (localPath, options) { +var ensureCordovaPlugins = function (localPath, options) { options = options || {}; var plugins = options.packagePlugins; if (! plugins) { @@ -275,101 +336,93 @@ cordova.ensureCordovaPlugins = function (localPath, options) { _.extend(plugins, project.getCordovaPlugins()); var cordovaPath = path.join(localPath, 'cordova-build'); - var localPluginsPath = localPluginsPathFromCordovaPath(cordovaPath); - var newSettings = null; + var settingsFile = path.join(cordovaPath, 'cordova-settings.json'); + var newSettings; if (options.settings) { newSettings = JSON.parse(fs.readFileSync(options.settings, "utf8")).cordova; - } - - - // XXX compare the latest used sha's with the currently required sha's for - // plugins fetched from a github/tarball url. - var pluginsOutput = execFileSyncOrThrow(localCordova, ['plugin', 'list'], - { cwd: cordovaPath }).stdout; - - var installedPlugins = {}; - // Check if there are any plugins - if (! pluginsOutput.match(/No plugins added/)) { - _.each(pluginsOutput.split('\n'), function (line) { - line = line.trim(); - if (line === '') - return; - var plugin = line.split(' ')[0]; - var version = line.split(' ')[1]; - installedPlugins[plugin] = version; - }); + fs.writeFileSync(settingsFile, JSON.stringify(newSettings, null, 2), + 'utf8'); } var oldSettings; - var settingsFile = path.join(cordovaPath, 'cordova-settings.json'); try { - oldSettings = JSON.parse( - fs.readFileSync(settingsFile, 'utf8')); + oldSettings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch(err) { if (err.code !== 'ENOENT') throw err; oldSettings = {}; - } finally { - if (newSettings) { - fs.writeFileSync(settingsFile, JSON.stringify(newSettings, null, 2), - 'utf8'); - } } - // This block checks to see if we should install or reinstall a plugin. + // XXX compare the latest used sha's with the currently required sha's for + // plugins fetched from a github/tarball url. + + var installedPlugins = getInstalledPlugins(cordovaPath); + + // Due to the dependency structure of Cordova plugins, it is impossible to + // upgrade the version on an individual Cordova plugin. Instead, whenever a + // new Cordova plugin is added or removed, or its version is changed, + // we just reinstall all of the plugins. + + // If there are Cordova settings and they have changed, then reinstall + // all of the plugins. + var shouldReinstallPlugins = newSettings && + ! _.isEqual(newSettings, oldSettings); + + // If we have newSettings then use them, otherwise use the old settings. + var settings = (newSettings ? newSettings : oldSettings) || {}; + + // Iterate through all of the plugin and find if any of them have a new + // version. _.each(plugins, function (version, name) { - // no-op if this plugin is already installed // XXX there is a hack here that never updates a package if you are // trying to install it from a URL, because we can't determine if // it's the right version or not - if (_.has(installedPlugins, name) && - (installedPlugins[name] === version || utils.isUrlWithSha(version))) { - - if (newSettings && newSettings[name] && - ! _.isEqual(oldSettings[name], newSettings[name])) { - // If we have newSettings and they are different, then continue. - } else { - return; - } + if (! _.has(installedPlugins, name) || + (installedPlugins[name] !== version && ! utils.isUrlWithSha(version))) { + // The version of the plugin has changed, or we do not contain a plugin. + shouldReinstallPlugins = true; } - - if (_.has(installedPlugins, name)) { - execFileSyncOrThrow(localCordova, ['plugin', 'rm', name], - { cwd: cordovaPath }); - } - - // XXX do something different for plugins fetched from a url. - var pluginInstallCommand = version ? name + '@' + version : name; - - if (version && utils.isUrlWithSha(version)) { - pluginInstallCommand = - fetchCordovaPluginFromShaUrl(version, localPluginsPath, name); - } - - var additionalArgs = []; - // XXX how do we get settings to work now? Do we require settings to be - // passed every time we add a plugin? - if (newSettings && newSettings[name]) { - if (! _.isObject(newSettings[name])) - throw new Error('Meteor.settings.cordova.' + name + ' is expected to be an object'); - _.each(newSettings[name], function (value, variable) { - additionalArgs.push('--variable'); - additionalArgs.push(variable + '=' + JSON.stringify(value)); - }); - } - process.stdout.write('Installing ' + pluginInstallCommand + '\n'); - var execRes = execFileSyncOrThrow(localCordova, - ['plugin', 'add', pluginInstallCommand].concat(additionalArgs), { cwd: cordovaPath }); - if (! execRes.success) - throw new Error("Failed to install plugin " + name + ": " + execRes.stderr); }); + // Check to see if we have any installed plugins that are not in the current + // set of plugins. _.each(installedPlugins, function (version, name) { - if (! _.has(plugins, name)) - execFileSyncOrThrow(localCordova, ['plugin', 'rm', name], { cwd: cordovaPath }); + if (! _.has(plugins, name)) { + shouldReinstallPlugins = true; + } }); + + if (shouldReinstallPlugins) { + // Loop through all of the current plugins and remove them one by one until + // we have no plugins. It's necessary to loop because we might have + // dependencies between plugins. + var uninstallAllPlugins = function () { + process.stdout.write("Uninstalling all Cordova plugins...\n"); + installedPlugins = getInstalledPlugins(cordovaPath); + while (_.size(installedPlugins)) { + _.each(_.keys(installedPlugins), function (name) { + uninstallPlugin(cordovaPath, name); + }); + installedPlugins = getInstalledPlugins(cordovaPath); + } + }; + uninstallAllPlugins(); + + // Now install all of the plugins. + try { + _.each(plugins, function (version, name) { + installPlugin(cordovaPath, name, version, settings[name]); + }); + } catch (err) { + // If a plugin fails to install, then remove all plugins and throw the + // error. Cordova doesn't remove the plugin by default for some reason. + // XXX don't throw and improve this error message. + uninstallAllPlugins(); + throw err; + } + } }; // Build a Cordova project, creating a Cordova project if necessary. @@ -388,7 +441,7 @@ var buildCordova = function (localPath, buildCommand, options) { cordova.ensureCordovaProject(localPath, options.appName); cordova.ensureCordovaPlatforms(localPath); - cordova.ensureCordovaPlugins(localPath, _.extend({}, options, { + ensureCordovaPlugins(localPath, _.extend({}, options, { packagePlugins: bundle.starManifest.cordovaDependencies })); diff --git a/tools/tests/cordova-plugins.js b/tools/tests/cordova-plugins.js index 8b37bf2082..9131eedada 100644 --- a/tools/tests/cordova-plugins.js +++ b/tools/tests/cordova-plugins.js @@ -155,12 +155,15 @@ selftest.define("add cordova plugins", function () { run = s.run("add", "cordova:org.apache.cordova.file"); run.matchErr("Must declare exact version"); + // The current behavior doesn't fail if a plugin is not in the registry until + // build time. run = s.run("add", "cordova:foo@1.0.0"); run.waitSecs(5); - // should fail to download the package as 'foo' doesn't exist - // printing the error message from cordova - run.matchErr("Error"); - run.expectExit(1); + run.match("added cordova plugin foo"); + + run = s.run("remove", "cordova:foo"); + run.waitSecs(5); + run.match("removed cordova plugin foo"); checkUserPlugins(s, ["org.apache.cordova.camera"]); @@ -175,12 +178,11 @@ selftest.define("add cordova plugins", function () { run = s.run("list-platforms"); run.match("android"); - run = s.run("bundle", "../a", "--android-path", "../android", - "--directory", "--debug", "--settings", "settings.json"); + run = s.run("build", "a", "--android-path", "../android", + "--settings", "settings.json"); run.waitSecs(30); - // This command fails because the FB plugin does not compile on - // Android without additional setup steps which we do not support - // at the moment. + // This fails because the FB plugin does not compile without additional + // configuration for android. run.expectExit(8); checkCordovaPlugins(s, @@ -194,8 +196,8 @@ selftest.define("add cordova plugins", function () { run = s.run("remove", "contains-cordova-plugin"); run.match("removed"); - run = s.run("bundle", "../a", "--android-path", "../android", - "--directory", "--debug", "--settings", "settings.json"); + run = s.run("build", "a", "--android-path", "../android", + "--settings", "settings.json"); run.waitSecs(30); run.expectExit(0); @@ -203,6 +205,13 @@ selftest.define("add cordova plugins", function () { "org.apache.cordova.console"]); run = s.run("remove", "cordova:org.apache.cordova.camera"); + run.match("removed"); run.expectExit(0); + + run = s.run("build", "a", "--android-path", "../android", + "--settings", "settings.json"); + run.waitSecs(30); + run.expectExit(0); + checkCordovaPlugins(s, ["org.apache.cordova.console"]); });