Cordova refactoring and change of runner behavior

- Refactored code in tools/cordova and introduced CordovaBuilder and
CordovaRunTarget classes
- CordovaRunner now builds and runs the project as part of the main
runner loop
- Some code cleanup and ES2015 conversions
This commit is contained in:
Martijn Walraven
2015-08-17 08:52:18 +02:00
parent d21bddeece
commit 87b11bdab5
22 changed files with 1423 additions and 1526 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
});
}
@@ -833,41 +817,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;
let cordovaPlatforms;
let parsedMobileServerUrl;
if (!options._serverOnly) {
cordovaPlatforms = projectContext.platformList.getCordovaPlatforms();
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.");
}
const mobileServerUrl = parseMobileServerOption(mobileServerOption,
'server');
var cordovaSettings = {};
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');
@@ -875,7 +852,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.
@@ -913,7 +890,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) {
@@ -926,56 +903,74 @@ var buildCommand = function (options) {
files.mkdir_p(outputPath);
if (! options.directory) {
try {
var outputTar = options._serverOnly ? outputPath :
files.pathJoin(outputPath, cordovaProject.appName + '.tar.gz');
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) {
Console.error("Errors during tarball creation:");
Console.error(err.message);
files.rm_recursive(buildDir);
return 1;
}
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 (!_.isEmpty(cordovaPlatforms)) {
let cordovaProject;
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");
}
});
main.captureAndExit('', () => {
buildmessage.enterJob({ title: "preparing Cordova project"}, () => {
cordovaProject = new CordovaProject(projectContext, appName);
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);
};
var findApkPath = function (dirPath, debug) {
return files.pathJoin(dirPath, 'outputs', 'apk', debug ? 'android-debug.apk' : 'android-release-unsigned.apk');
};
///////////////////////////////////////////////////////////////////////////////
// lint
///////////////////////////////////////////////////////////////////////////////
@@ -1434,7 +1429,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
@@ -1505,59 +1500,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);
});
const runTargets = parseRunTargets(_.intersection(
Object.keys(options), ['ios', 'ios-device', 'android', 'android-device']));
if (! _.isEmpty(mobileTargets)) {
var runners = [];
let cordovaRunner;
var platforms = platformsForTargets(mobileTargets);
projectContext.platformList.write(platforms);
// 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
@@ -1650,11 +1618,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.

View File

@@ -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
});
}
}

193
tools/cordova/build.js vendored
View File

@@ -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();
};

589
tools/cordova/builder.js vendored Normal file
View File

@@ -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
* (`<access origin="*"/>`) 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;
}
}
};
}

View File

@@ -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);
}
}
}

43
tools/cordova/index.js vendored Normal file
View File

@@ -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 : {};
}

View File

@@ -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();
}

View File

@@ -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
* (`<access origin="*"/>`) 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');
};

View File

@@ -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;
});
}

View File

@@ -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
});
});
});
}
};

View File

@@ -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);
function log(...args) {
Console.rawInfo(`%% ${util.format.apply(null, args)}\n`);
}
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;
};
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');
this.createIfNeeded();
}
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);
// 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);
});
}
}
chdirToProjectRoot() {
process.chdir(files.convertToOSPath(this.projectRoot));
// Preparing
prepare(bundlePath, plugins, options = {}) {
assert(bundlePath);
assert(plugins);
Console.debug('Preparing Cordova project');
const builder = new CordovaBuilder(this, bundlePath, plugins, options);
builder.start();
}
get defaultOptions() {
return { silent: !Console.verbose, verbose: Console.verbose };
// 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);
});
}
env(...extraPaths) {
let paths = (this.defaultPaths || []);
paths.unshift(...extraPaths);
const env = files.currentEnvWithPathsAdded(...paths);
return env;
}
// Running
get defaultPaths() {
const nodeBinDir = files.getCurrentNodeBinDir();
return [nodeBinDir];
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);
});
}
async removePlatform(platform) {
this.chdirToProjectRoot();
superspawn.setEnv(this.env());
return await cordova.raw.platform('rm', platform, 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);
});
}
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);
removePlugins(pluginsToRemove) {
Console.debug('Removing Cordova 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();
if (_.isEmpty(pluginsToRemove)) return;
uninstalledPlugins = _.difference(
Object.keys(pluginsToRemove), Object.keys(installedPlugins)
);
plugins = _.omit(pluginsToRemove, uninstalledPlugins);
};
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();
superspawn.setEnv(this.env(...options.extraPaths));
options = _.extend(this.defaultOptions, options);
Console.debug('Ensuring Cordova plugins are synchronized', plugins,
pluginsConfiguration);
return await cordova.raw.build(options);
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 };
}
// Run the project
async run(platform, isDevice, options) {
this.chdirToProjectRoot();
defaultEnvWithPathsAdded(...extraPaths) {
let paths = (this.defaultPaths || []);
paths.unshift(...extraPaths);
const env = files.currentEnvWithPathsAdded(...paths);
return env;
}
superspawn.setEnv(this.env(...options.extraPaths));
options = _.extend(this.defaultOptions, options,
{ platforms: [platform] });
get defaultPaths() {
const nodeBinDir = files.getCurrentNodeBinDir();
return [nodeBinDir];
}
if (isDevice) {
return await cordova.raw.run(options);
} else {
return await cordova.raw.emulate(options);
runCommands(asyncFunc, env = this.defaultEnvWithPathsAdded(),
cwd = this.projectRoot) {
const oldCwd = process.cwd();
if (cwd) {
process.chdir(files.convertToOSPath(cwd));
}
superspawn.setEnv(env);
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);
}
}
}
}

92
tools/cordova/run-targets.js vendored Normal file
View File

@@ -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);
}
}

23
tools/cordova/run.js vendored
View File

@@ -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}`);
}
});
};

112
tools/cordova/runner.js vendored Normal file
View File

@@ -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;
}
}

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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",

View File

@@ -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+-\.]*\:\/\//);