diff --git a/tools/cli/commands-cordova.js b/tools/cli/commands-cordova.js index bd1aa5e578e..5ca9a0c68e4 100644 --- a/tools/cli/commands-cordova.js +++ b/tools/cli/commands-cordova.js @@ -6,8 +6,8 @@ import { ProjectContext, PlatformList } from '../project-context.js'; import buildmessage from '../utils/buildmessage.js'; import files from '../fs/files.js'; -import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized, checkPlatformRequirements } from '../cordova/platforms.js'; -import { createCordovaProjectIfNecessary } from '../cordova/project.js'; +import * as cordova from '../cordova'; +import { CordovaProject } from '../cordova/project.js'; function createProjectContext(appDir) { const projectContext = new ProjectContext({ @@ -42,17 +42,18 @@ main.registerCommand({ for (platform of platformsToAdd) { if (_.contains(installedPlatforms, platform)) { buildmessage.error(`${platform}: platform is already added`); - } else if (!_.contains(AVAILABLE_PLATFORMS, platform)) { + } else if (!_.contains(cordova.AVAILABLE_PLATFORMS, platform)) { buildmessage.error(`${platform}: no such platform`); } } if (buildmessage.jobHasMessages()) return; - const cordovaProject = createCordovaProjectIfNecessary(projectContext); + const cordovaProject = new CordovaProject(projectContext); installedPlatforms = installedPlatforms.concat(platformsToAdd) - ensureCordovaPlatformsAreSynchronized(cordovaProject, installedPlatforms); + const cordovaPlatforms = cordova.filterPlatforms(installedPlatforms); + cordovaProject.ensurePlatformsAreSynchronized(cordovaPlatforms); if (buildmessage.jobHasMessages()) return; @@ -60,7 +61,7 @@ main.registerCommand({ for (platform of platformsToAdd) { Console.info(`${platform}: added platform`); - checkPlatformRequirements(cordovaProject, platform); + cordovaProject.checkPlatformRequirements(platform); } }); @@ -96,10 +97,11 @@ main.registerCommand({ if (buildmessage.jobHasMessages()) return; - installedPlatforms = _.without(installedPlatforms, ...platformsToRemove); + const cordovaProject = new CordovaProject(projectContext); - const cordovaProject = createCordovaProjectIfNecessary(projectContext); - ensureCordovaPlatformsAreSynchronized(cordovaProject, installedPlatforms); + installedPlatforms = _.without(installedPlatforms, ...platformsToRemove); + const cordovaPlatforms = cordova.filterPlatforms(installedPlatforms); + cordovaProject.ensurePlatformsAreSynchronized(cordovaPlatforms); if (buildmessage.jobHasMessages()) return; diff --git a/tools/cli/commands-packages.js b/tools/cli/commands-packages.js index 52d769db0d4..f7713f1d377 100644 --- a/tools/cli/commands-packages.js +++ b/tools/cli/commands-packages.js @@ -11,7 +11,6 @@ var catalog = require('../packaging/catalog/catalog.js'); var catalogRemote = require('../packaging/catalog/catalog-remote.js'); var isopack = require('../isobuild/isopack.js'); var updater = require('../packaging/updater.js'); -import { filterCordovaPackages } from '../cordova/plugins.js'; var Console = require('../console/console.js').Console; var projectContextModule = require('../project-context.js'); var colonConverter = require('../utils/colon-converter.js'); @@ -24,6 +23,8 @@ var packageMapModule = require('../packaging/package-map.js'); var packageClient = require('../packaging/package-client.js'); var tropohouse = require('../packaging/tropohouse.js'); +import * as cordova from '../cordova'; + // For each release (or package), we store a meta-record with its name, // maintainers, etc. This function takes in a name, figures out if // it is a release or a package, and fetches the correct record. @@ -1810,14 +1811,14 @@ main.registerCommand({ var exitCode = 0; - var filteredPackages = filterCordovaPackages(options.args); - var pluginsToAdd = filteredPackages.plugins; + const { plugins: pluginsToAdd, packages: packagesToAdd } = + cordova.splitPluginsAndPackages(options.args); if (pluginsToAdd.length) { - var plugins = projectContext.cordovaPluginsFile.getPluginVersions(); - var changed = false; - _.each(pluginsToAdd, function (pluginSpec) { - var parts = pluginSpec.split('@'); + let plugins = projectContext.cordovaPluginsFile.getPluginVersions(); + let changed = false; + for (pluginSpec of pluginsToAdd) { + let parts = pluginSpec.split('@'); if (parts.length !== 2) { Console.error( pluginSpec + ': exact version or tarball url is required'); @@ -1831,13 +1832,11 @@ main.registerCommand({ changed = true; Console.info("added cordova plugin " + parts[0]); } - }); + } changed && projectContext.cordovaPluginsFile.write(plugins); } - var args = filteredPackages.rest; - - if (_.isEmpty(args)) + if (_.isEmpty(packagesToAdd)) return exitCode; // Messages that we should print if we make any changes, but that don't count @@ -1849,7 +1848,7 @@ main.registerCommand({ // them -- add should be an atomic operation regardless of the package // order. var messages = buildmessage.capture(function () { - _.each(args, function (packageReq) { + _.each(packagesToAdd, function (packageReq) { buildmessage.enterJob("adding package " + packageReq, function () { var constraint = utils.parsePackageConstraint(packageReq, { useBuildmessage: true @@ -1993,16 +1992,16 @@ main.registerCommand({ }); // Special case on reserved package namespaces, such as 'cordova' - var filteredPackages = filterCordovaPackages(options.args); - var pluginsToRemove = filteredPackages.plugins; + const { plugins: pluginsToRemove, packages } = + cordova.splitPluginsAndPackages(options.args); - var exitCode = 0; + let exitCode = 0; // Update the plugins list if (pluginsToRemove.length) { - var plugins = projectContext.cordovaPluginsFile.getPluginVersions(); - var changed = false; - _.each(pluginsToRemove, function (pluginName) { + let plugins = projectContext.cordovaPluginsFile.getPluginVersions(); + let changed = false; + for (pluginName of pluginsToRemove) { if (/@/.test(pluginName)) { Console.error(pluginName + ": do not specify version constraints."); exitCode = 1; @@ -2015,21 +2014,19 @@ main.registerCommand({ " is not in this project."); exitCode = 1; } - }); + } changed && projectContext.cordovaPluginsFile.write(plugins); } - var args = filteredPackages.rest; - - if (_.isEmpty(args)) + if (_.isEmpty(packages)) return exitCode; // For each package name specified, check if we already have it and warn the // user. Because removing each package is a completely atomic operation that // has no chance of failure, this is just a warning message, it doesn't cause // us to stop. - var packagesToRemove = []; - _.each(args, function (packageName) { + let packagesToRemove = []; + _.each(packages, function (packageName) { if (/@/.test(packageName)) { Console.error(packageName + ": do not specify version constraints."); exitCode = 1; diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 6f2fecfb681..ff0165505b6 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -13,14 +13,15 @@ var httpHelpers = require('../utils/http-helpers.js'); var archinfo = require('../utils/archinfo.js'); var catalog = require('../packaging/catalog/catalog.js'); var stats = require('../meteor-services/stats.js'); -import { platformsForTargets } from '../cordova/platforms.js'; -import { buildCordovaProject } from '../cordova/build.js'; -import { buildCordovaRunners } from '../cordova/run.js'; var Console = require('../console/console.js').Console; var projectContextModule = require('../project-context.js'); - var release = require('../packaging/release.js'); +import * as cordova from '../cordova'; +import { CordovaProject } from '../cordova/project.js'; +import { CordovaRunner } from '../cordova/runner.js'; +import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js'; + // The architecture used by MDG's hosted servers; it's the architecture used by // 'meteor deploy'. var DEPLOY_ARCH = 'os.linux.x86_64'; @@ -72,78 +73,77 @@ var showInvalidArchMsg = function (arch) { // Utility functions to parse options in run/build/test-packages commands export function parseServerOptionsForRunCommand(options) { - const serverUrl = parsePortOption(options.port); + const parsedServerUrl = parsePortOption(options.port); // XXX COMPAT WITH 0.9.2.2 -- the 'mobile-port' option is deprecated const mobileServerOption = options['mobile-server'] || options['mobile-port']; - let mobileServerUrl; + let parsedMobileServerUrl; if (mobileServerOption) { - mobileServerUrl = parseMobileServerOption(mobileServerOption); + parsedMobileServerUrl = parseMobileServerOption(mobileServerOption); } else { - mobileServerUrl = mobileServerUrlForServerUrl(serverUrl, + parsedMobileServerUrl = detectMobileServerUrl(parsedServerUrl, isRunOnDeviceRequested(options)); } - return { serverUrl, mobileServerUrl }; + return { parsedServerUrl, parsedMobileServerUrl }; } function parsePortOption(portOption) { - let serverUrl; + let parsedServerUrl; try { - serverUrl = utils.parseUrl(portOption); - } catch (err) { + parsedServerUrl = utils.parseUrl(portOption); + } catch (error) { if (options.verbose) { Console.rawError( - `Error while parsing --port option: ${err.stack} \n`); + `Error while parsing --port option: ${error.stack} \n`); } else { - Console.error(err.message); + Console.error(error.message); } throw new main.ExitWithCode(1); } - if (!serverUrl.port) { + if (!parsedServerUrl.port) { Console.error("--port must include a port."); throw new main.ExitWithCode(1); } - return serverUrl; + return parsedServerUrl; } function parseMobileServerOption(mobileServerOption, optionName = 'mobile-server') { - let mobileServerUrl; + let parsedMobileServerUrl; try { - mobileServerUrl = utils.parseUrl(mobileServerOption, { - protocol: 'http://' - }); - } catch (err) { + parsedMobileServerUrl = utils.parseUrl(mobileServerOption, { + protocol: 'http://'}); + } catch (error) { if (options.verbose) { Console.rawError( - `Error while parsing --${optionName} option: ${err.stack} \n`); + `Error while parsing --${optionName} option: ${error.stack} \n`); } else { - Console.error(err.message); + Console.error(error.message); } throw new main.ExitWithCode(1); } - if (!mobileServerUrl.host) { - Console.error(`--${optionName} must specify a hostname.`); + if (!parsedMobileServerUrl.host) { + Console.error(`--${optionName} must include a hostname.`); throw new main.ExitWithCode(1); } - return mobileServerUrl; + return parsedMobileServerUrl; } -function mobileServerUrlForServerUrl(serverUrl, isRunOnDeviceRequested) { +function detectMobileServerUrl(parsedServerUrl, isRunOnDeviceRequested) { // If we are running on a device, use the auto-detected IP if (isRunOnDeviceRequested) { let myIp; try { myIp = utils.ipAddress(); - } catch (err) { + } catch (error) { Console.error( `Error detecting IP address for mobile app to connect to: -${err.message} +${error.message} Please specify the address that the mobile app should connect to with --mobile-server.`); throw new main.ExitWithCode(1); @@ -151,14 +151,14 @@ to with --mobile-server.`); return { protocol: 'http://', host: myIp, - port: serverUrl.port + port: parsedServerUrl.port }; } else { // We are running a simulator, use localhost return { protocol: 'http://', host: 'localhost', - port: serverUrl.port + port: parsedServerUrl.port }; } } @@ -166,10 +166,27 @@ to with --mobile-server.`); // Is a run on a device requested? // XXX This shouldn't be hard-coded function isRunOnDeviceRequested(options) { - return !!_.intersection(options.args, - ['ios-device', 'android-device']).length; + return !_.isEmpty(_.intersection(options.args, + ['ios-device', 'android-device'])); } +function parseRunTargets(targets) { + return targets.map((target) => { + const targetParts = target.split('-'); + const platform = targetParts[0]; + const isDevice = targetParts[1] === 'device'; + + if (platform == 'ios') { + return new iOSRunTarget(isDevice); + } else if (platform == 'android') { + return new AndroidRunTarget(isDevice); + } else { + Console.error(`Unknown run target: ${target}`); + throw new main.ExitWithCode(1); + } + }); +}; + /////////////////////////////////////////////////////////////////////////////// // options that act like commands /////////////////////////////////////////////////////////////////////////////// @@ -289,7 +306,7 @@ main.registerCommand(_.extend( function doRunCommand(options) { Console.setVerbose(!!options.verbose); - const { serverUrl, mobileServerUrl } = + const { parsedServerUrl, parsedMobileServerUrl } = parseServerOptionsForRunCommand(options); var projectContext = new projectContextModule.ProjectContext({ @@ -313,47 +330,6 @@ function doRunCommand(options) { } } - var runners = []; - // If additional args were specified, then also start a mobile build. - // XXX We should defer this work until after the proxy is listening! - // eg, move it into a CordovaBuildRunner or something. - - if (options.args.length) { - let cordovaProject; - // will asynchronously start mobile emulators/devices - try { - Console.debug('Will compile mobile builds'); - // Run the constraint solver and build local packages. - // XXX This code should be part of the main runner loop so that we can - // wait on a fix, just like in the non-Cordova case! (That would also - // move the build after the proxy listen.) - main.captureAndExit("=> Errors while initializing project:", function () { - projectContext.prepareProjectForBuild(); - }); - projectContext.packageMapDelta.displayOnConsole(); - - let targets = options.args; - var platforms = platformsForTargets(targets); - cordovaProject = buildCordovaProject(projectContext, platforms, _.extend({ - debug: !options.production - }, options, { - protocol: mobileServerUrl.protocol, - host: mobileServerUrl.host, - port: mobileServerUrl.port - })); - - runners = runners.concat( - buildCordovaRunners(projectContext, cordovaProject, targets, options)); - } catch (err) { - if (err instanceof main.ExitWithCode) { - throw err; - } else { - Console.printError(err, 'Error while running for mobile platforms'); - return 1; - } - } - } - let appHost, appPort; if (options['app-port']) { var appPortMatch = options['app-port'].match(/^(?:(.+):)?([0-9]+)?$/); @@ -379,22 +355,30 @@ function doRunCommand(options) { // NOTE: this calls process.exit() when testing is done. if (options['test']){ options.once = true; - const serverUrlString = "http://" + (serverUrl.host || "localhost") + - ":" + serverUrl.port; + const serverUrlForVelocity = + `http://${(parsedServerUrl.host || "localhost")}:${parsedServerUrl.port}`; const velocity = require('../runners/run-velocity.js'); - velocity.runVelocity(serverUrlString); + velocity.runVelocity(serverUrlForVelocity); } - let mobileServerUrlString = mobileServerUrl.protocol + mobileServerUrl.host; - if (mobileServerUrl.port) { - mobileServerUrlString += `:${mobileServerUrl.port}`; + // Additional args are interpreted as run targets + const runTargets = parseRunTargets(options.args); + + let cordovaRunner; + + if (!_.isEmpty(runTargets)) { + main.captureAndExit('', 'initializing Cordova project', () => { + const cordovaProject = new CordovaProject(projectContext); + cordovaRunner = new CordovaRunner(cordovaProject, runTargets); + cordovaRunner.checkPlatformsForRunTargets(); + }); } var runAll = require('../runners/run-all.js'); return runAll.run({ projectContext: projectContext, - proxyPort: serverUrl.port, - proxyHost: serverUrl.host, + proxyPort: parsedServerUrl.port, + proxyHost: parsedServerUrl.host, appPort: appPort, appHost: appHost, debugPort: options['debug-port'], @@ -406,9 +390,9 @@ function doRunCommand(options) { rootUrl: process.env.ROOT_URL, mongoUrl: process.env.MONGO_URL, oplogUrl: process.env.MONGO_OPLOG_URL, - mobileServerUrl: mobileServerUrlString, + mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), once: options.once, - extraRunners: runners + cordovaRunner: cordovaRunner }); } @@ -802,41 +786,34 @@ var buildCommand = function (options) { options.settings = options['mobile-settings']; } - var mobilePlatforms = []; - if (! options._serverOnly) { - mobilePlatforms = projectContext.platformList.getCordovaPlatforms(); - } + const appName = files.pathBasename(options.appDir); - if (!_.isEmpty(mobilePlatforms) && !options._serverOnly) { - // XXX COMPAT WITH 0.9.2.2 -- the --mobile-port option is deprecated - const mobileServerOption = options.server || options["mobile-port"]; - if (!mobileServerOption) { - // For Cordova builds, require '--server'. - // XXX better error message? - Console.error( - "Supply the server hostname and port in the --server option " + - "for mobile app builds."); - return 1; - } - const mobileServerUrl = parseMobileServerOption(mobileServerOption, - 'server'); + let cordovaPlatforms; + let parsedMobileServerUrl; + if (!options._serverOnly) { + cordovaPlatforms = projectContext.platformList.getCordovaPlatforms(); - var cordovaSettings = {}; + if (process.platform !== 'darwin' && _.contains(cordovaPlatforms, 'ios')) { + cordovaPlatforms = _.without(cordovaPlatforms, 'ios'); + Console.warn("Currently, it is only possible to build iOS apps on an OS X system."); + } - try { - cordovaProject = - buildCordovaProject(projectContext, mobilePlatforms, _.extend({}, - options, { - protocol: mobileServerUrl.protocol, - host: mobileServerUrl.host, - port: mobileServerUrl.port - })); - } catch (err) { - if (err instanceof main.ExitWithCode) - throw err; - Console.printError(err, "Error while building for mobile platforms"); - return 1; + if (!_.isEmpty(cordovaPlatforms)) { + // XXX COMPAT WITH 0.9.2.2 -- the --mobile-port option is deprecated + const mobileServerOption = options.server || options["mobile-port"]; + if (!mobileServerOption) { + // For Cordova builds, require '--server'. + // XXX better error message? + Console.error( + "Supply the server hostname and port in the --server option " + + "for mobile app builds."); + return 1; + } + parsedMobileServerUrl = parseMobileServerOption(mobileServerOption, + 'server'); } + } else { + cordovaPlatforms = []; } var buildDir = projectContext.getProjectLocalDirectory('build_tar'); @@ -844,7 +821,7 @@ var buildCommand = function (options) { // Unless we're just making a tarball, warn if people try to build inside the // app directory. - if (options.directory || ! _.isEmpty(mobilePlatforms)) { + if (options.directory || ! _.isEmpty(cordovaPlatforms)) { var relative = files.pathRelative(options.appDir, outputPath); // We would like the output path to be outside the app directory, which // means the first step to getting there is going up a level. @@ -882,7 +859,7 @@ var buildCommand = function (options) { // is then 'meteor bundle' with no args fails if you have any local // packages with binary npm dependencies serverArch: bundleArch, - buildMode: options.debug ? 'development' : 'production' + buildMode: options.debug ? 'development' : 'production', } }); if (bundleResult.errors) { @@ -895,54 +872,72 @@ var buildCommand = function (options) { files.mkdir_p(outputPath); if (! options.directory) { - try { - var outputTar = options._serverOnly ? outputPath : - files.pathJoin(outputPath, cordovaProject.appName + '.tar.gz'); - - files.createTarball(files.pathJoin(buildDir, 'bundle'), outputTar); - } catch (err) { - Console.error("Errors during tarball creation:"); - Console.error(err.message); - files.rm_recursive(buildDir); - return 1; - } + main.captureAndExit('', 'creating server tarball', () => { + try { + var outputTar = options._serverOnly ? outputPath : + files.pathJoin(outputPath, appName + '.tar.gz'); + + files.createTarball(files.pathJoin(buildDir, 'bundle'), outputTar); + } catch (err) { + buildmessage.exception(err); + files.rm_recursive(buildDir); + } + }); } - // Copy over the Cordova builds AFTER we bundle so that they are not included - // in the main bundle. - !options._serverOnly && _.each(mobilePlatforms, function (platformName) { - var buildPath = files.pathJoin( - projectContext.getProjectLocalDirectory('cordova-build'), - 'platforms', platformName); - var platformPath = files.pathJoin(outputPath, platformName); - - if (platformName === 'ios') { - if (process.platform !== 'darwin') return; - files.cp_r(buildPath, files.pathJoin(platformPath, 'project')); - files.writeFile( - files.pathJoin(platformPath, 'README'), - "This is an auto-generated XCode project for your iOS application.\n\n" + - "Instructions for publishing your iOS app to App Store can be found at:\n" + - "https://github.com/meteor/meteor/wiki/How-to-submit-your-iOS-app-to-App-Store\n", - "utf8"); - } else if (platformName === 'android') { - files.cp_r(buildPath, files.pathJoin(platformPath, 'project')); - var apkPath = findApkPath(files.pathJoin(buildPath, 'build'), options.debug); - files.copyFile(apkPath, files.pathJoin(platformPath, options.debug ? 'debug.apk' : 'release-unsigned.apk')); - files.writeFile( - files.pathJoin(platformPath, 'README'), - "This is an auto-generated Gradle project for your Android application.\n\n" + - "Instructions for publishing your Android app to Play Store can be found at:\n" + - "https://github.com/meteor/meteor/wiki/How-to-submit-your-Android-app-to-Play-Store\n", - "utf8"); - } - }); + if (!_.isEmpty(cordovaPlatforms)) { + let cordovaProject; - files.rm_recursive(buildDir); -}; + main.captureAndExit('', () => { + buildmessage.enterJob({ title: "preparing Cordova project"}, () => { + cordovaProject = new CordovaProject(projectContext, appName); -var findApkPath = function (dirPath, debug) { - return files.pathJoin(dirPath, 'outputs', 'apk', debug ? 'android-debug.apk' : 'android-release-unsigned.apk'); + const plugins = cordova.pluginsFromStarManifest( + bundleResult.starManifest); + + cordovaProject.prepare(bundlePath, plugins, + { settingsFile: options.settings, + mobileServerUrl: utils.formatUrl(parsedMobileServerUrl) }); + }); + + for (platform of cordovaPlatforms) { + buildmessage.enterJob({ title: `building Cordova project for \ +${cordova.displayNameForPlatform(platform)}`}, () => { + let buildOptions = []; + if (!options.debug) buildOptions.push('--release'); + cordovaProject.build([platform], buildOptions); + + const buildPath = files.pathJoin( + projectContext.getProjectLocalDirectory('cordova-build'), + 'platforms', platform); + const platformOutputPath = files.pathJoin(outputPath, platform); + + if (platform === 'ios') { + files.cp_r(buildPath, files.pathJoin(platformOutputPath, 'project')); + files.writeFile( + files.pathJoin(platformOutputPath, 'README'), + "This is an auto-generated XCode project for your iOS application.\n\n" + + "Instructions for publishing your iOS app to App Store can be found at:\n" + + "https://github.com/meteor/meteor/wiki/How-to-submit-your-iOS-app-to-App-Store\n", + "utf8"); + } else if (platform === 'android') { + files.cp_r(buildPath, files.pathJoin(platformOutputPath, 'project')); + const apkPath = files.pathJoin(buildPath, 'build', 'outputs', 'apk', + options.debug ? 'android-debug.apk' : 'android-release-unsigned.apk') + files.copyFile(apkPath, files.pathJoin(platformOutputPath, options.debug ? 'debug.apk' : 'release-unsigned.apk')); + files.writeFile( + files.pathJoin(platformOutputPath, 'README'), + "This is an auto-generated Gradle project for your Android application.\n\n" + + "Instructions for publishing your Android app to Play Store can be found at:\n" + + "https://github.com/meteor/meteor/wiki/How-to-submit-your-Android-app-to-Play-Store\n", + "utf8"); + } + }); + } + }); + } + + files.rm_recursive(buildDir); }; /////////////////////////////////////////////////////////////////////////////// @@ -1403,7 +1398,7 @@ main.registerCommand({ }, function (options) { Console.setVerbose(!!options.verbose); - const { serverUrl, mobileServerUrl } = + const { parsedServerUrl, parsedMobileServerUrl } = parseServerOptionsForRunCommand(options); // Find any packages mentioned by a path instead of a package name. We will @@ -1474,59 +1469,32 @@ main.registerCommand({ // runner, once the proxy is listening. The changes we made were persisted to // disk, so projectContext.reset won't make us forget anything. - var mobileOptions = ['ios', 'ios-device', 'android', 'android-device']; - var mobileTargets = []; - _.each(mobileOptions, function (option) { - if (options[option]) - mobileTargets.push(option); - }); - - if (! _.isEmpty(mobileTargets)) { - var runners = []; + const runTargets = parseRunTargets(_.intersection( + Object.keys(options), ['ios', 'ios-device', 'android', 'android-device'])); - var platforms = platformsForTargets(mobileTargets); - projectContext.platformList.write(platforms); + let cordovaRunner; - // Run the constraint solver and build local packages. - // XXX This code should be part of the main runner loop so that we can - // wait on a fix, just like in the non-Cordova case! (That would also - // move the build after the proxy listen.) - main.captureAndExit("=> Errors while initializing project:", function () { - projectContext.prepareProjectForBuild(); + if (!_.isEmpty(runTargets)) { + main.captureAndExit('', 'initializing Cordova project', () => { + const cordovaProject = new CordovaProject(projectContext); + cordovaRunner = new CordovaRunner(cordovaProject, runTargets); + projectContext.platformList.write(cordovaRunner.platformsForRunTargets); + cordovaRunner.checkPlatformsForRunTargets(); }); - // No need to display the PackageMapDelta here, since it would include all - // of the packages! - - try { - const cordovaProject = buildCordovaProject(projectContext, platforms, - _.extend({}, options, { - debug: ! options.production - }, { - protocol: mobileServerUrl.protocol, - host: mobileServerUrl.host, - port: mobileServerUrl.port - })); - runners = runners.concat(buildCordovaRunners(projectContext, - cordovaProject, mobileTargets, options)); - } catch (err) { - if (err instanceof main.ExitWithCode) { - throw err; - } else { - Console.printError(err, 'Error while testing for mobile platforms'); - return 1; - } - } - options.extraRunners = runners; } + options.cordovaRunner = cordovaRunner; + if (options.velocity) { - const serverUrlString = "http://" + (parsedUrl.host || "localhost") + - ":" + parsedUrl.port; + const serverUrlForVelocity = + `http://${(parsedServerUrl.host || "localhost")}:${parsedServerUrl.port}`; const velocity = require('../runners/run-velocity.js'); - velocity.runVelocity(serverUrlString); + velocity.runVelocity(serverUrlForVelocity); } - return runTestAppForPackages(projectContext, options); + return runTestAppForPackages(projectContext, _.extend( + options, + { mobileServerUrl: utils.formatUrl(parsedMobileServerUrl) })); }); // Returns the "local-test:*" package names for the given package names (or for @@ -1619,11 +1587,12 @@ var runTestAppForPackages = function (projectContext, options) { rootUrl: process.env.ROOT_URL, mongoUrl: process.env.MONGO_URL, oplogUrl: process.env.MONGO_OPLOG_URL, + mobileServerUrl: options.mobileServerUrl, once: options.once, recordPackageUsage: false, selenium: options.selenium, seleniumBrowser: options['selenium-browser'], - extraRunners: options.extraRunners, + cordovaRunner: options.cordovaRunner, // On the first run, we shouldn't display the delta between "no packages // in the temp app" and "all the packages we're testing". If we make // changes and reload, though, it's fine to display them. diff --git a/tools/cordova/android-runner.js b/tools/cordova/android-runner.js deleted file mode 100644 index 963ce7608d6..00000000000 --- a/tools/cordova/android-runner.js +++ /dev/null @@ -1,44 +0,0 @@ -import isopackets from '../tool-env/isopackets.js' -import files from '../fs/files.js'; -import { Console } from '../console.js'; - -import CordovaRunner from './cordova-runner.js' -import { execFileSyncOrThrow, execFileAsyncOrThrow } from './utils.js' - -export default class AndroidRunner extends CordovaRunner { - constructor(projectContext, cordovaProject, isDevice, options) { - super(projectContext, cordovaProject, options); - this.isDevice = isDevice; - } - - get platform() { - return 'android'; - } - - get displayName() { - return this.isDevice ? 'Android Device' : 'Android Emulator'; - } - - checkRequirementsAndSetEnvIfNeeded() { - const platformsDir = files.pathJoin(this.cordovaProject.projectRoot, 'platforms'); - const modulePath = files.pathJoin(platformsDir, 'android', 'cordova', 'lib', 'check_reqs'); - Promise.await(require(files.convertToOSPath(modulePath)).run()); - } - - async run(options) { - return this.cordovaProject.run(this.platform, this.isDevice, options) - } - - async tailLogs(options) { - // Make cordova-android handle requirements and set env if needed - this.checkRequirementsAndSetEnvIfNeeded(); - - // Clear logs - execFileSyncOrThrow('adb', ['logcat', '-c']); - - execFileAsyncOrThrow('adb', ['logcat'], { - verbose: true, - lineMapper: null - }); - } -} diff --git a/tools/cordova/build.js b/tools/cordova/build.js deleted file mode 100644 index ef90f6a0804..00000000000 --- a/tools/cordova/build.js +++ /dev/null @@ -1,193 +0,0 @@ -import _ from 'underscore'; -import util from 'util'; -import { Console } from '../console.js'; -import buildmessage from '../buildmessage.js'; -import files from '../fs/files.js'; -import bundler from '../isobuild/bundler.js'; -import archinfo from '../archinfo.js'; -import release from '../packaging/release.js'; -import isopackets from '../tool-env/isopackets.js' - -import { createCordovaProjectIfNecessary } from './project.js'; -import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized, - checkCordovaPlatforms } from './platforms.js'; -import { ensureCordovaPluginsAreSynchronized } from './plugins.js'; -import { processMobileControlFile } from './mobile-control-file.js'; - -const WEB_ARCH_NAME = "web.cordova"; - -// Returns the cordovaDependencies of the Cordova arch from a star json. -export function getCordovaDependenciesFromStar(star) { - var cordovaProgram = _.findWhere(star.programs, { arch: WEB_ARCH_NAME }); - if (cordovaProgram) { - return cordovaProgram.cordovaDependencies; - } else { - return {}; - } -} - -// Build a Cordova project, creating it if necessary. -export function buildCordovaProject(projectContext, platforms, options) { - if (_.isEmpty(platforms)) return; - - Console.debug('Building the Cordova project'); - - platforms = checkCordovaPlatforms(projectContext, platforms); - - // Make sure there is a project, as all other operations depend on that - const cordovaProject = createCordovaProjectIfNecessary(projectContext); - - buildmessage.enterJob({ title: 'building for mobile devices' }, function () { - const bundlePath = - projectContext.getProjectLocalDirectory('build-cordova-temp'); - - Console.debug('Bundling the web.cordova program of the app'); - const bundle = getBundle(projectContext, bundlePath, options); - - // Check and consume the control file - const controlFilePath = - files.pathJoin(projectContext.projectDir, 'mobile-config.js'); - - processMobileControlFile( - controlFilePath, - projectContext, - cordovaProject, - options.host); - - ensureCordovaPlatformsAreSynchronized(cordovaProject, - projectContext.platformList.getPlatforms()); - - ensureCordovaPluginsAreSynchronized(cordovaProject, getCordovaDependenciesFromStar( - bundle.starManifest)); - - const wwwPath = files.pathJoin(cordovaProject.projectRoot, 'www'); - - Console.debug('Removing the www folder'); - files.rm_recursive(wwwPath); - - const applicationPath = files.pathJoin(wwwPath, 'application'); - const programPath = files.pathJoin(bundlePath, 'programs', WEB_ARCH_NAME); - - Console.debug('Writing www/application folder'); - files.mkdir_p(applicationPath); - files.cp_r(programPath, applicationPath); - - // Clean up the temporary bundle directory - files.rm_recursive(bundlePath); - - Console.debug('Writing index.html'); - - // Generate index.html - var indexHtml = generateCordovaBoilerplate( - projectContext, applicationPath, options); - files.writeFile(files.pathJoin(applicationPath, 'index.html'), indexHtml, 'utf8'); - - // Write the cordova loader - Console.debug('Writing meteor_cordova_loader'); - var loaderPath = files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js'); - var loaderCode = files.readFile(loaderPath); - files.writeFile(files.pathJoin(wwwPath, 'meteor_cordova_loader.js'), loaderCode); - - Console.debug('Writing a default index.html for cordova app'); - var indexPath = files.pathJoin(__dirname, 'client', 'cordova_index.html'); - var indexContent = files.readFile(indexPath); - files.writeFile(files.pathJoin(wwwPath, 'index.html'), indexContent); - - // Cordova Build Override feature (c) - var buildOverridePath = - files.pathJoin(projectContext.projectDir, 'cordova-build-override'); - - if (files.exists(buildOverridePath) && - files.stat(buildOverridePath).isDirectory()) { - Console.debug('Copying over the cordova-build-override'); - files.cp_r(buildOverridePath, cordovaProject.projectRoot); - } - - // Run the actual build - Console.debug('Running the build command'); - - buildmessage.enterJob({ title: 'building mobile project' }, () => { - const buildOptions = options.debug ? [] : ['release']; - Promise.await(cordovaProject.build({ platforms: platforms, options: buildOptions })); - }); - }); - - Console.debug('Done building the cordova build project'); - - return cordovaProject; -}; - -// options -// - debug -function getBundle(projectContext, bundlePath, options) { - var bundleResult = bundler.bundle({ - projectContext: projectContext, - outputPath: bundlePath, - buildOptions: { - minifyMode: options.debug ? 'development' : 'production', - // XXX can we ask it not to create the server arch? - serverArch: archinfo.host(), - webArchs: [WEB_ARCH_NAME], - includeDebug: !!options.debug - } - }); - - if (bundleResult.errors) { - // XXX better error handling? - throw new Error("Errors prevented bundling:\n" + - bundleResult.errors.formatMessages()); - } - - return bundleResult; -}; - -function generateCordovaBoilerplate(projectContext, clientDir, options) { - var clientJsonPath = files.convertToOSPath(files.pathJoin(clientDir, 'program.json')); - var clientJson = JSON.parse(files.readFile(clientJsonPath, 'utf8')); - var manifest = clientJson.manifest; - var settings = options.settings ? - JSON.parse(files.readFile(options.settings, 'utf8')) : {}; - var publicSettings = settings['public']; - - var meteorRelease = - release.current.isCheckout() ? "none" : release.current.name; - - var configDummy = {}; - if (publicSettings) configDummy.PUBLIC_SETTINGS = publicSettings; - - const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing']; - var calculatedHash = - WebAppHashing.calculateClientHash(manifest, null, configDummy); - - // XXX partially copied from autoupdate package - var version = process.env.AUTOUPDATE_VERSION || calculatedHash; - - var mobileServer = options.protocol + options.host; - if (options.port) { - mobileServer = mobileServer + ":" + options.port; - } - - var runtimeConfig = { - meteorRelease: meteorRelease, - ROOT_URL: mobileServer + "/", - // XXX propagate it from options? - ROOT_URL_PATH_PREFIX: '', - DDP_DEFAULT_CONNECTION_URL: mobileServer, - autoupdateVersionCordova: version, - appId: projectContext.appIdentifier - }; - - if (publicSettings) - runtimeConfig.PUBLIC_SETTINGS = publicSettings; - - const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator']; - var boilerplate = new Boilerplate(WEB_ARCH_NAME, manifest, { - urlMapper: _.identity, - pathMapper: (path) => files.convertToOSPath(files.pathJoin(clientDir, path)), - baseDataExtension: { - meteorRuntimeConfig: JSON.stringify( - encodeURIComponent(JSON.stringify(runtimeConfig))) - } - }); - return boilerplate.toHTML(); -}; diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js new file mode 100644 index 00000000000..9958c926774 --- /dev/null +++ b/tools/cordova/builder.js @@ -0,0 +1,589 @@ +import _ from 'underscore'; +import util from 'util'; +import { Console } from '../console/console.js'; +import buildmessage from '../utils/buildmessage.js'; +import files from '../fs/files.js'; +import bundler from '../isobuild/bundler.js'; +import archinfo from '../utils/archinfo.js'; +import release from '../packaging/release.js'; +import isopackets from '../tool-env/isopackets.js'; +import utils from '../utils/utils.js'; + +import { CORDOVA_ARCH } from './index.js'; + +// Hard-coded size constants + +const iconsIosSizes = { + 'iphone': '60x60', + 'iphone_2x': '120x120', + 'iphone_3x': '180x180', + 'ipad': '76x76', + 'ipad_2x': '152x152' +}; + +const iconsAndroidSizes = { + 'android_ldpi': '36x36', + 'android_mdpi': '42x42', + 'android_hdpi': '72x72', + 'android_xhdpi': '96x96' +}; + +const launchIosSizes = { + 'iphone': '320x480', + 'iphone_2x': '640x960', + 'iphone5': '640x1136', + 'iphone6': '750x1334', + 'iphone6p_portrait': '1242x2208', + 'iphone6p_landscape': '2208x1242', + 'ipad_portrait': '768x1004', + 'ipad_portrait_2x': '1536x2008', + 'ipad_landscape': '1024x748', + 'ipad_landscape_2x': '2048x1496' +}; + +const launchAndroidSizes = { + 'android_ldpi_portrait': '320x426', + 'android_ldpi_landscape': '426x320', + 'android_mdpi_portrait': '320x470', + 'android_mdpi_landscape': '470x320', + 'android_hdpi_portrait': '480x640', + 'android_hdpi_landscape': '640x480', + 'android_xhdpi_portrait': '720x960', + 'android_xhdpi_landscape': '960x720' +}; + +export class CordovaBuilder { + constructor(cordovaProject, bundlePath, plugins, options) { + this.cordovaProject = cordovaProject; + + this.bundlePath = bundlePath; + this.plugins = plugins; + this.options = options; + + this.resourcesPath = files.pathJoin( + this.cordovaProject.projectRoot, + 'resources'); + } + + get projectContext() { + return this.cordovaProject.projectContext; + } + + start() { + buildmessage.assertInCapture(); + + buildmessage.enterJob({ title: `preparing Cordova project` }, () => { + this.initalizeDefaults(); + + this.processControlFile(); + + this.writeConfigXmlAndCopyResources(); + this.copyWWW(); + this.copyBuildOverride(); + + this.cordovaProject.ensurePlatformsAreSynchronized(); + this.cordovaProject.ensurePluginsAreSynchronized(this.plugins, + this.pluginsConfiguration); + }); + } + + initalizeDefaults() { + const defaultBuildNumber = (Date.now() % 1000000).toString(); + this.metadata = { + id: 'com.id' + this.projectContext.appIdentifier, + version: '0.0.1', + buildNumber: defaultBuildNumber, + name: this.cordovaProject.appName, + description: 'New Meteor Mobile App', + author: 'A Meteor Developer', + email: 'n/a', + website: 'n/a' + }; + + // set some defaults different from the Phonegap/Cordova defaults + this.additionalConfiguration = { + 'webviewbounce': false, + 'DisallowOverscroll': true, + 'deployment-target': '7.0' + }; + + if (this.projectContext.packageMap.getInfo('launch-screen')) { + this.additionalConfiguration.AutoHideSplashScreen = false; + this.additionalConfiguration.SplashScreen = 'screen'; + this.additionalConfiguration.SplashScreenDelay = 10000; + } + + // Default access rules for plain Meteor-Cordova apps. + // Rules can be extended with mobile-config API. + // The value is `true` if the protocol or domain should be allowed, + // 'external' if should handled externally. + this.accessRules = { + // Allow external calls to things like email client or maps app or a + // phonebook app. + 'tel:*': 'external', + 'geo:*': 'external', + 'mailto:*': 'external', + 'sms:*': 'external', + 'market:*': 'external', + + // phonegap/cordova related protocols + // "file:" protocol is used to access first files from disk + 'file:*': true, + 'cdv:*': true, + 'gap:*': true, + + // allow Meteor's local emulated server url - this is the url from which the + // application loads its assets + 'http://meteor.local/*': true + }; + + const mobileServerUrl = this.options.mobileServerUrl; + const serverDomain = mobileServerUrl ? + utils.parseUrl(mobileServerUrl).host : null; + + // If the remote server domain is known, allow access to it for xhr and DDP + // connections. + if (serverDomain) { + this.accessRules['*://' + serverDomain + '/*'] = true; + // Android talks to localhost over 10.0.2.2. This config file is used for + // multiple platforms, so any time that we say the server is on localhost we + // should also say it is on 10.0.2.2. + if (serverDomain === 'localhost') { + this.accessRules['*://10.0.2.2/*'] = true; + } + } + + this.imagePaths = { + icon: {}, + splash: {} + }; + + // Defaults are Meteor meatball images located in tools/cordova/assets directory + const assetsPath = files.pathJoin(__dirname, 'assets'); + const iconsPath = files.pathJoin(assetsPath, 'icons'); + const launchScreensPath = files.pathJoin(assetsPath, 'launchscreens'); + + const setIcon = (size, name) => { + this.imagePaths.icon[name] = files.pathJoin(iconsPath, size + '.png'); + }; + + const setLaunchscreen = (size, name) => { + this.imagePaths.splash[name] = files.pathJoin(launchScreensPath, size + '.png'); + }; + + _.each(iconsIosSizes, setIcon); + _.each(iconsAndroidSizes, setIcon); + _.each(launchIosSizes, setLaunchscreen); + _.each(launchAndroidSizes, setLaunchscreen); + + this.pluginsConfiguration = {}; + } + + processControlFile() { + const controlFilePath = + files.pathJoin(this.projectContext.projectDir, 'mobile-config.js'); + + + if (files.exists(controlFilePath)) { + const code = files.readFile(controlFilePath, 'utf8'); + + try { + Console.debug('Running the mobile control file'); + files.runJavaScript(code, { + filename: 'mobile-config.js', + symbols: { App: App(this) } + }); + } catch (error) { + throw new Error('Error reading mobile-config.js:' + error.stack); + } + } + } + + writeConfigXmlAndCopyResources() { + const { XmlBuilder } = isopackets.load('cordova-support')['xmlbuilder']; + + let config = XmlBuilder.create('widget'); + + // set the root attributes + _.each({ + id: this.metadata.id, + version: this.metadata.version, + 'android-versionCode': this.metadata.buildNumber, + 'ios-CFBundleVersion': this.metadata.buildNumber, + xmlns: 'http://www.w3.org/ns/widgets', + 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0' + }, (value, key) => { + if (value) { + config.att(key, value); + } + }); + + // set the metadata + config.element('name').txt(this.metadata.name); + config.element('description').txt(this.metadata.description); + config.element('author', { + href: this.metadata.website, + email: this.metadata.email + }).txt(this.metadata.author); + + // set the additional configuration preferences + _.each(this.additionalConfiguration, (value, key) => { + config.element('preference', { + name: key, + value: value.toString() + }); + }); + + // load from index.html by default + config.element('content', { src: 'index.html' }); + + // Copy all the access rules + _.each(this.accessRules, (rule, pattern) => { + var opts = { origin: pattern }; + if (rule === 'external') + opts['launch-external'] = true; + + config.element('access', opts); + }); + + const iosPlatformElement = config.element('platform', { name: 'ios' }); + const androidPlatformElement = config.element('platform', { name: 'android' }); + + // Prepare the resources folder + files.rm_recursive(this.resourcesPath); + files.mkdir_p(this.resourcesPath); + + Console.debug('Copying resources for mobile apps'); + + // add icons and launch screens to config and copy the files on fs + this.configureAndCopyImages(iconsIosSizes, iosPlatformElement, 'icon'); + this.configureAndCopyImages(iconsAndroidSizes, androidPlatformElement, 'icon'); + this.configureAndCopyImages(launchIosSizes, iosPlatformElement, 'splash'); + this.configureAndCopyImages(launchAndroidSizes, androidPlatformElement, 'splash'); + + Console.debug('Writing new config.xml'); + const configXmlPath = files.pathJoin(this.cordovaProject.projectRoot, 'config.xml'); + const formattedXmlConfig = config.end({ pretty: true }); + files.writeFile(configXmlPath, formattedXmlConfig, 'utf8'); + } + + configureAndCopyImages(sizes, xmlElement, tag) { + const imageAttributes = (name, width, height, src) => { + const androidMatch = /android_(.?.dpi)_(landscape|portrait)/g.exec(name); + + let attributes = { + src: src, + width: width, + height: height + }; + + // XXX special case for Android + if (androidMatch) { + attributes.density = androidMatch[2].substr(0, 4) + '-' + androidMatch[1]; + } + + return attributes; + }; + + _.each(sizes, (size, name) => { + const [width, height] = size.split('x'); + + const suppliedPath = this.imagePaths[tag][name]; + if (!suppliedPath) + return; + + const suppliedFilename = _.last(suppliedPath.split(files.pathSep)); + let extension = _.last(suppliedFilename.split('.')); + + // XXX special case for 9-patch png's + if (suppliedFilename.match(/\.9\.png$/)) { + extension = '9.png'; + } + + const filename = name + '.' + tag + '.' + extension; + const src = files.pathJoin('resources', filename); + + // copy the file to the build folder with a standardized name + files.copyFile( + files.pathResolve(this.projectContext.projectDir, suppliedPath), + files.pathJoin(this.resourcesPath, filename)); + + // set it to the xml tree + xmlElement.element(tag, imageAttributes(name, width, height, src)); + + // XXX reuse one size for other dimensions + const dups = { + '60x60': ['29x29', '40x40', '50x50', '57x57', '58x58'], + '76x76': ['72x72'], + '152x152': ['144x144'], + '120x120': ['80x80', '100x100', '114x114'], + '768x1004': ['768x1024'], + '1536x2008': ['1536x2048'], + '1024x748': ['1024x768'], + '2048x1496': ['2048x1536'] + }[size]; + + // just use the same image + _.each(dups, (size) => { + const [width, height] = size.split('x'); + // XXX this is fine to not supply a name since it is always iOS, but + // this is a hack right now. + xmlElement.element(tag, imageAttributes('n/a', width, height, src)); + }); + }); + } + + copyWWW() { + const wwwPath = files.pathJoin(this.cordovaProject.projectRoot, 'www'); + + // Remove existing www + files.rm_recursive(wwwPath); + + // Create www and www/application directories + const applicationPath = files.pathJoin(wwwPath, 'application'); + files.mkdir_p(applicationPath); + + // Copy Cordova arch program from bundle to www/application + const programPath = files.pathJoin(this.bundlePath, 'programs', CORDOVA_ARCH); + files.cp_r(programPath, applicationPath); + + const bootstrapPage = this.generateBootstrapPage(applicationPath); + files.writeFile(files.pathJoin(applicationPath, 'index.html'), + bootstrapPage, 'utf8'); + + files.copyFile( + files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js'), + files.pathJoin(wwwPath, 'meteor_cordova_loader.js')); + files.copyFile( + files.pathJoin(__dirname, 'client', 'cordova_index.html'), + files.pathJoin(wwwPath, 'index.html')); + } + + generateBootstrapPage(applicationPath) { + const programJsonPath = files.convertToOSPath( + files.pathJoin(applicationPath, 'program.json')); + const programJson = JSON.parse(files.readFile(programJsonPath, 'utf8')); + const manifest = programJson.manifest; + + const settingsFile = this.options.settingsFile; + const settings = settingsFile ? + JSON.parse(files.readFile(settingsFile, 'utf8')) : {}; + const publicSettings = settings['public']; + + const meteorRelease = + release.current.isCheckout() ? "none" : release.current.name; + + let configDummy = {}; + if (publicSettings) { + configDummy.PUBLIC_SETTINGS = publicSettings; + } + + const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing']; + const calculatedHash = + WebAppHashing.calculateClientHash(manifest, null, configDummy); + + // XXX partially copied from autoupdate package + const version = process.env.AUTOUPDATE_VERSION || calculatedHash; + + const mobileServerUrl = this.options.mobileServerUrl; + + const runtimeConfig = { + meteorRelease: meteorRelease, + ROOT_URL: mobileServerUrl + "/", + // XXX propagate it from this.options? + ROOT_URL_PATH_PREFIX: '', + DDP_DEFAULT_CONNECTION_URL: mobileServerUrl, + autoupdateVersionCordova: version, + appId: this.projectContext.appIdentifier + }; + + if (publicSettings) + runtimeConfig.PUBLIC_SETTINGS = publicSettings; + + const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator']; + const boilerplate = new Boilerplate(CORDOVA_ARCH, manifest, { + urlMapper: _.identity, + pathMapper: (path) => files.convertToOSPath( + files.pathJoin(applicationPath, path)), + baseDataExtension: { + meteorRuntimeConfig: JSON.stringify( + encodeURIComponent(JSON.stringify(runtimeConfig))) + } + }); + + return boilerplate.toHTML(); + } + + copyBuildOverride() { + const buildOverridePath = + files.pathJoin(this.projectContext.projectDir, 'cordova-build-override'); + + if (files.exists(buildOverridePath) && + files.stat(buildOverridePath).isDirectory()) { + Console.debug('Copying over the cordova-build-override directory'); + files.cp_r(buildOverridePath, this.cordovaProject.projectRoot); + } + } +} + +function App(builder) { + /** + * @namespace App + * @global + * @summary The App configuration object in mobile-config.js + */ + return { + /** + * @summary Set your mobile app's core configuration information. + * @param {Object} options + * @param {String} [options.id,version,name,description,author,email,website] + * Each of the options correspond to a key in the app's core configuration + * as described in the [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_core_configuration_elements). + * @memberOf App + */ + info: function (options) { + // check that every key is meaningful + _.each(options, function (value, key) { + if (!_.has(builder.metadata, key)) + throw new Error("Unknown key in App.info configuration: " + key); + }); + + _.extend(builder.metadata, options); + }, + /** + * @summary Add a preference for your build as described in the + * [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_global_preferences). + * @param {String} name A preference name supported by Phonegap's + * `config.xml`. + * @param {String} value The value for that preference. + * @memberOf App + */ + setPreference: function (key, value) { + builder.additionalConfiguration[key] = value; + }, + + /** + * @summary Set the build-time configuration for a Phonegap plugin. + * @param {String} pluginName The identifier of the plugin you want to + * configure. + * @param {Object} config A set of key-value pairs which will be passed + * at build-time to configure the specified plugin. + * @memberOf App + */ + configurePlugin: function (pluginName, config) { + builder.pluginsConfiguration[pluginName] = config; + }, + + /** + * @summary Set the icons for your mobile app. + * @param {Object} icons An Object where the keys are different + * devices and screen sizes, and values are image paths + * relative to the project root directory. + * + * Valid key values: + * - `iphone` + * - `iphone_2x` + * - `iphone_3x` + * - `ipad` + * - `ipad_2x` + * - `android_ldpi` + * - `android_mdpi` + * - `android_hdpi` + * - `android_xhdpi` + * @memberOf App + */ + icons: function (icons) { + var validDevices = + _.keys(iconsIosSizes).concat(_.keys(iconsAndroidSizes)); + _.each(icons, function (value, key) { + if (!_.include(validDevices, key)) + throw new Error(key + ": unknown key in App.icons configuration."); + }); + _.extend(builder.imagePaths.icon, icons); + }, + + /** + * @summary Set the launch screen images for your mobile app. + * @param {Object} launchScreens A dictionary where keys are different + * devices, screen sizes, and orientations, and the values are image paths + * relative to the project root directory. + * + * For Android, launch screen images should + * be special "Nine-patch" image files that specify how they should be + * stretched. See the [Android docs](https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch). + * + * Valid key values: + * - `iphone` + * - `iphone_2x` + * - `iphone5` + * - `iphone6` + * - `iphone6p_portrait` + * - `iphone6p_landscape` + * - `ipad_portrait` + * - `ipad_portrait_2x` + * - `ipad_landscape` + * - `ipad_landscape_2x` + * - `android_ldpi_portrait` + * - `android_ldpi_landscape` + * - `android_mdpi_portrait` + * - `android_mdpi_landscape` + * - `android_hdpi_portrait` + * - `android_hdpi_landscape` + * - `android_xhdpi_portrait` + * - `android_xhdpi_landscape` + * + * @memberOf App + */ + launchScreens: function (launchScreens) { + var validDevices = + _.keys(launchIosSizes).concat(_.keys(launchAndroidSizes)); + + _.each(launchScreens, function (value, key) { + if (!_.include(validDevices, key)) + throw new Error(key + ": unknown key in App.launchScreens configuration."); + }); + _.extend(builder.imagePaths.splash, launchScreens); + }, + + /** + * @summary Set a new access rule based on origin domain for your app. + * By default your application has a limited list of servers it can contact. + * Use this method to extend this list. + * + * Default access rules: + * + * - `tel:*`, `geo:*`, `mailto:*`, `sms:*`, `market:*` are allowed and + * launch externally (phone app, or an email client on Android) + * - `gap:*`, `cdv:*`, `file:` are allowed (protocols required to access + * local file-system) + * - `http://meteor.local/*` is allowed (a domain Meteor uses to access + * app's assets) + * - The domain of the server passed to the build process (or local ip + * address in the development mode) is used to be able to contact the + * Meteor app server. + * + * Read more about domain patterns in [Cordova + * docs](http://cordova.apache.org/docs/en/4.0.0/guide_appdev_whitelist_index.md.html). + * + * Starting with Meteor 1.0.4 access rule for all domains and protocols + * (``) is no longer set by default due to + * [certain kind of possible + * attacks](http://cordova.apache.org/announcements/2014/08/04/android-351.html). + * + * @param {String} domainRule The pattern defining affected domains or URLs. + * @param {Object} [options] + * @param {Boolean} options.launchExternal Set to true if the matching URL + * should be handled externally (e.g. phone app or email client on Android). + * @memberOf App + */ + accessRule: function (domainRule, options) { + options = options || {}; + options.launchExternal = !!options.launchExternal; + if (options.launchExternal) { + builder.accessRules[domainRule] = 'external'; + } else { + builder.accessRules[domainRule] = true; + } + } + }; +} diff --git a/tools/cordova/cordova-runner.js b/tools/cordova/cordova-runner.js deleted file mode 100644 index e6e30bee666..00000000000 --- a/tools/cordova/cordova-runner.js +++ /dev/null @@ -1,64 +0,0 @@ -import _ from 'underscore'; -import { Console } from '../console.js'; - -// This is a runner, that we pass to Runner (run-all.js) -export default class CordovaRunner { - constructor(projectContext, cordovaProject, options) { - this.projectContext = projectContext; - this.cordovaProject = cordovaProject; - this.options = options; - } - - get title() { - return `app on ${this.displayName}`; - } - - prestart() { - // OAuth2 packages don't work so well with any mobile platform except the iOS - // simulator. Print a warning and direct users to the wiki page for help. (We - // do this now instead of in start() so we don't have to worry about - // projectContext being asynchronously reset.) - if (!(this.platform === "ios" && this.isDevice) && - this.projectContext.packageMap.getInfo('oauth2')) { - Console.warn(); - Console.labelWarn( - "It looks like you are using OAuth2 login in your app. " + - "Meteor's OAuth2 implementation does not currently work with " + - "mobile apps in local development mode, except in the iOS " + - "simulator. You can run the iOS simulator with 'meteor run ios'. " + - "For additional workarounds, see " + - Console.url( - "https://github.com/meteor/meteor/wiki/" + - "OAuth-for-mobile-Meteor-clients.")); - } - - // If we are targeting the remote devices, warn about ports and same network - if (this.isDevice) { - Console.warn(); - Console.labelWarn( - "You are testing your app on a remote device. " + - "For the mobile app to be able to connect to the local server, make " + - "sure your device is on the same network, and that the network " + - "configuration allows clients to talk to each other " + - "(no client isolation)."); - } - } - - start() { - Console.debug('Running Cordova for target', this.displayName); - - try { - Promise.await(this.run(this.options)); - } catch (err) { - Console.error(`${this.displayName}: failed to start the app.`, - err.message); - } - - try { - Promise.await(this.tailLogs(this.options)); - } catch (err) { - Console.error(`${this.displayName}: failed to tail logs.`, - err.message); - } - } -} diff --git a/tools/cordova/index.js b/tools/cordova/index.js new file mode 100644 index 00000000000..ebda15a3e89 --- /dev/null +++ b/tools/cordova/index.js @@ -0,0 +1,43 @@ +import _ from 'underscore'; + +export const CORDOVA_ARCH = "web.cordova"; + +export const AVAILABLE_PLATFORMS = ['ios', 'android']; + +const PLATFORM_TO_DISPLAY_NAME_MAP = { + 'ios': 'iOS', + 'android': 'Android' +}; + +export function displayNameForPlatform(platform) { + return PLATFORM_TO_DISPLAY_NAME_MAP[platform] || platform; +}; + +export function filterPlatforms(platforms) { + return _.intersection(platforms, AVAILABLE_PLATFORMS); +} + +export function splitPluginsAndPackages(packages) { + let result = { + plugins: [], + packages: [] + }; + + for (package of packages) { + const [namespace, ...rest] = package.split(':'); + if (namespace === 'cordova') { + const name = rest.join(':'); + result.plugins.push(name); + } else { + result.packages.push(package); + } + } + + return result; +} + +// Returns the cordovaDependencies of the Cordova arch from a star manifest. +export function pluginsFromStarManifest(star) { + var cordovaProgram = _.findWhere(star.programs, { arch: CORDOVA_ARCH }); + return cordovaProgram ? cordovaProgram.cordovaDependencies : {}; +} diff --git a/tools/cordova/ios-runner.js b/tools/cordova/ios-runner.js deleted file mode 100644 index 3ee319d1b17..00000000000 --- a/tools/cordova/ios-runner.js +++ /dev/null @@ -1,86 +0,0 @@ -import _ from 'underscore'; -import chalk from 'chalk'; -import { Console } from '../console.js'; -import files from '../fs/files.js'; -import isopackets from '../tool-env/isopackets.js' - -import CordovaRunner from './cordova-runner.js' -import { execFileSyncOrThrow, execFileAsyncOrThrow } from './utils.js' - -export default class iOSRunner extends CordovaRunner { - constructor(projectContext, cordovaProject, isDevice, options) { - super(projectContext, cordovaProject, options); - this.isDevice = isDevice; - } - - get platform() { - return 'ios'; - } - - get displayName() { - return this.isDevice ? 'iOS Device' : 'iOS Simulator'; - } - - async run(options = {}) { - // ios-deploy is super buggy, so we just open xcode and let the user - // start the app themselves. - if (this.isDevice) { - openInXcode(files.pathJoin(this.cordovaProject.projectRoot, 'platforms', 'ios')); - } else { - const cordovaBinPath = files.convertToOSPath( - files.pathJoin(files.getCurrentToolsDir(), - 'packages/cordova/.npm/package/node_modules/.bin')); - return this.cordovaProject.run(this.platform, this.isDevice, - _.extend(options, { extraPaths: [cordovaBinPath] })); - } - } - - async tailLogs(options) { - var logFilePath = - files.pathJoin(this.cordovaProject.projectRoot, 'platforms', 'ios', 'cordova', 'console.log'); - Console.debug('Printing logs for ios emulator, tailing file', logFilePath); - - // overwrite the file so we don't have to print the old logs - files.writeFile(logFilePath, ''); - // print the log file - execFileAsyncOrThrow('tail', ['-f', logFilePath], { - verbose: true, - lineMapper: null - }); - } -} - -function openInXcode(projectDir) { - // XXX this is buggy if your app directory is under something with a space, - // because the this.projectRoot part is not quoted for sh! - args = ['-c', 'open ' + - '"' + projectDir.replace(/"/g, "\\\"") + '"/*.xcodeproj']; - - try { - execFileSyncOrThrow('sh', args); - } catch (err) { - Console.error(); - Console.error(chalk.green("Could not open your project in Xcode.")); - Console.error(chalk.green("Try running again with the --verbose option.")); - Console.error( - chalk.green("Instructions for running your app on an iOS device: ") + - Console.url( - "https://github.com/meteor/meteor/wiki/" + - "How-to-run-your-app-on-an-iOS-device") - ); - Console.error(); - process.exit(2); - } - - Console.info(); - Console.info( - chalk.green( - "Your project has been opened in Xcode so that you can run your " + - "app on an iOS device. For further instructions, visit this " + - "wiki page: ") + - Console.url( - "https://github.com/meteor/meteor/wiki/" + - "How-to-run-your-app-on-an-iOS-device" - )); - Console.info(); -} diff --git a/tools/cordova/mobile-control-file.js b/tools/cordova/mobile-control-file.js deleted file mode 100644 index 52898225a6e..00000000000 --- a/tools/cordova/mobile-control-file.js +++ /dev/null @@ -1,441 +0,0 @@ -import _ from 'underscore'; -import { Console } from '../console.js'; -import files from '../fs/files.js'; -import isopackets from '../tool-env/isopackets.js' - -// Hard-coded constants -var iconIosSizes = { - 'iphone': '60x60', - 'iphone_2x': '120x120', - 'iphone_3x': '180x180', - 'ipad': '76x76', - 'ipad_2x': '152x152' -}; - -var iconAndroidSizes = { - 'android_ldpi': '36x36', - 'android_mdpi': '42x42', - 'android_hdpi': '72x72', - 'android_xhdpi': '96x96' -}; - -var launchIosSizes = { - 'iphone': '320x480', - 'iphone_2x': '640x960', - 'iphone5': '640x1136', - 'iphone6': '750x1334', - 'iphone6p_portrait': '1242x2208', - 'iphone6p_landscape': '2208x1242', - 'ipad_portrait': '768x1004', - 'ipad_portrait_2x': '1536x2008', - 'ipad_landscape': '1024x748', - 'ipad_landscape_2x': '2048x1496' -}; - -var launchAndroidSizes = { - 'android_ldpi_portrait': '320x426', - 'android_ldpi_landscape': '426x320', - 'android_mdpi_portrait': '320x470', - 'android_mdpi_landscape': '470x320', - 'android_hdpi_portrait': '480x640', - 'android_hdpi_landscape': '640x480', - 'android_xhdpi_portrait': '720x960', - 'android_xhdpi_landscape': '960x720' -}; - -// Given the mobile control file converts it to the Phongep/Cordova project's -// config.xml file and copies the necessary files (icons and launch screens) to -// the correct build location. Replaces all the old resources. -export function processMobileControlFile(controlFilePath, projectContext, cordovaProject, serverDomain) { - Console.debug('Processing the mobile control file'); - - // clean up the previous settings and resources - files.rm_recursive(files.pathJoin(cordovaProject.projectRoot, 'resources')); - - var code = ''; - - if (files.exists(controlFilePath)) { - // read the file if it exists - code = files.readFile(controlFilePath, 'utf8'); - } - - var defaultBuildNumber = (Date.now() % 1000000).toString(); - var metadata = { - id: 'com.id' + projectContext.appIdentifier, - version: '0.0.1', - buildNumber: defaultBuildNumber, - name: cordovaProject.appName, - description: 'New Meteor Mobile App', - author: 'A Meteor Developer', - email: 'n/a', - website: 'n/a' - }; - - // set some defaults different from the Phonegap/Cordova defaults - var additionalConfiguration = { - 'webviewbounce': false, - 'DisallowOverscroll': true, - 'deployment-target': '7.0' - }; - - if (projectContext.packageMap.getInfo('launch-screen')) { - additionalConfiguration.AutoHideSplashScreen = false; - additionalConfiguration.SplashScreen = 'screen'; - additionalConfiguration.SplashScreenDelay = 10000; - } - - // Defaults are Meteor meatball images located in tools/cordova/assets directory - var assetsPath = files.pathJoin(__dirname, 'assets'); - var iconsPath = files.pathJoin(assetsPath, 'icons'); - var launchscreensPath = files.pathJoin(assetsPath, 'launchscreens'); - var imagePaths = { - icon: {}, - splash: {} - }; - - // Default access rules for plain Meteor-Cordova apps. - // Rules can be extended with mobile-config API described below. - // The value is `true` if the protocol or domain should be allowed, - // 'external' if should handled externally. - var accessRules = { - // Allow external calls to things like email client or maps app or a - // phonebook app. - 'tel:*': 'external', - 'geo:*': 'external', - 'mailto:*': 'external', - 'sms:*': 'external', - 'market:*': 'external', - - // phonegap/cordova related protocols - // "file:" protocol is used to access first files from disk - 'file:*': true, - 'cdv:*': true, - 'gap:*': true, - - // allow Meteor's local emulated server url - this is the url from which the - // application loads its assets - 'http://meteor.local/*': true - }; - - // If the remote server domain is known, allow access to it for xhr and DDP - // connections. - if (serverDomain) { - accessRules['*://' + serverDomain + '/*'] = true; - // Android talks to localhost over 10.0.2.2. This config file is used for - // multiple platforms, so any time that we say the server is on localhost we - // should also say it is on 10.0.2.2. - if (serverDomain === 'localhost') { - accessRules['*://10.0.2.2/*'] = true; - } - } - - var setIcon = function (size, name) { - imagePaths.icon[name] = files.pathJoin(iconsPath, size + '.png'); - }; - var setLaunch = function (size, name) { - imagePaths.splash[name] = files.pathJoin(launchscreensPath, size + '.png'); - }; - - _.each(iconIosSizes, setIcon); - _.each(iconAndroidSizes, setIcon); - _.each(launchIosSizes, setLaunch); - _.each(launchAndroidSizes, setLaunch); - - /** - * @namespace App - * @global - * @summary The App configuration object in mobile-config.js - */ - var App = { - /** - * @summary Set your mobile app's core configuration information. - * @param {Object} options - * @param {String} [options.id,version,name,description,author,email,website] - * Each of the options correspond to a key in the app's core configuration - * as described in the [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_core_configuration_elements). - * @memberOf App - */ - info: function (options) { - // check that every key is meaningful - _.each(options, function (value, key) { - if (!_.has(metadata, key)) - throw new Error("Unknown key in App.info configuration: " + key); - }); - - _.extend(metadata, options); - }, - /** - * @summary Add a preference for your build as described in the - * [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_global_preferences). - * @param {String} name A preference name supported by Phonegap's - * `config.xml`. - * @param {String} value The value for that preference. - * @memberOf App - */ - setPreference: function (key, value) { - additionalConfiguration[key] = value; - }, - - /** - * @summary Set the build-time configuration for a Phonegap plugin. - * @param {String} pluginName The identifier of the plugin you want to - * configure. - * @param {Object} config A set of key-value pairs which will be passed - * at build-time to configure the specified plugin. - * @memberOf App - */ - configurePlugin: function (pluginName, config) { - pluginsConfiguration[pluginName] = config; - }, - - /** - * @summary Set the icons for your mobile app. - * @param {Object} icons An Object where the keys are different - * devices and screen sizes, and values are image paths - * relative to the project root directory. - * - * Valid key values: - * - `iphone` - * - `iphone_2x` - * - `iphone_3x` - * - `ipad` - * - `ipad_2x` - * - `android_ldpi` - * - `android_mdpi` - * - `android_hdpi` - * - `android_xhdpi` - * @memberOf App - */ - icons: function (icons) { - var validDevices = - _.keys(iconIosSizes).concat(_.keys(iconAndroidSizes)); - _.each(icons, function (value, key) { - if (!_.include(validDevices, key)) - throw new Error(key + ": unknown key in App.icons configuration."); - }); - _.extend(imagePaths.icon, icons); - }, - - /** - * @summary Set the launch screen images for your mobile app. - * @param {Object} launchScreens A dictionary where keys are different - * devices, screen sizes, and orientations, and the values are image paths - * relative to the project root directory. - * - * For Android, launch screen images should - * be special "Nine-patch" image files that specify how they should be - * stretched. See the [Android docs](https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch). - * - * Valid key values: - * - `iphone` - * - `iphone_2x` - * - `iphone5` - * - `iphone6` - * - `iphone6p_portrait` - * - `iphone6p_landscape` - * - `ipad_portrait` - * - `ipad_portrait_2x` - * - `ipad_landscape` - * - `ipad_landscape_2x` - * - `android_ldpi_portrait` - * - `android_ldpi_landscape` - * - `android_mdpi_portrait` - * - `android_mdpi_landscape` - * - `android_hdpi_portrait` - * - `android_hdpi_landscape` - * - `android_xhdpi_portrait` - * - `android_xhdpi_landscape` - * - * @memberOf App - */ - launchScreens: function (launchScreens) { - var validDevices = - _.keys(launchIosSizes).concat(_.keys(launchAndroidSizes)); - - _.each(launchScreens, function (value, key) { - if (!_.include(validDevices, key)) - throw new Error(key + ": unknown key in App.launchScreens configuration."); - }); - _.extend(imagePaths.splash, launchScreens); - }, - - /** - * @summary Set a new access rule based on origin domain for your app. - * By default your application has a limited list of servers it can contact. - * Use this method to extend this list. - * - * Default access rules: - * - * - `tel:*`, `geo:*`, `mailto:*`, `sms:*`, `market:*` are allowed and - * launch externally (phone app, or an email client on Android) - * - `gap:*`, `cdv:*`, `file:` are allowed (protocols required to access - * local file-system) - * - `http://meteor.local/*` is allowed (a domain Meteor uses to access - * app's assets) - * - The domain of the server passed to the build process (or local ip - * address in the development mode) is used to be able to contact the - * Meteor app server. - * - * Read more about domain patterns in [Cordova - * docs](http://cordova.apache.org/docs/en/4.0.0/guide_appdev_whitelist_index.md.html). - * - * Starting with Meteor 1.0.4 access rule for all domains and protocols - * (``) is no longer set by default due to - * [certain kind of possible - * attacks](http://cordova.apache.org/announcements/2014/08/04/android-351.html). - * - * @param {String} domainRule The pattern defining affected domains or URLs. - * @param {Object} [options] - * @param {Boolean} options.launchExternal Set to true if the matching URL - * should be handled externally (e.g. phone app or email client on Android). - * @memberOf App - */ - accessRule: function (domainRule, options) { - options = options || {}; - options.launchExternal = !!options.launchExternal; - if (options.launchExternal) { - accessRules[domainRule] = 'external'; - } else { - accessRules[domainRule] = true; - } - } - }; - - try { - Console.debug('Running the mobile control file'); - files.runJavaScript(code, { - filename: 'mobile-config.js', - symbols: { App: App } - }); - } catch (err) { - throw new Error('Error reading mobile-config.js:' + err.stack); - } - - const { XmlBuilder } = isopackets.load('cordova-support')['xmlbuilder']; - var config = XmlBuilder.create('widget'); - - _.each({ - id: metadata.id, - version: metadata.version, - 'android-versionCode': metadata.buildNumber, - 'ios-CFBundleVersion': metadata.buildNumber, - xmlns: 'http://www.w3.org/ns/widgets', - 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0' - }, function (val, key) { - config.att(key, val); - }); - - // set all the metadata - config.ele('name').txt(metadata.name); - config.ele('description').txt(metadata.description); - config.ele('author', { - href: metadata.website, - email: metadata.email - }).txt(metadata.author); - - // set the additional configuration preferences - _.each(additionalConfiguration, function (value, key) { - config.ele('preference', { - name: key, - value: value.toString() - }); - }); - - // load from index.html by default - config.ele('content', { src: 'index.html' }); - - // Copy all the access rules - _.each(accessRules, function (rule, pattern) { - var opts = { origin: pattern }; - if (rule === 'external') - opts['launch-external'] = true; - - config.ele('access', opts); - }); - - var iosPlatform = config.ele('platform', { name: 'ios' }); - var androidPlatform = config.ele('platform', { name: 'android' }); - - // Prepare the resources folder - var resourcesPath = files.pathJoin(cordovaProject.projectRoot, 'resources'); - files.rm_recursive(resourcesPath); - files.mkdir_p(resourcesPath); - - Console.debug('Copying resources for mobile apps'); - - var imageXmlRec = function (name, width, height, src) { - var androidMatch = /android_(.?.dpi)_(landscape|portrait)/g.exec(name); - var xmlRec = { - src: src, - width: width, - height: height - }; - - // XXX special case for Android - if (androidMatch) - xmlRec.density = androidMatch[2].substr(0, 4) + '-' + androidMatch[1]; - - return xmlRec; - }; - var setImages = function (sizes, xmlEle, tag) { - _.each(sizes, function (size, name) { - var width = size.split('x')[0]; - var height = size.split('x')[1]; - - var suppliedPath = imagePaths[tag][name]; - if (!suppliedPath) - return; - - var suppliedFilename = _.last(suppliedPath.split(files.pathSep)); - var extension = _.last(suppliedFilename.split('.')); - - // XXX special case for 9-patch png's - if (suppliedFilename.match(/\.9\.png$/)) { - extension = '9.png'; - } - - var fileName = name + '.' + tag + '.' + extension; - var src = files.pathJoin('resources', fileName); - - // copy the file to the build folder with a standardized name - files.copyFile(files.pathResolve(projectContext.projectDir, suppliedPath), - files.pathJoin(resourcesPath, fileName)); - - // set it to the xml tree - xmlEle.ele(tag, imageXmlRec(name, width, height, src)); - - // XXX reuse one size for other dimensions - var dups = { - '60x60': ['29x29', '40x40', '50x50', '57x57', '58x58'], - '76x76': ['72x72'], - '152x152': ['144x144'], - '120x120': ['80x80', '100x100', '114x114'], - '768x1004': ['768x1024'], - '1536x2008': ['1536x2048'], - '1024x748': ['1024x768'], - '2048x1496': ['2048x1536'] - }[size]; - - // just use the same image - _.each(dups, function (size) { - width = size.split('x')[0]; - height = size.split('x')[1]; - // XXX this is fine to not supply a name since it is always iOS, but - // this is a hack right now. - xmlEle.ele(tag, imageXmlRec('n/a', width, height, src)); - }); - }); - }; - - // add icons and launch screens to config and copy the files on fs - setImages(iconIosSizes, iosPlatform, 'icon'); - setImages(iconAndroidSizes, androidPlatform, 'icon'); - setImages(launchIosSizes, iosPlatform, 'splash'); - setImages(launchAndroidSizes, androidPlatform, 'splash'); - - var formattedXmlConfig = config.end({ pretty: true }); - var configPath = files.pathJoin(cordovaProject.projectRoot, 'config.xml'); - - Console.debug('Writing new config.xml'); - files.writeFile(configPath, formattedXmlConfig, 'utf8'); -}; diff --git a/tools/cordova/platforms.js b/tools/cordova/platforms.js deleted file mode 100644 index 1a931bdef53..00000000000 --- a/tools/cordova/platforms.js +++ /dev/null @@ -1,117 +0,0 @@ -import _ from 'underscore'; -import chalk from 'chalk'; -import main from '../cli/main.js'; -import { Console } from '../console.js'; -import { ProjectContext, PlatformList } from '../project-context.js'; -import buildmessage from '../buildmessage.js'; - -export const AVAILABLE_PLATFORMS = PlatformList.DEFAULT_PLATFORMS.concat( - ['android', 'ios']); - -const PLATFORM_TO_DISPLAY_NAME_MAP = { - 'ios': 'iOS', - 'android': 'Android' -}; - -export function displayNameForPlatform(platform) { - return PLATFORM_TO_DISPLAY_NAME_MAP[platform] || platform; -}; - -export function platformsForTargets(targets) { - targets = _.uniq(targets); - - var platforms = []; - // Find the platforms that correspond to the targets - // ie. ["ios", "android", "ios-device"] will produce ["ios", "android"] - _.each(targets, function (targetName) { - var platform = targetName.split('-')[0]; - if (!_.contains(platforms, platform)) { - platforms.push(platform); - } - }); - - return platforms; -}; - -// Ensures that the Cordova platforms are synchronized with the app-level -// platforms. -export function ensureCordovaPlatformsAreSynchronized(cordovaProject, platforms) { - // Filter out the default platforms, leaving the Cordova platforms - platforms = _.difference(platforms, PlatformList.DEFAULT_PLATFORMS); - const installedPlatforms = cordovaProject.getInstalledPlatforms(); - - for (platform of platforms) { - if (_.contains(installedPlatforms, platform)) continue; - - buildmessage.enterJob(`Adding platform: ${platform}`, () => { - Promise.await(cordovaProject.addPlatform(platform)); - }); - } - - for (platform of installedPlatforms) { - if (!_.contains(platforms, platform) && - _.contains(AVAILABLE_PLATFORMS, platform)) { - buildmessage.enterJob(`Removing platform: ${platform}`, () => { - Promise.await(cordovaProject.removePlatform(platform)); - }); - } - } -}; - -export function checkPlatformRequirements(cordovaProject, platform) { - const requirements = Promise.await(cordovaProject.checkRequirements([platform])); - let platformRequirements = requirements[platform]; - - if (!platformRequirements) { - Console.warn("Could not check platform requirements"); - return; - } - - // We don't use ios-deploy, but open Xcode to run on a device instead - platformRequirements = _.reject(platformRequirements, requirement => requirement.id === 'ios-deploy'); - - const satisifed = _.every(platformRequirements, requirement => requirement.installed); - - if (!satisifed) { - Console.info(`Make sure all installation requirements are satisfied - before running or building for ${displayNameForPlatform(platform)}:`); - for (requirement of platformRequirements) { - const name = requirement.name; - if (requirement.installed) { - Console.success(name); - } else { - const reason = requirement.metadata && requirement.metadata.reason; - if (reason) { - Console.failInfo(`${name}: ${reason}`); - } else { - Console.failInfo(name); - } - } - } - } - - return satisifed; -} - -// Filter out unsupported Cordova platforms, and exit if platform hasn't been -// added to the project yet -export function checkCordovaPlatforms(projectContext, platforms) { - var cordovaPlatformsInProject = projectContext.platformList.getCordovaPlatforms(); - return _.filter(platforms, function (platform) { - var inProject = _.contains(cordovaPlatformsInProject, platform); - - if (platform === 'ios' && process.platform !== 'darwin') { - Console.warn("Currently, it is only possible to build iOS apps on an OS X system."); - return false; - } - - if (!inProject) { - Console.warn("Please add the " + displayNameForPlatform(platform) + - " platform to your project first."); - Console.info("Run: " + Console.command("meteor add-platform " + platform)); - throw new main.ExitWithCode(2); - } - - return true; - }); -} diff --git a/tools/cordova/plugins.js b/tools/cordova/plugins.js deleted file mode 100644 index b41e43f19cc..00000000000 --- a/tools/cordova/plugins.js +++ /dev/null @@ -1,107 +0,0 @@ -import _ from 'underscore'; -import { Console } from '../console.js'; -import buildmessage from '../buildmessage.js'; -import files from '../fs/files.js'; -import utils from '../utils/utils.js'; - -// packages - list of strings -export function filterCordovaPackages(packages) { -// We hard-code the 'cordova' namespace - var ret = { - rest: [], - plugins: [] - }; - - _.each(packages, function (p) { - var namespace = p.split(':')[0]; - var name = p.split(':').slice(1).join(':'); - if (namespace === 'cordova') { - ret.plugins.push(name); - } else { - ret.rest.push(p); // leave it the same - } - }); - return ret; -} - -// Ensures that the Cordova plugins are synchronized with the app-level -// plugins. -export function ensureCordovaPluginsAreSynchronized(cordovaProject, plugins, - pluginsConfiguration = {}) { - Console.debug('Ensuring that the Cordova plugins are synchronized with the app-level plugins', plugins); - - var installedPlugins = Promise.await(cordovaProject.getInstalledPlugins()); - - // 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. - var shouldReinstallPlugins = false; - - // 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 pluginsFromLocalPath = {}; - _.each(plugins, function (version, name) { - // Check if plugin is installed from local path - let pluginFromLocalPath = utils.isUrlWithFileScheme(version); - if (pluginFromLocalPath) { - 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 && !pluginFromLocalPath)) { - // The version of the plugin has changed, or we do not contain a plugin. - shouldReinstallPlugins = true; - } - }); - - if (!_.isEmpty(pluginsFromLocalPath)) { - Console.debug('Reinstalling Cordova plugins added from the local path'); - } - - // 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)) { - shouldReinstallPlugins = true; - } - }); - - if (shouldReinstallPlugins || !_.isEmpty(pluginsFromLocalPath)) { - buildmessage.enterJob({ title: "installing Cordova plugins"}, function () { - installedPlugins = Promise.await(cordovaProject.getInstalledPlugins()); - - if (shouldReinstallPlugins) { - cordovaProject.removePlugins(installedPlugins); - } else { - cordovaProject.removePlugins(pluginsFromLocalPath); - } - - // Now install necessary plugins. - 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(pluginsToInstall, function (version, name) { - Promise.await(cordovaProject.addPlugin(name, version, pluginsConfiguration[name])); - - buildmessage.reportProgress({ - current: ++pluginsInstalled, - end: pluginsCount - }); - }); - }); - } -}; diff --git a/tools/cordova/project.js b/tools/cordova/project.js index 2d0425de3eb..157a54e147c 100644 --- a/tools/cordova/project.js +++ b/tools/cordova/project.js @@ -1,12 +1,20 @@ import _ from 'underscore'; +import util from 'util'; +import path from 'path'; +import assert from 'assert'; import chalk from 'chalk'; + import isopackets from '../tool-env/isopackets.js' import files from '../fs/files.js'; import utils from '../utils/utils.js'; import { Console } from '../console/console.js'; import buildmessage from '../utils/buildmessage.js'; +import main from '../cli/main.js'; import httpHelpers from '../utils/http-helpers.js'; +import { AVAILABLE_PLATFORMS, displayNameForPlatform } from './index.js'; +import { CordovaBuilder } from './builder.js'; + function loadDependenciesFromCordovaPackageIfNeeded() { if (typeof Cordova !== 'undefined') return; @@ -15,104 +23,214 @@ function loadDependenciesFromCordovaPackageIfNeeded() { events.on('results', logIfVerbose); events.on('log', logIfVerbose); - events.on('warn', console.warn); + events.on('warn', log); events.on('verbose', logIfVerbose); } -const logIfVerbose = (...args) => { +function logIfVerbose(...args) { if (Console.verbose) { - console.log(args); + log(...args); } }; -// Creates a Cordova project if necessary. -export function createCordovaProjectIfNecessary(projectContext) { - const cordovaPath = projectContext.getProjectLocalDirectory('cordova-build'); - const appName = files.pathBasename(projectContext.projectDir); - const cordovaProject = new CordovaProject(cordovaPath, appName); - - if (!files.exists(cordovaPath)) { - Console.debug('Cordova project doesn\'t exist, creating one'); - files.mkdir_p(files.pathDirname(cordovaPath)); - Promise.await(cordovaProject.create()); - } - - return cordovaProject; -}; +function log(...args) { + Console.rawInfo(`%% ${util.format.apply(null, args)}\n`); +} -export default class CordovaProject { - constructor(projectRoot, appName) { +export class CordovaProject { + constructor(projectContext, appName = files.pathBasename(projectContext.projectDir)) { loadDependenciesFromCordovaPackageIfNeeded(); - this.projectRoot = projectRoot; + this.projectContext = projectContext; + + this.projectRoot = projectContext.getProjectLocalDirectory('cordova-build'); this.appName = appName; this.pluginsDir = files.pathJoin(this.projectRoot, 'plugins'); this.localPluginsDir = files.pathJoin(this.projectRoot, 'local-plugins'); this.tarballPluginsLockPath = files.pathJoin(this.projectRoot, 'cordova-tarball-plugins.json'); - } - async create() { - // Cordova app identifiers have to look like Java namespaces. - // Change weird characters (especially hyphens) into underscores. - const appId = 'com.meteor.userapps.' + this.appName.replace(/[^a-zA-Z\d_$.]/g, '_'); - return await cordova.raw.create(files.convertToOSPath(this.projectRoot), appId, this.appName); + this.createIfNeeded(); } - chdirToProjectRoot() { - process.chdir(files.convertToOSPath(this.projectRoot)); + // Creating + + createIfNeeded() { + buildmessage.assertInCapture(); + + if (!files.exists(this.projectRoot)) { + buildmessage.enterJob({ title: "creating Cordova project" }, () => { + files.mkdir_p(files.pathDirname(this.projectRoot)); + // Cordova app identifiers have to look like Java namespaces. + // Change weird characters (especially hyphens) into underscores. + const appId = 'com.meteor.userapps.' + this.appName.replace(/[^a-zA-Z\d_$.]/g, '_'); + + // Don't set cwd to project root in runCommands because it doesn't exist yet + this.runCommands(async () => { + await cordova.raw.create(files.convertToOSPath(this.projectRoot), appId, this.appName); + }, this.defaultEnvWithPathsAdded(), null); + }); + } } - get defaultOptions() { - return { silent: !Console.verbose, verbose: Console.verbose }; + // Preparing + + prepare(bundlePath, plugins, options = {}) { + assert(bundlePath); + assert(plugins); + + Console.debug('Preparing Cordova project'); + + const builder = new CordovaBuilder(this, bundlePath, plugins, options); + builder.start(); } - env(...extraPaths) { - let paths = (this.defaultPaths || []); - paths.unshift(...extraPaths); - const env = files.currentEnvWithPathsAdded(...paths); - return env; + // Building + + build(platforms = this.installedPlatforms, options = [], extraPaths) { + const env = this.defaultEnvWithPathsAdded(...extraPaths); + const commandOptions = _.extend(this.defaultOptions, + { platforms: platforms, options: options }); + + Console.debug('Building Cordova project', commandOptions); + + this.runCommands(async () => { + await cordova.raw.build(commandOptions); + }); } - get defaultPaths() { - const nodeBinDir = files.getCurrentNodeBinDir(); - return [nodeBinDir]; + // Running + + async run(platform, isDevice, options = [], extraPaths) { + const env = this.defaultEnvWithPathsAdded(...extraPaths); + const commandOptions = _.extend(this.defaultOptions, + { platforms: [platform], options: options }); + + Console.debug('Running Cordova project', commandOptions); + + + this.runCommands(async () => { + if (isDevice) { + await cordova.raw.run(commandOptions); + } else { + await cordova.raw.emulate(commandOptions); + } + }, env); } // Platforms - async checkRequirements(platforms = null) { - this.chdirToProjectRoot(); - superspawn.setEnv(this.env()); - return await cordova.raw.requirements(platforms, this.defaultOptions); + checkPlatformRequirements(platform) { + if (platform === 'ios' && process.platform !== 'darwin') { + Console.warn("Currently, it is only possible to build iOS apps on an OS X system."); + return false; + } + + const installedPlatforms = this.installedPlatforms; + const inProject = _.contains(installedPlatforms, platform); + if (!inProject) { + Console.warn(`Please add the ${displayNameForPlatform(platform)} \ +platform to your project first.`); + Console.info(`Run: ${Console.command(`meteor add-platform ${platform}`)}`); + return false; + } + + const allRequirements = this.runCommands( + async () => { + return await cordova.raw.requirements([platform], this.defaultOptions); + }); + let requirements = allRequirements && allRequirements[platform]; + if (!requirements) { + Console.error(`Failed to check requirements for platform \ +${displayNameForPlatform(platform)}`); + return false; + } else if (requirements instanceof CordovaError) { + Console.error(`cordova: ${requirements.message}`); + return false; + } + + // We don't use ios-deploy, but open Xcode to run on a device instead + requirements = _.reject(requirements, requirement => requirement.id === 'ios-deploy'); + + const satisfied = _.every(requirements, requirement => requirement.installed); + if (!satisfied) { + Console.info(); + Console.info(`Make sure all installation requirements are satisfied \ +before running or building for ${displayNameForPlatform(platform)}:`); + for (requirement of requirements) { + const name = requirement.name; + if (requirement.installed) { + Console.success(name); + } else { + const reason = requirement.metadata && requirement.metadata.reason; + if (reason) { + Console.failInfo(`${name}: ${reason}`); + } else { + Console.failInfo(name); + } + } + } + } + return satisfied; } - getInstalledPlatforms() { + get installedPlatforms() { return cordova_util.listPlatforms(files.convertToOSPath(this.projectRoot)); } - async addPlatform(platform) { - this.chdirToProjectRoot(); - superspawn.setEnv(this.env()); - return await cordova.raw.platform('add', platform, this.defaultOptions); + updatePlatforms(platforms = this.installedPlatforms) { + this.runCommands(async () => { + await cordova.raw.platform('update', platforms, this.defaultOptions); + }); + } + + addPlatform(platform) { + this.runCommands(async () => { + await cordova.raw.platform('add', platform, this.defaultOptions); + }); + } + + removePlatform(platform) { + this.runCommands(async () => { + await cordova.raw.platform('rm', platform, this.defaultOptions); + }); } - async removePlatform(platform) { - this.chdirToProjectRoot(); - superspawn.setEnv(this.env()); - return await cordova.raw.platform('rm', platform, this.defaultOptions); + get cordovaPlatformsInApp() { + return this.projectContext.platformList.getCordovaPlatforms(); + } + + // Ensures that the Cordova platforms are synchronized with the app-level + // platforms. + ensurePlatformsAreSynchronized(platforms = this.cordovaPlatformsInApp) { + const installedPlatforms = this.installedPlatforms; + + for (platform of platforms) { + if (_.contains(installedPlatforms, platform)) continue; + + this.addPlatform(platform); + } + + for (platform of installedPlatforms) { + if (!_.contains(platforms, platform) && + _.contains(AVAILABLE_PLATFORMS, platform)) { + this.removePlatform(platform); + } + } } // Plugins - getInstalledPlugins() { - let pluginInfoProvider = new PluginInfoProvider(); - return _.object(_.map(pluginInfoProvider.getAllWithinSearchPath(files.convertToOSPath(this.pluginsDir)), plugin => { + get installedPlugins() { + const pluginInfoProvider = new PluginInfoProvider(); + const plugins = pluginInfoProvider.getAllWithinSearchPath( + files.convertToOSPath(this.pluginsDir)); + return _.object(plugins.map(plugin => { return [ plugin.id, plugin.version ]; })); } - async addPlugin(name, version, config) { + addPlugin(name, version, config) { let pluginTarget; if (version && utils.isUrlWithSha(version)) { pluginTarget = files.convertToOSPath(this.fetchCordovaPluginFromShaUrl(version, name)); @@ -123,6 +241,8 @@ export default class CordovaProject { pluginTarget = version ? `${name}@${version}` : name; } + Console.debug('Adding a Cordova plugin', pluginTarget); + let additionalArgs = []; _.each(config || {}, (value, variable) => { additionalArgs.push('--variable'); @@ -130,44 +250,42 @@ export default class CordovaProject { }); pluginTarget.concat(additionalArgs) - this.chdirToProjectRoot(); - superspawn.setEnv(this.env()); - return await cordova.raw.plugin('add', pluginTarget, this.defaultOptions); + this.runCommands(async () => { + await cordova.raw.plugin('add', pluginTarget, this.defaultOptions); + }); + + if (utils.isUrlWithSha(version)) { + Console.debug('Adding plugin to the tarball plugins lock', name); + let lock = this.getTarballPluginsLock(this.projectRoot); + lock[name] = version; + this.writeTarballPluginsLock(this.projectRoot, lock); + } } - async removePlugin(plugin, isFromTarballUrl = false) { - verboseLog('Removing a plugin', name); + removePlugin(plugin, isFromTarballUrl = false) { + Console.debug('Removing a Cordova plugin', plugin); - this.chdirToProjectRoot(); - superspawn.setEnv(this.env()); - await cordova.raw.plugin('rm', plugin, this.defaultOptions); + this.runCommands(async () => { + await cordova.raw.plugin('rm', plugin, this.defaultOptions); + }); if (isFromTarballUrl) { - Console.debug('Removing plugin from the tarball plugins lock', name); + Console.debug('Removing plugin from the tarball plugins lock', plugin); // also remove from tarball-url-based plugins lock - var lock = getTarballPluginsLock(this.projectRoot); + let lock = getTarballPluginsLock(this.projectRoot); delete lock[name]; writeTarballPluginsLock(this.projectRoot, lock); } } - async removePlugins(pluginsToRemove) { - Console.debug('Removing plugins', pluginsToRemove); - - // Loop through all of the plugins to remove and remove them one by one until - // we have deleted proper amount of plugins. It's necessary to loop because - // we might have dependencies between plugins. - while (pluginsToRemove.length > 0) { - await Promise.all(_.map(pluginsToRemove, (version, name) => { - removePlugin(name, utils.isUrlWithSha(version)); - })); - let installedPlugins = await this.installedPlugins(); - - uninstalledPlugins = _.difference( - Object.keys(pluginsToRemove), Object.keys(installedPlugins) - ); - plugins = _.omit(pluginsToRemove, uninstalledPlugins); - }; + removePlugins(pluginsToRemove) { + Console.debug('Removing Cordova plugins', pluginsToRemove); + + if (_.isEmpty(pluginsToRemove)) return; + + this.runCommands(async () => { + await cordova.raw.plugin('rm', Object.keys(pluginsToRemove), this.defaultOptions); + }); } getTarballPluginsLock() { @@ -205,7 +323,7 @@ export default class CordovaProject { } fetchCordovaPluginFromShaUrl(urlWithSha, pluginName) { - Console.debug('Fetching a tarball from url:', urlWithSha); + Console.debug('Fetching a Cordova plugin tarball from url:', urlWithSha); var pluginPath = files.pathJoin(this.localPluginsDir, pluginName); var pluginTarball = buildmessage.enterJob("downloading Cordova plugin", () => { @@ -246,34 +364,140 @@ export default class CordovaProject { getCordovaLocalPluginPath(pluginPath) { pluginPath = pluginPath.substr("file://".length); if (utils.isPathRelative(pluginPath)) { - return path.relative(this.projectRoot, path.resolve(projectDir, pluginPath)); + return path.relative( + this.projectRoot, + path.resolve(this.projectContext.projectDir, pluginPath)); } else { return pluginPath; } } - // Build the project - async build(options) { - this.chdirToProjectRoot(); + // Ensures that the Cordova plugins are synchronized with the app-level + // plugins. + ensurePluginsAreSynchronized(plugins, pluginsConfiguration = {}) { + buildmessage.assertInCapture(); + + Console.debug('Ensuring Cordova plugins are synchronized', plugins, + pluginsConfiguration); + + var installedPlugins = this.installedPlugins; + + // 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. + var shouldReinstallPlugins = false; + + // 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 pluginsFromLocalPath = {}; + _.each(plugins, (version, name) => { + // Check if plugin is installed from local path + let pluginFromLocalPath = utils.isUrlWithFileScheme(version); + if (pluginFromLocalPath) { + 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 && !pluginFromLocalPath)) { + // The version of the plugin has changed, or we do not contain a plugin. + shouldReinstallPlugins = true; + } + }); + + if (!_.isEmpty(pluginsFromLocalPath)) { + Console.debug('Reinstalling Cordova plugins added from the local path'); + } + + // Check to see if we have any installed plugins that are not in the current + // set of plugins. + _.each(installedPlugins, (version, name) => { + if (!_.has(plugins, name)) { + shouldReinstallPlugins = true; + } + }); + + if (shouldReinstallPlugins || !_.isEmpty(pluginsFromLocalPath)) { + buildmessage.enterJob({ title: "installing Cordova plugins"}, () => { + installedPlugins = this.installedPlugins; + + if (shouldReinstallPlugins) { + this.removePlugins(installedPlugins); + } else { + this.removePlugins(pluginsFromLocalPath); + } + + // Now install necessary plugins. + 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(pluginsToInstall, (version, name) => { + this.addPlugin(name, version, pluginsConfiguration[name]); + + buildmessage.reportProgress({ + current: ++pluginsInstalled, + end: pluginsCount + }); + }); + }); + } + }; + + // Cordova commands support + + get defaultOptions() { + return { silent: !Console.verbose, verbose: Console.verbose }; + } - superspawn.setEnv(this.env(...options.extraPaths)); - options = _.extend(this.defaultOptions, options); + defaultEnvWithPathsAdded(...extraPaths) { + let paths = (this.defaultPaths || []); + paths.unshift(...extraPaths); + const env = files.currentEnvWithPathsAdded(...paths); + return env; + } - return await cordova.raw.build(options); + get defaultPaths() { + const nodeBinDir = files.getCurrentNodeBinDir(); + return [nodeBinDir]; } - // Run the project - async run(platform, isDevice, options) { - this.chdirToProjectRoot(); + runCommands(asyncFunc, env = this.defaultEnvWithPathsAdded(), + cwd = this.projectRoot) { + const oldCwd = process.cwd(); + if (cwd) { + process.chdir(files.convertToOSPath(cwd)); + } - superspawn.setEnv(this.env(...options.extraPaths)); - options = _.extend(this.defaultOptions, options, - { platforms: [platform] }); + superspawn.setEnv(env); - if (isDevice) { - return await cordova.raw.run(options); - } else { - return await cordova.raw.emulate(options); + try { + return Promise.await(asyncFunc()); + } catch (error) { + if (error instanceof CordovaError) { + Console.error(`cordova: ${error.message}`); + Console.error(chalk.green("Try running again with the --verbose option \ +to help diagnose the issue.")); + throw new main.ExitWithCode(1); + } else { + throw error; + } + } finally { + if (oldCwd) { + process.chdir(oldCwd); + } } } } diff --git a/tools/cordova/run-targets.js b/tools/cordova/run-targets.js new file mode 100644 index 00000000000..20577f2cc66 --- /dev/null +++ b/tools/cordova/run-targets.js @@ -0,0 +1,92 @@ +import _ from 'underscore'; +import chalk from 'chalk'; +import child_process from 'child_process'; + +import runLog from '../runners/run-log.js'; +import { Console } from '../console/console.js'; +import files from '../fs/files.js'; + +export class CordovaRunTarget { + get title() { + return `app on ${this.displayName}`; + } +} + +export class iOSRunTarget extends CordovaRunTarget { + constructor(isDevice) { + super(); + this.platform = 'ios'; + this.isDevice = isDevice; + } + + get displayName() { + return this.isDevice ? "iOS Device" : "iOS Simulator"; + } + + async start(cordovaProject) { + // ios-deploy is super buggy, so we just open Xcode and let the user + // start the app themselves. + if (this.isDevice) { + openXcodeProject(files.pathJoin(cordovaProject.projectRoot, + 'platforms', 'ios', `${cordovaProject.appName}.xcodeproj`)); + } else { + // Add the cordova package npm bin path so Cordova can find ios-sim + const cordovaBinPath = files.convertToOSPath( + files.pathJoin(files.getCurrentToolsDir(), + 'packages/cordova/.npm/package/node_modules/.bin')); + + await cordovaProject.run(this.platform, this.isDevice, undefined, + [cordovaBinPath]); + + // Bring iOS Simulator to front + child_process.spawn('osascript', ['-e', + 'tell application "System Events" \ + to set frontmost of process "iOS Simulator" to true']); + } + } +} + +function openXcodeProject(projectPath) { + child_process.execFile('open', [projectPath], undefined, + (error, stdout, stderr) => { + if (error) { + Console.error(); + Console.error(chalk.green(`Failed to open your project in Xcode: +${error.message}`)); + Console.error( + chalk.green("Instructions for running your app on an iOS device: ") + + Console.url("https://github.com/meteor/meteor/wiki/" + + "How-to-run-your-app-on-an-iOS-device") + ); + Console.error(); + } else { + Console.info(); + Console.info( + chalk.green( + "Your project has been opened in Xcode so that you can run your " + + "app on an iOS device. For further instructions, visit this " + + "wiki page: ") + + Console.url( + "https://github.com/meteor/meteor/wiki/" + + "How-to-run-your-app-on-an-iOS-device" + )); + Console.info(); + } + }); +} + +export class AndroidRunTarget extends CordovaRunTarget { + constructor(isDevice) { + super(); + this.platform = 'android'; + this.isDevice = isDevice; + } + + get displayName() { + return this.isDevice ? "Android Device" : "Android Emulator"; + } + + async start(cordovaProject) { + await cordovaProject.run(this.platform, this.isDevice); + } +} diff --git a/tools/cordova/run.js b/tools/cordova/run.js deleted file mode 100644 index 81a5d2ff01f..00000000000 --- a/tools/cordova/run.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'underscore'; -import { Console } from '../console.js'; -import files from '../fs/files.js'; -import isopackets from '../tool-env/isopackets.js' - -import iOSRunner from './ios-runner.js'; -import AndroidRunner from './android-runner.js'; - -export function buildCordovaRunners(projectContext, cordovaProject, targets, options) { - return _.map(targets, (target) => { - let targetParts = target.split('-'); - const platform = targetParts[0]; - const isDevice = targetParts[1] === 'device'; - - if (platform == 'ios') { - return new iOSRunner(projectContext, cordovaProject, isDevice, options); - } else if (platform == 'android') { - return new AndroidRunner(projectContext, cordovaProject, isDevice, options); - } else { - throw new Error(`Unknown platform: ${platform}`); - } - }); -}; diff --git a/tools/cordova/runner.js b/tools/cordova/runner.js new file mode 100644 index 00000000000..92a0a8b920b --- /dev/null +++ b/tools/cordova/runner.js @@ -0,0 +1,112 @@ +import _ from 'underscore'; +import buildmessage from '../utils/buildmessage.js'; +import runLog from '../runners/run-log.js'; +import { Console } from '../console/console.js'; +import main from '../cli/main.js'; + +import { displayNameForPlatform, prepareProjectForBuild } from './index.js'; + +export class CordovaRunner { + constructor(cordovaProject, runTargets) { + this.cordovaProject = cordovaProject; + this.runTargets = runTargets; + } + + get projectContext() { + return this.cordovaProject.projectContext; + } + + get platformsForRunTargets() { + return _.uniq(this.runTargets.map((runTarget) => runTarget.platform)); + } + + checkPlatformsForRunTargets() { + this.cordovaProject.ensurePlatformsAreSynchronized(); + + let satisfied = true; + const messages = buildmessage.capture( + { title: `checking platform requirements` }, () => { + for (platform of this.platformsForRunTargets) { + satisfied = + this.cordovaProject.checkPlatformRequirements(platform) && + satisfied; + } + }); + + if (messages.hasMessages()) { + Console.printMessages(messages); + throw new main.ExitWithCode(1); + } else if (!satisfied) { + throw new main.ExitWithCode(1); + }; + } + + printWarningsIfNeeded() { + // OAuth2 packages don't work so well with any mobile platform except the iOS + // simulator. Print a warning and direct users to the wiki page for help. + if (this.projectContext.packageMap.getInfo('oauth2')) { + Console.warn(); + Console.labelWarn( + "It looks like you are using OAuth2 login in your app. " + + "Meteor's OAuth2 implementation does not currently work with " + + "mobile apps in local development mode, except in the iOS " + + "simulator. You can run the iOS simulator with 'meteor run ios'. " + + "For additional workarounds, see " + + Console.url( + "https://github.com/meteor/meteor/wiki/" + + "OAuth-for-mobile-Meteor-clients.")); + } + + // If we are targeting the remote devices, warn about ports and same network + if (_.findWhere(this.runTargets, { isDevice: true })) { + Console.warn(); + Console.labelWarn( + "You are testing your app on a remote device. " + + "For the mobile app to be able to connect to the local server, make " + + "sure your device is on the same network, and that the network " + + "configuration allows clients to talk to each other " + + "(no client isolation)."); + } + } + + prepareProject(bundlePath, plugins, options) { + this.cordovaProject.prepare(bundlePath, plugins, options); + } + + build() { + buildmessage.assertInCapture(); + + buildmessage.enterJob( + { title: `building Cordova project for platforms: \ +${this.platformsForRunTargets}` }, + () => { + this.cordovaProject.build(this.platformsForRunTargets, this.options); + }); + } + + startRunTargets() { + buildmessage.assertInCapture(); + + for (runTarget of this.runTargets) { + buildmessage.enterJob( + { title: `starting ${runTarget.title}` }, + () => { + // Do not await the returned promise + runTarget.start(this.cordovaProject); + + if (!buildmessage.jobHasMessages()) { + runLog.log(`Started ${runTarget.title}.`, { arrow: true }); + } + } + ); + } + } + + havePlatformsChanged() { + return false; + } + + havePluginsChanged() { + return false; + } +} diff --git a/tools/cordova/utils.js b/tools/cordova/utils.js deleted file mode 100644 index b133dba1e65..00000000000 --- a/tools/cordova/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -import _ from 'underscore'; -import files from '../fs/files.js'; -import { Console } from '../console.js'; -import { execFileAsync, execFileSync } from '../utils/utils.js'; - -export function execFileAsyncOrThrow(file, args, opts, cb) { - Console.debug('Running asynchronously: ', file, args); - - if (_.isFunction(opts)) { - cb = opts; - opts = undefined; - } - - var p = execFileAsync(file, args, opts); - p.on('close', function (code) { - var err = null; - if (code) - err = new Error(file + ' ' + args.join(' ') + - ' exited with non-zero code: ' + code + '. Use -v for' + - ' more logs.'); - - if (cb) cb(err, code); - else if (err) throw err; - }); -}; - -export function execFileSyncOrThrow(file, args, opts) { - Console.debug('Running synchronously: ', file, args); - - var childProcess = execFileSync(file, args, opts); - if (!childProcess.success) { - // XXX Include args - var message = 'Error running ' + file; - if (childProcess.stderr) { - message = message + "\n" + childProcess.stderr + "\n"; - } - if (childProcess.stdout) { - message = message + "\n" + childProcess.stdout + "\n"; - } - - throw new Error(message); - } - - return childProcess; -}; diff --git a/tools/runners/run-all.js b/tools/runners/run-all.js index 3bca3591022..495bf77298e 100644 --- a/tools/runners/run-all.js +++ b/tools/runners/run-all.js @@ -21,7 +21,7 @@ class Runner { appPort, banner, disableOplog, - extraRunners, + cordovaRunner, mongoUrl, onFailure, oplogUrl, @@ -60,8 +60,6 @@ class Runner { self.rootUrl = 'http://localhost:' + listenPort + '/'; } - self.extraRunners = extraRunners ? extraRunners.slice(0) : []; - self.proxy = new Proxy({ listenPort, listenHost: proxyHost, @@ -99,6 +97,7 @@ class Runner { rootUrl: self.rootUrl, proxy: self.proxy, noRestartBanner: self.quiet, + cordovaRunner: cordovaRunner }); self.selenium = null; @@ -114,11 +113,6 @@ class Runner { start() { const self = this; - // XXX: Include all runners, and merge parallel-launch patch - _.each(self.extraRunners, function (runner) { - runner && runner.prestart && runner.prestart(); - }); - self.proxy.start(); // print the banner only once we've successfully bound the port @@ -133,17 +127,6 @@ class Runner { self.updater.start(); } - _.forEach(self.extraRunners, function (extraRunner) { - if (! self.stopped) { - const title = extraRunner.title; - buildmessage.enterJob({ title: "starting " + title }, function () { - extraRunner.start(); - }); - if (! self.quiet && ! self.stopped) - runLog.log("Started " + title + ".", { arrow: true }); - } - }); - if (! self.stopped) { buildmessage.enterJob({ title: "starting your app" }, function () { self.appRunner.start(); @@ -204,9 +187,6 @@ class Runner { self.proxy.stop(); self.updater.stop(); self.mongoRunner && self.mongoRunner.stop(); - _.forEach(self.extraRunners, function (extraRunner) { - extraRunner.stop(); - }); self.appRunner.stop(); self.selenium && self.selenium.stop(); // XXX does calling this 'finish' still make sense now that runLog is a diff --git a/tools/runners/run-app.js b/tools/runners/run-app.js index cbb45c2a81a..3ca61b9535b 100644 --- a/tools/runners/run-app.js +++ b/tools/runners/run-app.js @@ -8,11 +8,12 @@ var bundler = require('../isobuild/bundler.js'); var buildmessage = require('../utils/buildmessage.js'); var runLog = require('./run-log.js'); var stats = require('../meteor-services/stats.js'); -import { getCordovaDependenciesFromStar } from '../cordova/build.js'; var Console = require('../console/console.js').Console; var catalog = require('../packaging/catalog/catalog.js'); var Profile = require('../tool-env/profile.js').Profile; var release = require('../packaging/release.js'); +import * as cordova from '../cordova'; +import { CordovaBuilder } from '../cordova/builder.js'; // Parse out s as if it were a bash command line. var bashParse = function (s) { @@ -351,6 +352,7 @@ var AppRunner = function (options) { self.buildOptions = options.buildOptions; self.rootUrl = options.rootUrl; self.mobileServerUrl = options.mobileServerUrl; + self.cordovaRunner = options.cordovaRunner; self.settingsFile = options.settingsFile; self.debugPort = options.debugPort; self.proxy = options.proxy; @@ -363,14 +365,6 @@ var AppRunner = function (options) { self.omitPackageMapDeltaDisplayOnFirstRun = options.omitPackageMapDeltaDisplayOnFirstRun; - // Keep track of the app's Cordova plugins and platforms. If the set - // of plugins or platforms changes from one run to the next, we just - // exit, because we don't yet have a way to, for example, get the new - // plugins to the mobile clients or stop a running client on a - // platform that has been removed. - self.cordovaPlugins = null; - self.cordovaPlatforms = null; - self.fiber = null; self.startFuture = null; self.runFuture = null; @@ -449,7 +443,7 @@ _.extend(AppRunner.prototype, { _runOnce: function (options) { var self = this; options = options || {}; - var firstRun = options.firstRun; + const firstRun = options.firstRun; Console.enableProgressDisplay(true); @@ -620,32 +614,6 @@ _.extend(AppRunner.prototype, { }; } - firstRun = false; - - var platforms = self.projectContext.platformList.getCordovaPlatforms(); - platforms.sort(); - if (self.cordovaPlatforms && - ! _.isEqual(self.cordovaPlatforms, platforms)) { - return { - outcome: 'outdated-cordova-platforms' - }; - } - // XXX This is racy --- we should get this from the pre-runner build, not - // from the first runner build. - self.cordovaPlatforms = platforms; - - var plugins = getCordovaDependenciesFromStar( - bundleResult.starManifest); - - if (self.cordovaPlugins && ! _.isEqual(self.cordovaPlugins, plugins)) { - return { - outcome: 'outdated-cordova-plugins' - }; - } - // XXX This is racy --- we should get this from the pre-runner build, not - // from the first runner build. - self.cordovaPlugins = plugins; - var serverWatchSet = bundleResult.serverWatchSet; serverWatchSet.merge(settingsWatchSet); @@ -659,6 +627,41 @@ _.extend(AppRunner.prototype, { serverWatchSet = combinedWatchSetForBundleResult(bundleResult); } + const cordovaRunner = self.cordovaRunner; + if (cordovaRunner) { + if (firstRun) { + const plugins = cordova.pluginsFromStarManifest(bundleResult.starManifest); + const { settingsFile, mobileServerUrl } = self; + const messages = buildmessage.capture(() => { + cordovaRunner.prepareProject(bundlePath, plugins, + { settingsFile, mobileServerUrl }); + cordovaRunner.printWarningsIfNeeded(); + cordovaRunner.startRunTargets(); + }); + + if (messages.hasMessages()) { + return { + outcome: 'bundle-fail', + errors: messages, + watchSet: combinedWatchSetForBundleResult(bundleResult) + }; + } + } else { + // If the set of Cordova of platforms or plugins changes from one run + // to the next, we just exit, because we don't yet have a way to, + // for example, get the new plugins to the mobile clients or stop a + // running client on a platform that has been removed. + + if (cordovaRunner.havePlatformsChanged()) { + return { outcome: 'outdated-cordova-platforms' }; + } + + if (cordovaRunner.havePluginsChanged()) { + return { outcome: 'outdated-cordova-plugins' }; + } + } + } + // Atomically (1) see if we've been stop()'d, (2) if not, create a // future that can be used to stop() us once we start running. if (self.exitFuture) @@ -700,7 +703,7 @@ _.extend(AppRunner.prototype, { }); // Empty self._beforeStartFutures and await its elements. - if (options.firstRun && self._beforeStartFuture) { + if (firstRun && self._beforeStartFuture) { var stopped = self._beforeStartFuture.wait(); if (stopped) { return true; diff --git a/tools/tests/cordova-platforms.js b/tools/tests/cordova-platforms.js index 4f570e383dc..e39a334f3d8 100644 --- a/tools/tests/cordova-platforms.js +++ b/tools/tests/cordova-platforms.js @@ -13,7 +13,7 @@ selftest.define("add cordova platforms", ["cordova"], function () { run = s.run("run", "android"); run.matchErr("Please add the Android platform to your project first"); run.match("meteor add-platform android"); - run.expectExit(2); + run.expectExit(1); run = s.run("install-sdk", "android"); run.waitSecs(90); // Big downloads @@ -26,12 +26,12 @@ selftest.define("add cordova platforms", ["cordova"], function () { run = s.run("remove-platform", "foo"); run.matchErr("foo: platform is not"); - run.expectExit(0); + run.expectExit(1); run = s.run("remove-platform", "android"); run.match("removed"); run = s.run("run", "android"); run.matchErr("Please add the Android platform to your project first"); run.match("meteor add-platform android"); - run.expectExit(2); + run.expectExit(1); }); diff --git a/tools/tests/cordova-run.js b/tools/tests/cordova-run.js index 3fac6c400a2..42690b924af 100644 --- a/tools/tests/cordova-run.js +++ b/tools/tests/cordova-run.js @@ -1,4 +1,4 @@ -import selftest from '../selftest.js'; +import selftest from '../tool-testing/selftest.js'; import utils from '../utils/utils.js'; import { parseServerOptionsForRunCommand } from '../cli/commands-cordova.js'; @@ -34,7 +34,7 @@ selftest.define('get mobile server argument for meteor run', ['cordova'], functi }).mobileServerUrl, { host: utils.ipAddress(), port: "3000", protocol: "http://" }); // meteor run -p example.com:3000 --mobile-server 4000 => error, mobile - // server must specify a hostname + // server must include a hostname selftest.expectThrows(() => { parseServerOptionsForRunCommand({ port: "example.com:3000", diff --git a/tools/utils/utils.js b/tools/utils/utils.js index e8caa865a9e..55a2ef59135 100644 --- a/tools/utils/utils.js +++ b/tools/utils/utils.js @@ -20,7 +20,7 @@ var utils = exports; // undefined} or something like that. // // 'defaults' is an optional object with 'host', 'port', and 'protocol' keys. -var parseUrl = function (str, defaults) { +exports.parseUrl = function (str, defaults) { // XXX factor this out into a {type: host/port}? defaults = defaults || {}; @@ -52,7 +52,16 @@ var parseUrl = function (str, defaults) { }; }; -var ipAddress = function () { +// 'defaults' is an optional object with 'host', 'port', and 'protocol' keys. +exports.formatUrl = function (url, defaults) { + let string = url.protocol + url.host; + if (url.port) { + string += `:${url.port}`; + } + return string; +} + +exports.ipAddress = function () { let defaultRoute; // netroute is not available on Windows if (false) { @@ -100,9 +109,6 @@ exports.hasScheme = function (str) { return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//); }; -exports.parseUrl = parseUrl; - -exports.ipAddress = ipAddress; exports.hasScheme = function (str) { return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//);