diff --git a/tools/commands-cordova.js b/tools/commands-cordova.js index 5e27aebb48..ecf8c4a4f3 100644 --- a/tools/commands-cordova.js +++ b/tools/commands-cordova.js @@ -396,12 +396,10 @@ var ensureCordovaProject = function (projectContext, appName) { // --- Cordova platforms --- -// Ensures that the Cordova platforms are synchronized with the app-level -// platforms. -var ensureCordovaPlatforms = function (projectContext) { - verboseLog('Ensuring that platforms in cordova build project are in sync'); +// Get the currently installed platforms from cordova build +var getCordovaInstalledPlatforms = function(projectContext) { var cordovaPath = projectContext.getProjectLocalDirectory('cordova-build'); - var platforms = projectContext.platformList.getCordovaPlatforms(); + var platformsList = execFileSyncOrThrow( localCordova, ['platform', 'list'], { cwd: cordovaPath, env: buildCordovaEnv() }); @@ -415,9 +413,18 @@ var ensureCordovaPlatforms = function (projectContext) { throw new Error('Failed to parse the output of `cordova platform list`: ' + platformsList.stdout); - var installedPlatforms = _.map(platformsStrings.split(', '), function (s) { + return installedPlatforms = _.map(platformsStrings.split(', '), function (s) { return s.split(' ')[0]; }); +} + +// Ensures that the Cordova platforms are synchronized with the app-level +// platforms. +var ensureCordovaPlatforms = function (projectContext) { + verboseLog('Ensuring that platforms in cordova build project are in sync'); + var cordovaPath = projectContext.getProjectLocalDirectory('cordova-build'); + var platforms = projectContext.platformList.getCordovaPlatforms(); + var installedPlatforms = getCordovaInstalledPlatforms(projectContext); _.each(platforms, function (platform) { if (_.contains(installedPlatforms, platform)) @@ -464,8 +471,16 @@ var targetsToPlatforms = cordova.targetsToPlatforms = function (targets) { var installPlugin = function (cordovaPath, name, version, conf) { verboseLog('Installing a plugin', name, version); + var pluginInstallCommand; + + if (utils.isUrlWithFileUri(version)) { + // Strip file:// + pluginInstallCommand = version.substr(7); + } else { + pluginInstallCommand = version ? name + '@' + version : name; + } + // 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)) { @@ -559,7 +574,7 @@ var writeTarballPluginsLock = function (cordovaPath, tarballPluginsLock) { // Returns the list of installed plugins as a hash from plugin name to version. var getInstalledPlugins = function (cordovaPath) { - verboseLog('Getting installed plugins for project'); + verboseLog('Getting installed plugins for project in ' + cordovaPath); var installedPlugins = {}; var pluginsOutput = execFileSyncOrThrow(localCordova, ['plugin', 'list'], @@ -614,17 +629,30 @@ var ensureCordovaPlugins = function (projectContext, options) { // 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. + // we just reinstall all of the plugins. Additionally if there are any plugins + // added from the local path, we will reinstall them just to be sure they + // are up to date since we do not track changes in their sources. - var shouldReinstallPlugins = false; + var shouldReinstallPlugins = false, + shouldReinstallPluginsFromLocalPaths = false, + pluginsFromLocalPath = {}; - // Iterate through all of the plugin and find if any of them have a new - // version. + // Iterate through all of the plugins and find if any of them have a new + // version. Additionally check if we have plugins installed from local path. + var pluginFromLocalPath; _.each(plugins, function (version, name) { + // Check if plugin is installed from local path + pluginFromLocalPath = utils.isUrlWithFileUri(version); + if (pluginFromLocalPath) { + shouldReinstallPluginsFromLocalPaths = true; + pluginsFromLocalPath[name] = version; + } + // 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) { + if (! _.has(installedPlugins, name) || + (installedPlugins[name] !== version && !pluginFromLocalPath)) { // The version of the plugin has changed, or we do not contain a plugin. shouldReinstallPlugins = true; } @@ -638,37 +666,63 @@ var ensureCordovaPlugins = function (projectContext, options) { } }); - if (shouldReinstallPlugins) { + if (shouldReinstallPluginsFromLocalPaths) + verboseLog('Reinstalling cordova plugins added from the local path'); + + if (shouldReinstallPlugins || shouldReinstallPluginsFromLocalPaths) { // 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 () { + // we have deleted proper amount of plugins. It's necessary to loop because + // we might have dependencies between plugins. + var uninstallPlugins = function (pluginsToUninstall, clearPluginsDirectory) { installedPlugins = getInstalledPlugins(cordovaPath); - while (_.size(installedPlugins)) { - _.each(installedPlugins, function (version, name) { - uninstallPlugin(cordovaPath, name, utils.isUrlWithSha(version)); + var uninstalled = 1; + // Detect if we couldn't delete any more plugins just to avoid + // hanging forever + while (uninstalled.length !== 0 && _.size(pluginsToUninstall)) { + _.each(pluginsToUninstall, function (version, name) { + uninstallPlugin(cordovaPath, name, utils.isUrlWithSha(version)); }); installedPlugins = getInstalledPlugins(cordovaPath); + + uninstalled = _.difference( + Object.keys(pluginsToUninstall), Object.keys(installedPlugins) + ); + pluginsToUninstall = _.omit(pluginsToUninstall, uninstalled); } // XXX HACK, because Cordova doesn't properly clear its plugins on `rm`. // This will completely destroy the project state. We should work with // Cordova to fix the bug in their system, because it doesn't seem // like there's a way around this. - files.rm_recursive(files.pathJoin(cordovaPath, 'platforms')); + if (clearPluginsDirectory) + files.rm_recursive(files.pathJoin(cordovaPath, 'platforms')); + ensureCordovaPlatforms(projectContext); }; buildmessage.enterJob({ title: "installing Cordova plugins"}, function () { - uninstallAllPlugins(); + installedPlugins = getInstalledPlugins(cordovaPath); + if (shouldReinstallPlugins) + uninstallPlugins(installedPlugins, true); + else + uninstallPlugins(pluginsFromLocalPath, false); - // Now install all of the plugins. + // Now install necessary plugins. try { // XXX: forkJoin with parallel false? - var pluginsInstalled = 0; + var pluginsInstalled, pluginsToInstall; + + if (shouldReinstallPlugins) { + pluginsInstalled = 0; + pluginsToInstall = plugins; + } else { + pluginsInstalled = _.size(installedPlugins); + pluginsToInstall = pluginsFromLocalPath; + } var pluginsCount = _.size(plugins); + buildmessage.reportProgress({ current: 0, end: pluginsCount }); - _.each(plugins, function (version, name) { + _.each(pluginsToInstall, function (version, name) { installPlugin(cordovaPath, name, version, pluginsConfiguration[name]); buildmessage.reportProgress({ @@ -680,7 +734,7 @@ var ensureCordovaPlugins = function (projectContext, options) { // 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(); + uninstallPlugins(getInstalledPlugins(cordovaPath), true); throw err; } }); diff --git a/tools/tests/apps/package-tests/packages/empty-cordova-plugin/package.js b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/package.js new file mode 100644 index 0000000000..e7c55aa499 --- /dev/null +++ b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/package.js @@ -0,0 +1,10 @@ +Package.describe({ + version: "1.0.0", + summary: "contains a empty cordova plugin" +}); + +Package.onUse(function(api) { + Cordova.depends({ + 'com.cordova.empty': 'file://../../../../cordova-local-plugin' + }); +}); diff --git a/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/plugin.xml b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/plugin.xml new file mode 100755 index 0000000000..397ef6b5d6 --- /dev/null +++ b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/plugin.xml @@ -0,0 +1,24 @@ + + + Empty + Cordova Empty Plugin + cordova,empty + https://github.com + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty.java b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty.java new file mode 100755 index 0000000000..84fcd04dec --- /dev/null +++ b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty.java @@ -0,0 +1,17 @@ +package com.cordova.empty; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; + +import org.json.JSONArray; +import org.json.JSONException; + +public class Empty extends CordovaPlugin { + public Object onMessage(String id, Object data) { + return null; + } + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + return false; + } +} diff --git a/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty_changed.java b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty_changed.java new file mode 100755 index 0000000000..2d4af561b2 --- /dev/null +++ b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/src/android/Empty_changed.java @@ -0,0 +1,18 @@ +package com.cordova.empty; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; + +import org.json.JSONArray; +import org.json.JSONException; + +public class Empty extends CordovaPlugin { + public Object onMessage(String id, Object data) { + System.out.println("change"); + return null; + } + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + return false; + } +} diff --git a/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/www/Empty.js b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/www/Empty.js new file mode 100755 index 0000000000..fa9262448c --- /dev/null +++ b/tools/tests/apps/package-tests/packages/empty-cordova-plugin/plugin/www/Empty.js @@ -0,0 +1,3 @@ +var Empty = { +}; +module.exports = Empty; diff --git a/tools/tests/cordova-plugins.js b/tools/tests/cordova-plugins.js index 473afcd908..1cf4ea4daa 100644 --- a/tools/tests/cordova-plugins.js +++ b/tools/tests/cordova-plugins.js @@ -17,17 +17,15 @@ var copyFile = function(from, to, sand) { sand.write(to, contents); }; - var localCordova = files.pathJoin(files.getCurrentToolsDir(), "tools", "cordova-scripts", "cordova.sh"); + + // Given a sandbox, that has the app as its currend cwd, read the versions file -// and check that it contains the plugins that we are looking for. We don't -// check the order, we just want to make sure that the right dependencies are -// in. +// and read the plugins list. // // sand: a sandbox, that has the main app directory as its cwd. -// plugins: an array of plugins in order. -var checkCordovaPlugins = selftest.markStack(function(sand, plugins) { +var getCordovaPluginsList = function(sand) { var lines = selftest.execFileSync(localCordova, ['plugins'], { cwd: files.pathJoin(sand.cwd, '.meteor', 'local', 'cordova-build'), @@ -38,12 +36,24 @@ var checkCordovaPlugins = selftest.markStack(function(sand, plugins) { if (lines[0].match(/No plugins/)) { lines = []; } - lines.sort(); + return lines; +} + +// Given a sandbox, that has the app as its currend cwd, read the versions file +// and check that it contains the plugins that we are looking for. We don't +// check the order, we just want to make sure that the right dependencies are +// in. +// +// sand: a sandbox, that has the main app directory as its cwd. +// plugins: an array of plugins in order. +var checkCordovaPlugins = selftest.markStack(function(sand, plugins) { + var cordovaPlugins = getCordovaPluginsList(sand); + plugins = _.clone(plugins).sort(); var i = 0; - _.each(lines, function(line) { + _.each(cordovaPlugins, function(line) { if (!line || line === '') return; // XXX should check for the version as well? selftest.expectEqual(line.split(' ')[0], plugins[i]); @@ -52,6 +62,16 @@ var checkCordovaPlugins = selftest.markStack(function(sand, plugins) { selftest.expectEqual(plugins.length, i); }); +// Like the function above but only looks if a certain plugin is on the list +var checkCordovaPluginExists = selftest.markStack(function(sand, plugin) { + var cordovaPlugins = getCordovaPluginsList(sand); + var found = false; + cordovaPlugins = cordovaPlugins.map(function (line) { + if (line && line !== '') return line.split(' ')[0]; + }); + selftest.expectTrue(_.contains(cordovaPlugins, plugin)); +}); + // Given a sandbox, that has the app as its cwd, read the cordova plugins // file and check that it contains exactly the plugins specified, in order. // @@ -131,7 +151,6 @@ selftest.define("change cordova plugins", ["cordova"], function () { run.match("restarted"); }); - // Add plugins through the command line, and make sure that the correct set of // changes is reflected in .meteor/packages, .meteor/versions and list selftest.define("add cordova plugins", ["slow", "cordova"], function () { @@ -194,7 +213,7 @@ selftest.define("add cordova plugins", ["slow", "cordova"], function () { run.match("android"); run = s.run("build", "../a", "--server", "localhost:3000"); - run.waitSecs(30); + run.waitSecs(60); // This fails because the FB plugin does not compile without additional // configuration for android. run.expectExit(1); @@ -232,6 +251,35 @@ selftest.define("add cordova plugins", ["slow", "cordova"], function () { run.waitSecs(60); run.expectExit(0); checkCordovaPlugins(s, ["org.apache.cordova.device"]); + + run = s.run("remove", "cordova:org.apache.cordova.device"); + run.match("removed"); + run.expectExit(0); + + run = s.run("add", "cordova:com.example.plugin@file://"); + run.matchErr("exact version of dependency"); + run.expectExit(1); + + run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory"); + run.waitSecs(5); + run.match("added cordova plugin com.example.plugin"); + run.expectExit(0); + + checkUserPlugins(s, ["com.example.plugin"]); + + // This should fail beacuse the plugin does not exists at the specified path + run = s.run("build", "../a", "--server", "localhost:3000"); + run.waitSecs(30); + run.expectExit(1); + + checkCordovaPlugins(s, []); + + // Add a package with Cordova.depends with local plugin (added from path) + run = s.run("add", "empty-cordova-plugin"); + run.match("added,"); + run.match("contains a empty cordova plugin"); + run.expectExit(0); + }); selftest.define("remove cordova plugins", function () { @@ -260,6 +308,19 @@ selftest.define("remove cordova plugins", function () { run.match("removed"); run.expectExit(1); checkUserPlugins(s, []); + + run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory"); + run.waitSecs(5); + run.match("added cordova plugin com.example.plugin"); + run.expectExit(0); + checkUserPlugins(s, ["com.example.plugin"]); + + run = s.run("remove", "cordova:com.example.plugin"); + run.waitSecs(5); + run.match("removed"); + run.expectExit(0); + checkUserPlugins(s, []); + }); selftest.define("meteor exits when cordova platforms change", ["slow", "cordova"], function () { @@ -328,6 +389,115 @@ selftest.define("meteor exits when cordova platforms change", ["slow", "cordova" run.expectExit(254); }); +selftest.define("meteor reinstalls only local cordova plugins on consecutive builds/runs", ["slow", "cordova"], function () { + var s = new Sandbox(); + var run; + + s.createApp("myapp", "package-tests"); + s.cd("myapp"); + + run = s.run("add-platform", "android"); + run.match("Do you agree"); + run.write("Y\n"); + run.waitSecs(90); + run.match("added platform"); + + var + pluginPath = "../cordova-local-plugin", + pluginSource = "packages/empty-cordova-plugin/plugin", + androidPluginSource = ".meteor/local/cordova-build/platforms/android/src"; + + + // Copy fake cordova plugin to ../cordova-local-plugin + s.mkdir(pluginPath); + s.cp(pluginSource + '/plugin.xml', pluginPath + '/plugin.xml'); + s.mkdir(pluginPath + '/www'); + s.mkdir(pluginPath + '/src'); + s.mkdir(pluginPath + '/src/android'); + s.cp(pluginSource + '/www/Empty.js', pluginPath +'/www/Empty.js'); + s.cp( + pluginSource + '/src/android/Empty.java', + pluginPath + '/src/android/Empty.java' + ); + + // Add the local cordova plugin + run = s.run("add", "cordova:com.cordova.empty@file://../../../../cordova-local-plugin"); + run.waitSecs(60); + run.match("added cordova plugin com.cordova.empty"); + run.expectExit(0); + + checkUserPlugins(s, ["com.cordova.empty"]); + + // Run meteor and check if the cordova android build have the plugin file. + // Using "android-device" because "android" would start the simulator. + run = s.run("run", "android-device"); + run.waitSecs(60); + run.match("Started your app"); + run.stop(); + + selftest.expectTrue( + s.read( + androidPluginSource + "/com/cordova/empty/Empty.java" + ).indexOf('change') === -1 + ); + selftest.expectTrue( + s.read( + androidPluginSource + "/com/cordova/empty/Empty.java" + ).indexOf('CordovaPlugin') > -1 + ); + + // Copy changed file to the plugin + s.cp( + pluginSource + '/src/android/Empty_changed.java', + pluginPath + '/src/android/Empty.java' + ); + + // Check if the local plugin will be refreshed + run = s.run("run", "android-device"); + run.waitSecs(60); + run.match("Started your app"); + run.stop(); + + selftest.expectTrue( + s.read( + androidPluginSource + "/com/cordova/empty/Empty.java" + ).indexOf('change') > -1 + ); + + // Now test the same scenario but with builds + s.cp( + pluginSource + '/src/android/Empty.java', + pluginPath + '/src/android/Empty.java' + ); + + run = s.run("build", "../a", "--server", "localhost:3000"); + run.waitSecs(60); + run.expectExit(0); + + selftest.expectTrue( + s.read( + "../a/android/project/src/com/cordova/empty/Empty.java" + ).indexOf('change') === -1 + ); + + checkCordovaPluginExists(s, "com.cordova.empty"); + + s.cp( + pluginSource + '/src/android/Empty_changed.java', + pluginPath + '/src/android/Empty.java' + ); + + run = s.run("build", "../a", "--server", "localhost:3000"); + run.waitSecs(60); + run.expectExit(0); + + selftest.expectTrue( + s.read( + "../a/android/project/src/com/cordova/empty/Empty.java" + ).indexOf('change') > -1 + ); +}); + selftest.define("meteor exits when cordova plugins change", ["slow", "cordova"], function () { var s = new Sandbox(); var run; diff --git a/tools/utils.js b/tools/utils.js index f9e2fb97b2..b41b3dfc67 100644 --- a/tools/utils.js +++ b/tools/utils.js @@ -466,6 +466,10 @@ exports.generateSubsetsOfIncreasingSize = function (total, cb) { } }; +exports.isUrlWithFileUri = function (x) { + return /^file:\/\/.+/.test(x); +}; + exports.isUrlWithSha = function (x) { // For now, just support http/https, which is at least less restrictive than // the old "github only" rule. @@ -491,7 +495,8 @@ exports.ensureOnlyExactVersions = function (dependencies) { }); }; exports.isExactVersion = function (version) { - return semver.valid(version) || exports.isUrlWithSha(version); + return semver.valid(version) || exports.isUrlWithSha(version) + || exports.isUrlWithFileUri(version); };