aboutsummaryrefslogtreecommitdiff
path: root/cordova/lib
diff options
context:
space:
mode:
authorKumar Priyansh <[email protected]>2019-01-19 12:37:14 +0530
committerKumar Priyansh <[email protected]>2019-01-19 12:37:14 +0530
commitdcdfc94cb39dfe2c39925a0145ffa45e2d061c30 (patch)
tree4f6379d955555b298c0e7b83a67e264240ee5614 /cordova/lib
parent76f7b3678d3f1ff99c3935a774d420453b0c3cb9 (diff)
downloadWeatherApp-dcdfc94cb39dfe2c39925a0145ffa45e2d061c30.tar.xz
WeatherApp-dcdfc94cb39dfe2c39925a0145ffa45e2d061c30.zip
Initial Upload via GIT
Diffstat (limited to 'cordova/lib')
-rwxr-xr-xcordova/lib/Podfile.js245
-rwxr-xr-xcordova/lib/PodsJson.js115
-rwxr-xr-xcordova/lib/build.js412
-rwxr-xr-xcordova/lib/check_reqs.js228
-rwxr-xr-xcordova/lib/clean.js42
-rwxr-xr-xcordova/lib/copy-www-build-step.js73
-rwxr-xr-xcordova/lib/list-devices67
-rwxr-xr-xcordova/lib/list-emulator-build-targets107
-rwxr-xr-xcordova/lib/list-emulator-images47
-rwxr-xr-xcordova/lib/list-started-emulators50
-rwxr-xr-xcordova/lib/plugman/pluginHandlers.js400
-rwxr-xr-xcordova/lib/prepare.js1153
-rwxr-xr-xcordova/lib/projectFile.js134
-rwxr-xr-xcordova/lib/run.js244
-rwxr-xr-xcordova/lib/spawn.js47
-rwxr-xr-xcordova/lib/start-emulator30
-rwxr-xr-xcordova/lib/versions.js194
17 files changed, 3588 insertions, 0 deletions
diff --git a/cordova/lib/Podfile.js b/cordova/lib/Podfile.js
new file mode 100755
index 0000000..49173c4
--- /dev/null
+++ b/cordova/lib/Podfile.js
@@ -0,0 +1,245 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var util = require('util');
+var events = require('cordova-common').events;
+var Q = require('q');
+var superspawn = require('cordova-common').superspawn;
+var CordovaError = require('cordova-common').CordovaError;
+
+Podfile.FILENAME = 'Podfile';
+
+function Podfile (podFilePath, projectName) {
+ this.podToken = '##INSERT_POD##';
+
+ this.path = podFilePath;
+ this.projectName = projectName;
+ this.contents = null;
+ this.pods = null;
+ this.__dirty = false;
+
+ // check whether it is named Podfile
+ var filename = this.path.split(path.sep).pop();
+ if (filename !== Podfile.FILENAME) {
+ throw new CordovaError(util.format('Podfile: The file at %s is not `%s`.', this.path, Podfile.FILENAME));
+ }
+
+ if (!projectName) {
+ throw new CordovaError('Podfile: The projectName was not specified in the constructor.');
+ }
+
+ if (!fs.existsSync(this.path)) {
+ events.emit('verbose', util.format('Podfile: The file at %s does not exist.', this.path));
+ events.emit('verbose', 'Creating new Podfile in platforms/ios');
+ this.clear();
+ this.write();
+ } else {
+ events.emit('verbose', 'Podfile found in platforms/ios');
+ // parse for pods
+ this.pods = this.__parseForPods(fs.readFileSync(this.path, 'utf8'));
+ }
+}
+
+Podfile.prototype.__parseForPods = function (text) {
+ // split by \n
+ var arr = text.split('\n');
+
+ // aim is to match (space insignificant around the comma, comma optional):
+ // pod 'Foobar', '1.2'
+ // pod 'Foobar', 'abc 123 1.2'
+ // pod 'PonyDebugger', :configurations => ['Debug', 'Beta']
+ var podRE = new RegExp('pod \'([^\']*)\'\\s*,?\\s*(.*)');
+
+ // only grab lines that don't have the pod spec'
+ return arr.filter(function (line) {
+ var m = podRE.exec(line);
+
+ return (m !== null);
+ })
+ .reduce(function (obj, line) {
+ var m = podRE.exec(line);
+
+ if (m !== null) {
+ // strip out any single quotes around the value m[2]
+ var podSpec = m[2].replace(/^\'|\'$/g, ''); /* eslint no-useless-escape : 0 */
+ obj[m[1]] = podSpec; // i.e pod 'Foo', '1.2' ==> { 'Foo' : '1.2'}
+ }
+
+ return obj;
+ }, {});
+};
+
+Podfile.prototype.escapeSingleQuotes = function (string) {
+ return string.replace('\'', '\\\'');
+};
+
+Podfile.prototype.getTemplate = function () {
+ // Escaping possible ' in the project name
+ var projectName = this.escapeSingleQuotes(this.projectName);
+ return util.format(
+ '# DO NOT MODIFY -- auto-generated by Apache Cordova\n' +
+ 'platform :ios, \'8.0\'\n' +
+ 'target \'%s\' do\n' +
+ '\tproject \'%s.xcodeproj\'\n' +
+ '%s\n' +
+ 'end\n',
+ projectName, projectName, this.podToken);
+};
+
+Podfile.prototype.addSpec = function (name, spec) {
+ name = name || '';
+ // optional
+ spec = spec; /* eslint no-self-assign : 0 */
+
+ if (!name.length) { // blank names are not allowed
+ throw new CordovaError('Podfile addSpec: name is not specified.');
+ }
+
+ this.pods[name] = spec;
+ this.__dirty = true;
+
+ events.emit('verbose', util.format('Added pod line for `%s`', name));
+};
+
+Podfile.prototype.removeSpec = function (name) {
+ if (this.existsSpec(name)) {
+ delete this.pods[name];
+ this.__dirty = true;
+ }
+
+ events.emit('verbose', util.format('Removed pod line for `%s`', name));
+};
+
+Podfile.prototype.getSpec = function (name) {
+ return this.pods[name];
+};
+
+Podfile.prototype.existsSpec = function (name) {
+ return (name in this.pods);
+};
+
+Podfile.prototype.clear = function () {
+ this.pods = {};
+ this.__dirty = true;
+};
+
+Podfile.prototype.destroy = function () {
+ fs.unlinkSync(this.path);
+ events.emit('verbose', util.format('Deleted `%s`', this.path));
+};
+
+Podfile.prototype.write = function () {
+ var text = this.getTemplate();
+ var self = this;
+
+ var podsString =
+ Object.keys(this.pods).map(function (key) {
+ var name = key;
+ var spec = self.pods[key];
+
+ if (spec.length) {
+ if (spec.indexOf(':') === 0) {
+ // don't quote it, it's a specification (starts with ':')
+ return util.format('\tpod \'%s\', %s', name, spec);
+ } else {
+ // quote it, it's a version
+ return util.format('\tpod \'%s\', \'%s\'', name, spec);
+ }
+ } else {
+ return util.format('\tpod \'%s\'', name);
+ }
+ }).join('\n');
+
+ text = text.replace(this.podToken, podsString);
+ fs.writeFileSync(this.path, text, 'utf8');
+ this.__dirty = false;
+
+ events.emit('verbose', 'Wrote to Podfile.');
+};
+
+Podfile.prototype.isDirty = function () {
+ return this.__dirty;
+};
+
+Podfile.prototype.before_install = function (toolOptions) {
+ toolOptions = toolOptions || {};
+
+ // Template tokens in order: project name, project name, debug | release
+ var template =
+ '// DO NOT MODIFY -- auto-generated by Apache Cordova\n' +
+ '#include "Pods/Target Support Files/Pods-%s/Pods-%s.%s.xcconfig"';
+
+ var debugContents = util.format(template, this.projectName, this.projectName, 'debug');
+ var releaseContents = util.format(template, this.projectName, this.projectName, 'release');
+
+ var debugConfigPath = path.join(this.path, '..', 'pods-debug.xcconfig');
+ var releaseConfigPath = path.join(this.path, '..', 'pods-release.xcconfig');
+
+ fs.writeFileSync(debugConfigPath, debugContents, 'utf8');
+ fs.writeFileSync(releaseConfigPath, releaseContents, 'utf8');
+
+ return Q.resolve(toolOptions);
+};
+
+Podfile.prototype.install = function (requirementsCheckerFunction) {
+ var opts = {};
+ opts.cwd = path.join(this.path, '..'); // parent path of this Podfile
+ opts.stdio = 'pipe';
+ var first = true;
+ var self = this;
+
+ if (!requirementsCheckerFunction) {
+ requirementsCheckerFunction = Q();
+ }
+
+ return requirementsCheckerFunction()
+ .then(function (toolOptions) {
+ return self.before_install(toolOptions);
+ })
+ .then(function (toolOptions) {
+ if (toolOptions.ignore) {
+ events.emit('verbose', '==== pod install start ====\n');
+ events.emit('verbose', toolOptions.ignoreMessage);
+ return Q.resolve();
+ } else {
+ return superspawn.spawn('pod', ['install', '--verbose'], opts)
+ .progress(function (stdio) {
+ if (stdio.stderr) { console.error(stdio.stderr); }
+ if (stdio.stdout) {
+ if (first) {
+ events.emit('verbose', '==== pod install start ====\n');
+ first = false;
+ }
+ events.emit('verbose', stdio.stdout);
+ }
+ });
+ }
+ })
+ .then(function () { // done
+ events.emit('verbose', '==== pod install end ====\n');
+ })
+ .fail(function (error) {
+ throw error;
+ });
+};
+
+module.exports.Podfile = Podfile;
diff --git a/cordova/lib/PodsJson.js b/cordova/lib/PodsJson.js
new file mode 100755
index 0000000..0470527
--- /dev/null
+++ b/cordova/lib/PodsJson.js
@@ -0,0 +1,115 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var fs = require('fs');
+var path = require('path');
+var util = require('util');
+var events = require('cordova-common').events;
+var CordovaError = require('cordova-common').CordovaError;
+
+PodsJson.FILENAME = 'pods.json';
+
+function PodsJson (podsJsonPath) {
+ this.path = podsJsonPath;
+ this.contents = null;
+ this.__dirty = false;
+
+ var filename = this.path.split(path.sep).pop();
+ if (filename !== PodsJson.FILENAME) {
+ throw new CordovaError(util.format('PodsJson: The file at %s is not `%s`.', this.path, PodsJson.FILENAME));
+ }
+
+ if (!fs.existsSync(this.path)) {
+ events.emit('verbose', util.format('pods.json: The file at %s does not exist.', this.path));
+ events.emit('verbose', 'Creating new pods.json in platforms/ios');
+ this.clear();
+ this.write();
+ } else {
+ events.emit('verbose', 'pods.json found in platforms/ios');
+ // load contents
+ this.contents = fs.readFileSync(this.path, 'utf8');
+ this.contents = JSON.parse(this.contents);
+ }
+}
+
+PodsJson.prototype.get = function (name) {
+ return this.contents[name];
+};
+
+PodsJson.prototype.remove = function (name) {
+ if (this.contents[name]) {
+ delete this.contents[name];
+ this.__dirty = true;
+ events.emit('verbose', util.format('Remove from pods.json for `%s`', name));
+ }
+};
+
+PodsJson.prototype.clear = function () {
+ this.contents = {};
+ this.__dirty = true;
+};
+
+PodsJson.prototype.destroy = function () {
+ fs.unlinkSync(this.path);
+ events.emit('verbose', util.format('Deleted `%s`', this.path));
+};
+
+PodsJson.prototype.write = function () {
+ if (this.contents) {
+ fs.writeFileSync(this.path, JSON.stringify(this.contents, null, 4));
+ this.__dirty = false;
+ events.emit('verbose', 'Wrote to pods.json.');
+ }
+};
+
+PodsJson.prototype.set = function (name, type, spec, count) {
+ this.setJson(name, { name: name, type: type, spec: spec, count: count });
+};
+
+PodsJson.prototype.increment = function (name) {
+ var val = this.get(name);
+ if (val) {
+ val.count++;
+ this.setJson(val);
+ }
+};
+
+PodsJson.prototype.decrement = function (name) {
+ var val = this.get(name);
+ if (val) {
+ val.count--;
+ if (val.count <= 0) {
+ this.remove(name);
+ } else {
+ this.setJson(val);
+ }
+ }
+};
+
+PodsJson.prototype.setJson = function (name, json) {
+ this.contents[name] = json;
+ this.__dirty = true;
+ events.emit('verbose', util.format('Set pods.json for `%s`', name));
+};
+
+PodsJson.prototype.isDirty = function () {
+ return this.__dirty;
+};
+
+module.exports.PodsJson = PodsJson;
diff --git a/cordova/lib/build.js b/cordova/lib/build.js
new file mode 100755
index 0000000..b68262e
--- /dev/null
+++ b/cordova/lib/build.js
@@ -0,0 +1,412 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var Q = require('q');
+var path = require('path');
+var shell = require('shelljs');
+var spawn = require('./spawn');
+var fs = require('fs');
+var plist = require('plist');
+var util = require('util');
+
+var check_reqs = require('./check_reqs');
+var projectFile = require('./projectFile');
+
+var events = require('cordova-common').events;
+
+var projectPath = path.join(__dirname, '..', '..');
+var projectName = null;
+
+// These are regular expressions to detect if the user is changing any of the built-in xcodebuildArgs
+/* eslint-disable no-useless-escape */
+var buildFlagMatchers = {
+ 'xcconfig': /^\-xcconfig\s*(.*)$/,
+ 'workspace': /^\-workspace\s*(.*)/,
+ 'scheme': /^\-scheme\s*(.*)/,
+ 'configuration': /^\-configuration\s*(.*)/,
+ 'sdk': /^\-sdk\s*(.*)/,
+ 'destination': /^\-destination\s*(.*)/,
+ 'archivePath': /^\-archivePath\s*(.*)/,
+ 'configuration_build_dir': /^(CONFIGURATION_BUILD_DIR=.*)/,
+ 'shared_precomps_dir': /^(SHARED_PRECOMPS_DIR=.*)/
+};
+/* eslint-enable no-useless-escape */
+
+/**
+ * Returns a promise that resolves to the default simulator target; the logic here
+ * matches what `cordova emulate ios` does.
+ *
+ * The return object has two properties: `name` (the Xcode destination name),
+ * `identifier` (the simctl identifier), and `simIdentifier` (essentially the cordova emulate target)
+ *
+ * @return {Promise}
+ */
+function getDefaultSimulatorTarget () {
+ return require('./list-emulator-build-targets').run()
+ .then(function (emulators) {
+ var targetEmulator;
+ if (emulators.length > 0) {
+ targetEmulator = emulators[0];
+ }
+ emulators.forEach(function (emulator) {
+ if (emulator.name.indexOf('iPhone') === 0) {
+ targetEmulator = emulator;
+ }
+ });
+ return targetEmulator;
+ });
+}
+
+module.exports.run = function (buildOpts) {
+ var emulatorTarget = '';
+
+ buildOpts = buildOpts || {};
+
+ if (buildOpts.debug && buildOpts.release) {
+ return Q.reject('Cannot specify "debug" and "release" options together.');
+ }
+
+ if (buildOpts.device && buildOpts.emulator) {
+ return Q.reject('Cannot specify "device" and "emulator" options together.');
+ }
+
+ if (buildOpts.buildConfig) {
+ if (!fs.existsSync(buildOpts.buildConfig)) {
+ return Q.reject('Build config file does not exist:' + buildOpts.buildConfig);
+ }
+ events.emit('log', 'Reading build config file:', path.resolve(buildOpts.buildConfig));
+ var contents = fs.readFileSync(buildOpts.buildConfig, 'utf-8');
+ var buildConfig = JSON.parse(contents.replace(/^\ufeff/, '')); // Remove BOM
+ if (buildConfig.ios) {
+ var buildType = buildOpts.release ? 'release' : 'debug';
+ var config = buildConfig.ios[buildType];
+ if (config) {
+ ['codeSignIdentity', 'codeSignResourceRules', 'provisioningProfile', 'developmentTeam', 'packageType', 'buildFlag', 'iCloudContainerEnvironment', 'automaticProvisioning'].forEach(
+ function (key) {
+ buildOpts[key] = buildOpts[key] || config[key];
+ });
+ }
+ }
+ }
+
+ return require('./list-devices').run()
+ .then(function (devices) {
+ if (devices.length > 0 && !(buildOpts.emulator)) {
+ // we also explicitly set device flag in options as we pass
+ // those parameters to other api (build as an example)
+ buildOpts.device = true;
+ return check_reqs.check_ios_deploy();
+ }
+ }).then(function () {
+ // CB-12287: Determine the device we should target when building for a simulator
+ if (!buildOpts.device) {
+ var newTarget = buildOpts.target || '';
+
+ if (newTarget) {
+ // only grab the device name, not the runtime specifier
+ newTarget = newTarget.split(',')[0];
+ }
+ // a target was given to us, find the matching Xcode destination name
+ var promise = require('./list-emulator-build-targets').targetForSimIdentifier(newTarget);
+ return promise.then(function (theTarget) {
+ if (!theTarget) {
+ return getDefaultSimulatorTarget().then(function (defaultTarget) {
+ emulatorTarget = defaultTarget.name;
+ events.emit('log', 'Building for ' + emulatorTarget + ' Simulator');
+ return emulatorTarget;
+ });
+ } else {
+ emulatorTarget = theTarget.name;
+ events.emit('log', 'Building for ' + emulatorTarget + ' Simulator');
+ return emulatorTarget;
+ }
+ });
+ }
+ }).then(function () {
+ return check_reqs.run();
+ }).then(function () {
+ return findXCodeProjectIn(projectPath);
+ }).then(function (name) {
+ projectName = name;
+ var extraConfig = '';
+ if (buildOpts.codeSignIdentity) {
+ extraConfig += 'CODE_SIGN_IDENTITY = ' + buildOpts.codeSignIdentity + '\n';
+ extraConfig += 'CODE_SIGN_IDENTITY[sdk=iphoneos*] = ' + buildOpts.codeSignIdentity + '\n';
+ }
+ if (buildOpts.codeSignResourceRules) {
+ extraConfig += 'CODE_SIGN_RESOURCE_RULES_PATH = ' + buildOpts.codeSignResourceRules + '\n';
+ }
+ if (buildOpts.provisioningProfile) {
+ extraConfig += 'PROVISIONING_PROFILE = ' + buildOpts.provisioningProfile + '\n';
+ }
+ if (buildOpts.developmentTeam) {
+ extraConfig += 'DEVELOPMENT_TEAM = ' + buildOpts.developmentTeam + '\n';
+ }
+ return Q.nfcall(fs.writeFile, path.join(__dirname, '..', 'build-extras.xcconfig'), extraConfig, 'utf-8');
+ }).then(function () {
+ var configuration = buildOpts.release ? 'Release' : 'Debug';
+
+ events.emit('log', 'Building project: ' + path.join(projectPath, projectName + '.xcworkspace'));
+ events.emit('log', '\tConfiguration: ' + configuration);
+ events.emit('log', '\tPlatform: ' + (buildOpts.device ? 'device' : 'emulator'));
+
+ var buildOutputDir = path.join(projectPath, 'build', (buildOpts.device ? 'device' : 'emulator'));
+
+ // remove the build/device folder before building
+ return spawn('rm', [ '-rf', buildOutputDir ], projectPath)
+ .then(function () {
+ var xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, buildOpts.device, buildOpts.buildFlag, emulatorTarget);
+ return spawn('xcodebuild', xcodebuildArgs, projectPath);
+ });
+
+ }).then(function () {
+ if (!buildOpts.device || buildOpts.noSign) {
+ return;
+ }
+
+ var locations = {
+ root: projectPath,
+ pbxproj: path.join(projectPath, projectName + '.xcodeproj', 'project.pbxproj')
+ };
+
+ var bundleIdentifier = projectFile.parse(locations).getPackageName();
+ var exportOptions = {'compileBitcode': false, 'method': 'development'};
+
+ if (buildOpts.packageType) {
+ exportOptions.method = buildOpts.packageType;
+ }
+
+ if (buildOpts.iCloudContainerEnvironment) {
+ exportOptions.iCloudContainerEnvironment = buildOpts.iCloudContainerEnvironment;
+ }
+
+ if (buildOpts.developmentTeam) {
+ exportOptions.teamID = buildOpts.developmentTeam;
+ }
+
+ if (buildOpts.provisioningProfile && bundleIdentifier) {
+ exportOptions.provisioningProfiles = { [ bundleIdentifier ]: String(buildOpts.provisioningProfile) };
+ exportOptions.signingStyle = 'manual';
+ }
+
+ if (buildOpts.codeSignIdentity) {
+ exportOptions.signingCertificate = buildOpts.codeSignIdentity;
+ }
+
+ var exportOptionsPlist = plist.build(exportOptions);
+ var exportOptionsPath = path.join(projectPath, 'exportOptions.plist');
+
+ var buildOutputDir = path.join(projectPath, 'build', 'device');
+
+ function checkSystemRuby () {
+ var ruby_cmd = shell.which('ruby');
+
+ if (ruby_cmd !== '/usr/bin/ruby') {
+ events.emit('warn', 'Non-system Ruby in use. This may cause packaging to fail.\n' +
+ 'If you use RVM, please run `rvm use system`.\n' +
+ 'If you use chruby, please run `chruby system`.');
+ }
+ }
+
+ function packageArchive () {
+ var xcodearchiveArgs = getXcodeArchiveArgs(projectName, projectPath, buildOutputDir, exportOptionsPath, buildOpts.automaticProvisioning);
+ return spawn('xcodebuild', xcodearchiveArgs, projectPath);
+ }
+
+ return Q.nfcall(fs.writeFile, exportOptionsPath, exportOptionsPlist, 'utf-8')
+ .then(checkSystemRuby)
+ .then(packageArchive);
+ });
+};
+
+/**
+ * Searches for first XCode project in specified folder
+ * @param {String} projectPath Path where to search project
+ * @return {Promise} Promise either fulfilled with project name or rejected
+ */
+function findXCodeProjectIn (projectPath) {
+ // 'Searching for Xcode project in ' + projectPath);
+ var xcodeProjFiles = shell.ls(projectPath).filter(function (name) {
+ return path.extname(name) === '.xcodeproj';
+ });
+
+ if (xcodeProjFiles.length === 0) {
+ return Q.reject('No Xcode project found in ' + projectPath);
+ }
+ if (xcodeProjFiles.length > 1) {
+ events.emit('warn', 'Found multiple .xcodeproj directories in \n' +
+ projectPath + '\nUsing first one');
+ }
+
+ var projectName = path.basename(xcodeProjFiles[0], '.xcodeproj');
+ return Q.resolve(projectName);
+}
+
+module.exports.findXCodeProjectIn = findXCodeProjectIn;
+
+/**
+ * Returns array of arguments for xcodebuild
+ * @param {String} projectName Name of xcode project
+ * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
+ * @param {String} configuration Configuration name: debug|release
+ * @param {Boolean} isDevice Flag that specify target for package (device/emulator)
+ * @param {Array} buildFlags
+ * @param {String} emulatorTarget Target for emulator (rather than default)
+ * @return {Array} Array of arguments that could be passed directly to spawn method
+ */
+function getXcodeBuildArgs (projectName, projectPath, configuration, isDevice, buildFlags, emulatorTarget) {
+ var xcodebuildArgs;
+ var options;
+ var buildActions;
+ var settings;
+ var customArgs = {};
+ customArgs.otherFlags = [];
+
+ if (buildFlags) {
+ if (typeof buildFlags === 'string' || buildFlags instanceof String) {
+ parseBuildFlag(buildFlags, customArgs);
+ } else { // buildFlags is an Array of strings
+ buildFlags.forEach(function (flag) {
+ parseBuildFlag(flag, customArgs);
+ });
+ }
+ }
+
+ if (isDevice) {
+ options = [
+ '-xcconfig', customArgs.xcconfig || path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'),
+ '-workspace', customArgs.workspace || projectName + '.xcworkspace',
+ '-scheme', customArgs.scheme || projectName,
+ '-configuration', customArgs.configuration || configuration,
+ '-destination', customArgs.destination || 'generic/platform=iOS',
+ '-archivePath', customArgs.archivePath || projectName + '.xcarchive'
+ ];
+ buildActions = [ 'archive' ];
+ settings = [
+ customArgs.configuration_build_dir || 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'device'),
+ customArgs.shared_precomps_dir || 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch')
+ ];
+ // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
+ // This is preferable to just ignoring the flags that the user has passed in.
+ if (customArgs.sdk) {
+ customArgs.otherFlags = customArgs.otherFlags.concat(['-sdk', customArgs.sdk]);
+ }
+ } else { // emulator
+ options = [
+ '-xcconfig', customArgs.xcconfig || path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'),
+ '-workspace', customArgs.project || projectName + '.xcworkspace',
+ '-scheme', customArgs.scheme || projectName,
+ '-configuration', customArgs.configuration || configuration,
+ '-sdk', customArgs.sdk || 'iphonesimulator',
+ '-destination', customArgs.destination || 'platform=iOS Simulator,name=' + emulatorTarget
+ ];
+ buildActions = [ 'build' ];
+ settings = [
+ customArgs.configuration_build_dir || 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'emulator'),
+ customArgs.shared_precomps_dir || 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch')
+ ];
+ // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
+ // This is preferable to just ignoring the flags that the user has passed in.
+ if (customArgs.archivePath) {
+ customArgs.otherFlags = customArgs.otherFlags.concat(['-archivePath', customArgs.archivePath]);
+ }
+ }
+ xcodebuildArgs = options.concat(buildActions).concat(settings).concat(customArgs.otherFlags);
+ return xcodebuildArgs;
+}
+
+/**
+ * Returns array of arguments for xcodebuild
+ * @param {String} projectName Name of xcode project
+ * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
+ * @param {String} outputPath Output directory to contain the IPA
+ * @param {String} exportOptionsPath Path to the exportOptions.plist file
+ * @param {Boolean} autoProvisioning Whether to allow Xcode to automatically update provisioning
+ * @return {Array} Array of arguments that could be passed directly to spawn method
+ */
+function getXcodeArchiveArgs (projectName, projectPath, outputPath, exportOptionsPath, autoProvisioning) {
+ return [
+ '-exportArchive',
+ '-archivePath', projectName + '.xcarchive',
+ '-exportOptionsPlist', exportOptionsPath,
+ '-exportPath', outputPath
+ ].concat(autoProvisioning ? ['-allowProvisioningUpdates'] : []);
+}
+
+function parseBuildFlag (buildFlag, args) {
+ var matched;
+ for (var key in buildFlagMatchers) {
+ var found = buildFlag.match(buildFlagMatchers[key]);
+ if (found) {
+ matched = true;
+ // found[0] is the whole match, found[1] is the first match in parentheses.
+ args[key] = found[1];
+ events.emit('warn', util.format('Overriding xcodebuildArg: %s', buildFlag));
+ }
+ }
+
+ if (!matched) {
+ // If the flag starts with a '-' then it is an xcodebuild built-in option or a
+ // user-defined setting. The regex makes sure that we don't split a user-defined
+ // setting that is wrapped in quotes.
+ /* eslint-disable no-useless-escape */
+ if (buildFlag[0] === '-' && !buildFlag.match(/^.*=(\".*\")|(\'.*\')$/)) {
+ args.otherFlags = args.otherFlags.concat(buildFlag.split(' '));
+ events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag.split(' ')));
+ } else {
+ args.otherFlags.push(buildFlag);
+ events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag));
+ }
+ }
+}
+
+// help/usage function
+module.exports.help = function help () {
+ console.log('');
+ console.log('Usage: build [--debug | --release] [--archs=\"<list of architectures...>\"]');
+ console.log(' [--device | --simulator] [--codeSignIdentity=\"<identity>\"]');
+ console.log(' [--codeSignResourceRules=\"<resourcerules path>\"]');
+ console.log(' [--developmentTeam=\"<Team ID>\"]');
+ console.log(' [--provisioningProfile=\"<provisioning profile>\"]');
+ console.log(' --help : Displays this dialog.');
+ console.log(' --debug : Builds project in debug mode. (Default)');
+ console.log(' --release : Builds project in release mode.');
+ console.log(' -r : Shortcut :: builds project in release mode.');
+ /* eslint-enable no-useless-escape */
+ // TODO: add support for building different archs
+ // console.log(" --archs : Builds project binaries for specific chip architectures (`anycpu`, `arm`, `x86`, `x64`).");
+ console.log(' --device, --simulator');
+ console.log(' : Specifies, what type of project to build');
+ console.log(' --codeSignIdentity : Type of signing identity used for code signing.');
+ console.log(' --codeSignResourceRules : Path to ResourceRules.plist.');
+ console.log(' --developmentTeam : New for Xcode 8. The development team (Team ID)');
+ console.log(' to use for code signing.');
+ console.log(' --provisioningProfile : UUID of the profile.');
+ console.log(' --device --noSign : Builds project without application signing.');
+ console.log('');
+ console.log('examples:');
+ console.log(' build ');
+ console.log(' build --debug');
+ console.log(' build --release');
+ console.log(' build --codeSignIdentity="iPhone Distribution" --provisioningProfile="926c2bd6-8de9-4c2f-8407-1016d2d12954"');
+ // TODO: add support for building different archs
+ // console.log(" build --release --archs=\"armv7\"");
+ console.log('');
+ process.exit(0);
+};
diff --git a/cordova/lib/check_reqs.js b/cordova/lib/check_reqs.js
new file mode 100755
index 0000000..21a2208
--- /dev/null
+++ b/cordova/lib/check_reqs.js
@@ -0,0 +1,228 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+'use strict';
+
+const Q = require('q');
+const shell = require('shelljs');
+const util = require('util');
+const versions = require('./versions');
+
+const SUPPORTED_OS_PLATFORMS = [ 'darwin' ];
+
+const XCODEBUILD_MIN_VERSION = '7.0.0';
+const XCODEBUILD_NOT_FOUND_MESSAGE =
+ 'Please install version ' + XCODEBUILD_MIN_VERSION + ' or greater from App Store';
+
+const IOS_DEPLOY_MIN_VERSION = '1.9.2';
+const IOS_DEPLOY_NOT_FOUND_MESSAGE =
+ 'Please download, build and install version ' + IOS_DEPLOY_MIN_VERSION + ' or greater' +
+ ' from https://github.com/phonegap/ios-deploy into your path, or do \'npm install -g ios-deploy\'';
+
+const COCOAPODS_MIN_VERSION = '1.0.1';
+const COCOAPODS_NOT_FOUND_MESSAGE =
+ 'Please install version ' + COCOAPODS_MIN_VERSION + ' or greater from https://cocoapods.org/';
+const COCOAPODS_NOT_SYNCED_MESSAGE =
+ 'The CocoaPods repo has not been synced yet, this will take a long time (approximately 500MB as of Sept 2016). Please run `pod setup` first to sync the repo.';
+const COCOAPODS_SYNCED_MIN_SIZE = 475; // in megabytes
+const COCOAPODS_SYNC_ERROR_MESSAGE =
+ 'The CocoaPods repo has been created, but there appears to be a sync error. The repo size should be at least ' + COCOAPODS_SYNCED_MIN_SIZE + '. Please run `pod setup --verbose` to sync the repo.';
+const COCOAPODS_REPO_NOT_FOUND_MESSAGE = 'The CocoaPods repo at ~/.cocoapods was not found.';
+
+/**
+ * Checks if xcode util is available
+ * @return {Promise} Returns a promise either resolved with xcode version or rejected
+ */
+module.exports.run = module.exports.check_xcodebuild = function () {
+ return checkTool('xcodebuild', XCODEBUILD_MIN_VERSION, XCODEBUILD_NOT_FOUND_MESSAGE);
+};
+
+/**
+ * Checks if ios-deploy util is available
+ * @return {Promise} Returns a promise either resolved with ios-deploy version or rejected
+ */
+module.exports.check_ios_deploy = function () {
+ return checkTool('ios-deploy', IOS_DEPLOY_MIN_VERSION, IOS_DEPLOY_NOT_FOUND_MESSAGE);
+};
+
+module.exports.check_os = function () {
+ // Build iOS apps available for OSX platform only, so we reject on others platforms
+ return os_platform_is_supported() ?
+ Q.resolve(process.platform) :
+ Q.reject('Cordova tooling for iOS requires Apple macOS');
+};
+
+function os_platform_is_supported () {
+ return (SUPPORTED_OS_PLATFORMS.indexOf(process.platform) !== -1);
+}
+
+function check_cocoapod_tool (toolChecker) {
+ toolChecker = toolChecker || checkTool;
+ if (os_platform_is_supported()) { // CB-12856
+ return toolChecker('pod', COCOAPODS_MIN_VERSION, COCOAPODS_NOT_FOUND_MESSAGE, 'CocoaPods');
+ } else {
+ return Q.resolve({
+ 'ignore': true,
+ 'ignoreMessage': `CocoaPods check and installation ignored on ${process.platform}`
+ });
+ }
+}
+
+/**
+ * Checks if cocoapods repo size is what is expected
+ * @return {Promise} Returns a promise either resolved or rejected
+ */
+module.exports.check_cocoapods_repo_size = function () {
+ return check_cocoapod_tool()
+ .then(function (toolOptions) {
+ // check size of ~/.cocoapods repo
+ let commandString = util.format('du -sh %s/.cocoapods', process.env.HOME);
+ let command = shell.exec(commandString, { silent: true });
+ // command.output is e.g "750M path/to/.cocoapods", we just scan the number
+ let size = toolOptions.ignore ? 0 : parseFloat(command.output);
+
+ if (toolOptions.ignore || command.code === 0) { // success, parse output
+ return Q.resolve(size, toolOptions);
+ } else { // error, perhaps not found
+ return Q.reject(util.format('%s (%s)', COCOAPODS_REPO_NOT_FOUND_MESSAGE, command.output));
+ }
+ })
+ .then(function (repoSize, toolOptions) {
+ if (toolOptions.ignore || COCOAPODS_SYNCED_MIN_SIZE <= repoSize) { // success, expected size
+ return Q.resolve(toolOptions);
+ } else {
+ return Q.reject(COCOAPODS_SYNC_ERROR_MESSAGE);
+ }
+ });
+};
+
+/**
+ * Checks if cocoapods is available, and whether the repo is synced (because it takes a long time to download)
+ * @return {Promise} Returns a promise either resolved or rejected
+ */
+module.exports.check_cocoapods = function (toolChecker) {
+ return check_cocoapod_tool(toolChecker)
+ // check whether the cocoapods repo has been synced through `pod repo` command
+ // a value of '0 repos' means it hasn't been synced
+ .then(function (toolOptions) {
+ let code = shell.exec('pod repo | grep -e "^0 repos"', { silent: true }).code;
+ let repoIsSynced = (code !== 0);
+
+ if (toolOptions.ignore || repoIsSynced) {
+ // return check_cocoapods_repo_size();
+ // we could check the repo size above, but it takes too long.
+ return Q.resolve(toolOptions);
+ } else {
+ return Q.reject(COCOAPODS_NOT_SYNCED_MESSAGE);
+ }
+ });
+};
+
+/**
+ * Checks if specific tool is available.
+ * @param {String} tool Tool name to check. Known tools are 'xcodebuild' and 'ios-deploy'
+ * @param {Number} minVersion Min allowed tool version.
+ * @param {String} message Message that will be used to reject promise.
+ * @param {String} toolFriendlyName Friendly name of the tool, to report to the user. Optional.
+ * @return {Promise} Returns a promise either resolved with tool version or rejected
+ */
+function checkTool (tool, minVersion, message, toolFriendlyName) {
+ toolFriendlyName = toolFriendlyName || tool;
+
+ // Check whether tool command is available at all
+ let tool_command = shell.which(tool);
+ if (!tool_command) {
+ return Q.reject(toolFriendlyName + ' was not found. ' + (message || ''));
+ }
+
+ // check if tool version is greater than specified one
+ return versions.get_tool_version(tool).then(function (version) {
+ version = version.trim();
+ return versions.compareVersions(version, minVersion) >= 0 ?
+ Q.resolve({ 'version': version }) :
+ Q.reject('Cordova needs ' + toolFriendlyName + ' version ' + minVersion +
+ ' or greater, you have version ' + version + '. ' + (message || ''));
+ });
+}
+
+/**
+ * Object that represents one of requirements for current platform.
+ * @param {String} id The unique identifier for this requirements.
+ * @param {String} name The name of requirements. Human-readable field.
+ * @param {Boolean} isFatal Marks the requirement as fatal. If such requirement will fail
+ * next requirements' checks will be skipped.
+ */
+let Requirement = function (id, name, isFatal) {
+ this.id = id;
+ this.name = name;
+ this.installed = false;
+ this.metadata = {};
+ this.isFatal = isFatal || false;
+};
+
+/**
+ * Methods that runs all checks one by one and returns a result of checks
+ * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method
+ *
+ * @return Promise<Requirement[]> Array of requirements. Due to implementation, promise is always fulfilled.
+ */
+module.exports.check_all = function () {
+
+ const requirements = [
+ new Requirement('os', 'Apple macOS', true),
+ new Requirement('xcode', 'Xcode'),
+ new Requirement('ios-deploy', 'ios-deploy'),
+ new Requirement('CocoaPods', 'CocoaPods')
+ ];
+
+ let result = [];
+ let fatalIsHit = false;
+
+ let checkFns = [
+ module.exports.check_os,
+ module.exports.check_xcodebuild,
+ module.exports.check_ios_deploy,
+ module.exports.check_cocoapods
+ ];
+
+ // Then execute requirement checks one-by-one
+ return checkFns.reduce(function (promise, checkFn, idx) {
+ return promise.then(function () {
+ // If fatal requirement is failed,
+ // we don't need to check others
+ if (fatalIsHit) return Q();
+
+ let requirement = requirements[idx];
+ return checkFn()
+ .then(function (version) {
+ requirement.installed = true;
+ requirement.metadata.version = version;
+ result.push(requirement);
+ }, function (err) {
+ if (requirement.isFatal) fatalIsHit = true;
+ requirement.metadata.reason = err;
+ result.push(requirement);
+ });
+ });
+ }, Q())
+ .then(function () {
+ // When chain is completed, return requirements array to upstream API
+ return result;
+ });
+};
diff --git a/cordova/lib/clean.js b/cordova/lib/clean.js
new file mode 100755
index 0000000..20e8ac6
--- /dev/null
+++ b/cordova/lib/clean.js
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var Q = require('q');
+var path = require('path');
+var shell = require('shelljs');
+var spawn = require('./spawn');
+
+var projectPath = path.join(__dirname, '..', '..');
+
+module.exports.run = function () {
+ var projectName = shell.ls(projectPath).filter(function (name) {
+ return path.extname(name) === '.xcodeproj';
+ })[0];
+
+ if (!projectName) {
+ return Q.reject('No Xcode project found in ' + projectPath);
+ }
+
+ return spawn('xcodebuild', ['-project', projectName, '-configuration', 'Debug', '-alltargets', 'clean'], projectPath)
+ .then(function () {
+ return spawn('xcodebuild', ['-project', projectName, '-configuration', 'Release', '-alltargets', 'clean'], projectPath);
+ }).then(function () {
+ return shell.rm('-rf', path.join(projectPath, 'build'));
+ });
+};
diff --git a/cordova/lib/copy-www-build-step.js b/cordova/lib/copy-www-build-step.js
new file mode 100755
index 0000000..e05aacf
--- /dev/null
+++ b/cordova/lib/copy-www-build-step.js
@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+// This script copies the www directory into the Xcode project.
+
+// This script should not be called directly.
+// It is called as a build step from Xcode.
+
+var BUILT_PRODUCTS_DIR = process.env.BUILT_PRODUCTS_DIR;
+var FULL_PRODUCT_NAME = process.env.FULL_PRODUCT_NAME;
+var COPY_HIDDEN = process.env.COPY_HIDDEN;
+var PROJECT_FILE_PATH = process.env.PROJECT_FILE_PATH;
+
+var path = require('path');
+var fs = require('fs');
+var shell = require('shelljs');
+var srcDir = 'www';
+var dstDir = path.join(BUILT_PRODUCTS_DIR, FULL_PRODUCT_NAME);
+var dstWwwDir = path.join(dstDir, 'www');
+
+if (!BUILT_PRODUCTS_DIR) {
+ console.error('The script is meant to be run as an Xcode build step and relies on env variables set by Xcode.');
+ process.exit(1);
+}
+
+try {
+ fs.statSync(srcDir);
+} catch (e) {
+ console.error('Path does not exist: ' + srcDir);
+ process.exit(2);
+}
+
+// Code signing files must be removed or else there are
+// resource signing errors.
+shell.rm('-rf', dstWwwDir);
+shell.rm('-rf', path.join(dstDir, '_CodeSignature'));
+shell.rm('-rf', path.join(dstDir, 'PkgInfo'));
+shell.rm('-rf', path.join(dstDir, 'embedded.mobileprovision'));
+
+// Copy www dir recursively
+var code;
+if (COPY_HIDDEN) {
+ code = shell.exec('rsync -Lra "' + srcDir + '" "' + dstDir + '"').code;
+} else {
+ code = shell.exec('rsync -Lra --exclude="- .*" "' + srcDir + '" "' + dstDir + '"').code;
+}
+
+if (code !== 0) {
+ console.error('Error occured on copying www. Code: ' + code);
+ process.exit(3);
+}
+
+// Copy the config.xml file.
+shell.cp('-f', path.join(path.dirname(PROJECT_FILE_PATH), path.basename(PROJECT_FILE_PATH, '.xcodeproj'), 'config.xml'),
+ dstDir);
diff --git a/cordova/lib/list-devices b/cordova/lib/list-devices
new file mode 100755
index 0000000..047d595
--- /dev/null
+++ b/cordova/lib/list-devices
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+
+var Q = require('q'),
+ exec = require('child_process').exec;
+
+/**
+ * Gets list of connected iOS devices
+ * @return {Promise} Promise fulfilled with list of available iOS devices
+ */
+function listDevices() {
+ var commands = [
+ Q.nfcall(exec, 'system_profiler SPUSBDataType | sed -n -e \'/iPad/,/Serial/p\' | grep "Serial Number:" | awk -F ": " \'{print $2 " iPad"}\''),
+ Q.nfcall(exec, 'system_profiler SPUSBDataType | sed -n -e \'/iPhone/,/Serial/p\' | grep "Serial Number:" | awk -F ": " \'{print $2 " iPhone"}\''),
+ Q.nfcall(exec, 'system_profiler SPUSBDataType | sed -n -e \'/iPod/,/Serial/p\' | grep "Serial Number:" | awk -F ": " \'{print $2 " iPod"}\'')
+ ];
+
+ // wrap al lexec calls into promises and wait until they're fullfilled
+ return Q.all(commands).then(function (results) {
+ var accumulator = [];
+ results.forEach(function (result) {
+ var devicefound;
+ // Each command promise resolves with array [stout, stderr], and we need stdout only
+ // Append stdout lines to accumulator
+ devicefound = result[0].trim().split('\n');
+ if(devicefound && devicefound.length) {
+ devicefound.forEach(function(device) {
+ if (device) {
+ accumulator.push(device);
+ }
+ });
+ }
+ });
+ return accumulator;
+ });
+}
+
+exports.run = listDevices;
+
+// Check if module is started as separate script.
+// If so, then invoke main method and print out results.
+if (!module.parent) {
+ listDevices().then(function (devices) {
+ devices.forEach(function (device) {
+ console.log(device);
+ });
+ });
+} \ No newline at end of file
diff --git a/cordova/lib/list-emulator-build-targets b/cordova/lib/list-emulator-build-targets
new file mode 100755
index 0000000..c0d566f
--- /dev/null
+++ b/cordova/lib/list-emulator-build-targets
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+
+var Q = require('q'),
+ exec = require('child_process').exec;
+
+/**
+ * Returns a list of available simulator build targets of the form
+ *
+ * [
+ * { name: <xcode-destination-name>,
+ * identifier: <simctl-identifier>,
+ * simIdentifier: <cordova emulate target>
+ * }
+ * ]
+ *
+ */
+function listEmulatorBuildTargets () {
+ return Q.nfcall(exec, 'xcrun simctl list --json')
+ .then(function(stdio) {
+ return JSON.parse(stdio[0]);
+ })
+ .then(function(simInfo) {
+ var devices = simInfo.devices;
+ var deviceTypes = simInfo.devicetypes;
+ return deviceTypes.reduce(function (typeAcc, deviceType) {
+ if (!deviceType.name.match(/^[iPad|iPhone]/)) {
+ // ignore targets we don't support (like Apple Watch or Apple TV)
+ return typeAcc;
+ }
+ var availableDevices = Object.keys(devices).reduce(function (availAcc, deviceCategory) {
+ var availableDevicesInCategory = devices[deviceCategory];
+ availableDevicesInCategory.forEach(function (device) {
+ if (device.name === deviceType.name.replace(/\-inch/g, ' inch') &&
+ device.availability.toLowerCase().indexOf('unavailable') < 0) {
+ availAcc.push(device);
+ }
+ });
+ return availAcc;
+ }, []);
+ // we only want device types that have at least one available device
+ // (regardless of OS); this filters things out like iPhone 4s, which
+ // is present in deviceTypes, but probably not available on the user's
+ // system.
+ if (availableDevices.length > 0) {
+ typeAcc.push(deviceType);
+ }
+ return typeAcc;
+ }, []);
+ })
+ .then(function(filteredTargets) {
+ // the simIdentifier, or cordova emulate target name, is the very last part
+ // of identifier.
+ return filteredTargets.map(function (target) {
+ var identifierPieces = target.identifier.split(".");
+ target.simIdentifier = identifierPieces[identifierPieces.length-1];
+ return target;
+ });
+ });
+}
+
+exports.run = listEmulatorBuildTargets;
+
+/**
+ * Given a simIdentifier, return the matching target.
+ *
+ * @param {string} simIdentifier a target, like "iPhone-SE"
+ * @return {Object} the matching target, or undefined if no match
+ */
+exports.targetForSimIdentifier = function(simIdentifier) {
+ return listEmulatorBuildTargets()
+ .then(function(targets) {
+ return targets.reduce(function(acc, target) {
+ if (!acc && target.simIdentifier.toLowerCase() === simIdentifier.toLowerCase()) {
+ acc = target;
+ }
+ return acc;
+ }, undefined);
+ });
+}
+
+// Check if module is started as separate script.
+// If so, then invoke main method and print out results.
+if (!module.parent) {
+ listEmulatorBuildTargets().then(function (targets) {
+ console.log(JSON.stringify(targets, null, 2));
+ });
+}
diff --git a/cordova/lib/list-emulator-images b/cordova/lib/list-emulator-images
new file mode 100755
index 0000000..d8be576
--- /dev/null
+++ b/cordova/lib/list-emulator-images
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+
+var Q = require('q'),
+ iossim = require('ios-sim'),
+ exec = require('child_process').exec,
+ check_reqs = require('./check_reqs');
+
+/**
+ * Gets list of iOS devices available for simulation
+ * @return {Promise} Promise fulfilled with list of devices available for simulation
+ */
+function listEmulatorImages () {
+ return Q.resolve(iossim.getdevicetypes());
+}
+
+
+exports.run = listEmulatorImages;
+
+// Check if module is started as separate script.
+// If so, then invoke main method and print out results.
+if (!module.parent) {
+ listEmulatorImages().then(function (names) {
+ names.forEach(function (name) {
+ console.log(name);
+ });
+ });
+}
diff --git a/cordova/lib/list-started-emulators b/cordova/lib/list-started-emulators
new file mode 100755
index 0000000..710fa2f
--- /dev/null
+++ b/cordova/lib/list-started-emulators
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+
+var Q = require('q'),
+ exec = require('child_process').exec;
+
+/**
+ * Gets list of running iOS simulators
+ * @return {Promise} Promise fulfilled with list of running iOS simulators
+ */
+function listStartedEmulators () {
+ // wrap exec call into promise
+ return Q.nfcall(exec, 'ps aux | grep -i "[i]OS Simulator"')
+ .then(function () {
+ return Q.nfcall(exec, 'defaults read com.apple.iphonesimulator "SimulateDevice"');
+ }).then(function (stdio) {
+ return stdio[0].trim().split('\n');
+ });
+}
+
+exports.run = listStartedEmulators;
+
+// Check if module is started as separate script.
+// If so, then invoke main method and print out results.
+if (!module.parent) {
+ listStartedEmulators().then(function (emulators) {
+ emulators.forEach(function (emulator) {
+ console.log(emulator);
+ });
+ });
+}
diff --git a/cordova/lib/plugman/pluginHandlers.js b/cordova/lib/plugman/pluginHandlers.js
new file mode 100755
index 0000000..1f6920f
--- /dev/null
+++ b/cordova/lib/plugman/pluginHandlers.js
@@ -0,0 +1,400 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+'use strict';
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var util = require('util');
+var events = require('cordova-common').events;
+var CordovaError = require('cordova-common').CordovaError;
+
+// These frameworks are required by cordova-ios by default. We should never add/remove them.
+var keep_these_frameworks = [
+ 'MobileCoreServices.framework',
+ 'CoreGraphics.framework',
+ 'AssetsLibrary.framework'
+];
+
+var handlers = {
+ 'source-file': {
+ install: function (obj, plugin, project, options) {
+ installHelper('source-file', obj, plugin.dir, project.projectDir, plugin.id, options, project);
+ },
+ uninstall: function (obj, plugin, project, options) {
+ uninstallHelper('source-file', obj, project.projectDir, plugin.id, options, project);
+ }
+ },
+ 'header-file': {
+ install: function (obj, plugin, project, options) {
+ installHelper('header-file', obj, plugin.dir, project.projectDir, plugin.id, options, project);
+ },
+ uninstall: function (obj, plugin, project, options) {
+ uninstallHelper('header-file', obj, project.projectDir, plugin.id, options, project);
+ }
+ },
+ 'resource-file': {
+ install: function (obj, plugin, project, options) {
+ var src = obj.src;
+ var target = obj.target;
+ var srcFile = path.resolve(plugin.dir, src);
+
+ if (!target) {
+ target = path.basename(src);
+ }
+ var destFile = path.resolve(project.resources_dir, target);
+
+ if (!fs.existsSync(srcFile)) {
+ throw new CordovaError('Cannot find resource file "' + srcFile + '" for plugin ' + plugin.id + ' in iOS platform');
+ }
+ if (fs.existsSync(destFile)) {
+ throw new CordovaError('File already exists at destination "' + destFile + '" for resource file specified by plugin ' + plugin.id + ' in iOS platform');
+ }
+ project.xcode.addResourceFile(path.join('Resources', target));
+ var link = !!(options && options.link);
+ copyFile(plugin.dir, src, project.projectDir, destFile, link);
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var src = obj.src;
+ var target = obj.target;
+
+ if (!target) {
+ target = path.basename(src);
+ }
+ var destFile = path.resolve(project.resources_dir, target);
+
+ project.xcode.removeResourceFile(path.join('Resources', target));
+ shell.rm('-rf', destFile);
+ }
+ },
+ 'framework': { // CB-5238 custom frameworks only
+ install: function (obj, plugin, project, options) {
+ var src = obj.src;
+ var custom = !!(obj.custom); // convert to boolean (if truthy/falsy)
+ var embed = !!(obj.embed); // convert to boolean (if truthy/falsy)
+ var link = !embed; // either link or embed can be true, but not both. the other has to be false
+
+ if (!custom) {
+ var keepFrameworks = keep_these_frameworks;
+
+ if (keepFrameworks.indexOf(src) < 0) {
+ if (obj.type === 'podspec') {
+ // podspec handled in Api.js
+ } else {
+ project.frameworks[src] = project.frameworks[src] || 0;
+ project.frameworks[src]++;
+ let opt = { customFramework: false, embed: false, link: true, weak: obj.weak };
+ events.emit('verbose', util.format('Adding non-custom framework to project... %s -> %s', src, JSON.stringify(opt)));
+ project.xcode.addFramework(src, opt);
+ events.emit('verbose', util.format('Non-custom framework added to project. %s -> %s', src, JSON.stringify(opt)));
+ }
+ }
+ return;
+ }
+ var srcFile = path.resolve(plugin.dir, src);
+ var targetDir = path.resolve(project.plugins_dir, plugin.id, path.basename(src));
+ if (!fs.existsSync(srcFile)) throw new CordovaError('Cannot find framework "' + srcFile + '" for plugin ' + plugin.id + ' in iOS platform');
+ if (fs.existsSync(targetDir)) throw new CordovaError('Framework "' + targetDir + '" for plugin ' + plugin.id + ' already exists in iOS platform');
+ var symlink = !!(options && options.link);
+ copyFile(plugin.dir, src, project.projectDir, targetDir, symlink); // frameworks are directories
+ // CB-10773 translate back slashes to forward on win32
+ var project_relative = fixPathSep(path.relative(project.projectDir, targetDir));
+ // CB-11233 create Embed Frameworks Build Phase if does not exist
+ var existsEmbedFrameworks = project.xcode.buildPhaseObject('PBXCopyFilesBuildPhase', 'Embed Frameworks');
+ if (!existsEmbedFrameworks && embed) {
+ events.emit('verbose', '"Embed Frameworks" Build Phase (Embedded Binaries) does not exist, creating it.');
+ project.xcode.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Embed Frameworks', null, 'frameworks');
+ }
+ let opt = { customFramework: true, embed: embed, link: link, sign: true };
+ events.emit('verbose', util.format('Adding custom framework to project... %s -> %s', src, JSON.stringify(opt)));
+ project.xcode.addFramework(project_relative, opt);
+ events.emit('verbose', util.format('Custom framework added to project. %s -> %s', src, JSON.stringify(opt)));
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var src = obj.src;
+
+ if (!obj.custom) { // CB-9825 cocoapod integration for plugins
+ var keepFrameworks = keep_these_frameworks;
+ if (keepFrameworks.indexOf(src) < 0) {
+ if (obj.type === 'podspec') {
+ var podsJSON = require(path.join(project.projectDir, 'pods.json'));
+ if (podsJSON[src]) {
+ if (podsJSON[src].count > 1) {
+ podsJSON[src].count = podsJSON[src].count - 1;
+ } else {
+ delete podsJSON[src];
+ }
+ }
+ } else {
+ // this should be refactored
+ project.frameworks[src] = project.frameworks[src] || 1;
+ project.frameworks[src]--;
+ if (project.frameworks[src] < 1) {
+ // Only remove non-custom framework from xcode project
+ // if there is no references remains
+ project.xcode.removeFramework(src);
+ delete project.frameworks[src];
+ }
+ }
+ }
+ return;
+ }
+
+ var targetDir = fixPathSep(path.resolve(project.plugins_dir, plugin.id, path.basename(src)));
+ var pbxFile = project.xcode.removeFramework(targetDir, {customFramework: true});
+ if (pbxFile) {
+ project.xcode.removeFromPbxEmbedFrameworksBuildPhase(pbxFile);
+ }
+ shell.rm('-rf', targetDir);
+ }
+ },
+ 'lib-file': {
+ install: function (obj, plugin, project, options) {
+ events.emit('verbose', '<lib-file> install is not supported for iOS plugins');
+ },
+ uninstall: function (obj, plugin, project, options) {
+ events.emit('verbose', '<lib-file> uninstall is not supported for iOS plugins');
+ }
+ },
+ 'asset': {
+ install: function (obj, plugin, project, options) {
+ if (!obj.src) {
+ throw new CordovaError(generateAttributeError('src', 'asset', plugin.id));
+ }
+ if (!obj.target) {
+ throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
+ }
+
+ copyFile(plugin.dir, obj.src, project.www, obj.target);
+ if (options && options.usePlatformWww) copyFile(plugin.dir, obj.src, project.platformWww, obj.target);
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var target = obj.target;
+
+ if (!target) {
+ throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
+ }
+
+ removeFile(project.www, target);
+ removeFileF(path.resolve(project.www, 'plugins', plugin.id));
+ if (options && options.usePlatformWww) {
+ removeFile(project.platformWww, target);
+ removeFileF(path.resolve(project.platformWww, 'plugins', plugin.id));
+ }
+ }
+ },
+ 'js-module': {
+ install: function (obj, plugin, project, options) {
+ // Copy the plugin's files into the www directory.
+ var moduleSource = path.resolve(plugin.dir, obj.src);
+ var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname(obj.src)));
+
+ // Read in the file, prepend the cordova.define, and write it back out.
+ var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
+ if (moduleSource.match(/.*\.json$/)) {
+ scriptContent = 'module.exports = ' + scriptContent;
+ }
+ scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';
+
+ var moduleDestination = path.resolve(project.www, 'plugins', plugin.id, obj.src);
+ shell.mkdir('-p', path.dirname(moduleDestination));
+ fs.writeFileSync(moduleDestination, scriptContent, 'utf-8');
+ if (options && options.usePlatformWww) {
+ var platformWwwDestination = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
+ shell.mkdir('-p', path.dirname(platformWwwDestination));
+ fs.writeFileSync(platformWwwDestination, scriptContent, 'utf-8');
+ }
+ },
+ uninstall: function (obj, plugin, project, options) {
+ var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
+ removeFileAndParents(project.www, pluginRelativePath);
+ if (options && options.usePlatformWww) removeFileAndParents(project.platformWww, pluginRelativePath);
+ }
+ }
+};
+
+module.exports.getInstaller = function (type) {
+ if (handlers[type] && handlers[type].install) {
+ return handlers[type].install;
+ }
+
+ events.emit('warn', '<' + type + '> is not supported for iOS plugins');
+};
+
+module.exports.getUninstaller = function (type) {
+ if (handlers[type] && handlers[type].uninstall) {
+ return handlers[type].uninstall;
+ }
+
+ events.emit('warn', '<' + type + '> is not supported for iOS plugins');
+};
+
+function installHelper (type, obj, plugin_dir, project_dir, plugin_id, options, project) {
+ var srcFile = path.resolve(plugin_dir, obj.src);
+ var targetDir = path.resolve(project.plugins_dir, plugin_id, obj.targetDir || '');
+ var destFile = path.join(targetDir, path.basename(obj.src));
+
+ var project_ref;
+ var link = !!(options && options.link);
+ if (link) {
+ var trueSrc = fs.realpathSync(srcFile);
+ // Create a symlink in the expected place, so that uninstall can use it.
+ if (options && options.force) {
+ copyFile(plugin_dir, trueSrc, project_dir, destFile, link);
+ } else {
+ copyNewFile(plugin_dir, trueSrc, project_dir, destFile, link);
+ }
+ // Xcode won't save changes to a file if there is a symlink involved.
+ // Make the Xcode reference the file directly.
+ // Note: Can't use path.join() here since it collapses 'Plugins/..', and xcode
+ // library special-cases Plugins/ prefix.
+ project_ref = 'Plugins/' + fixPathSep(path.relative(fs.realpathSync(project.plugins_dir), trueSrc));
+ } else {
+ if (options && options.force) {
+ copyFile(plugin_dir, srcFile, project_dir, destFile, link);
+ } else {
+ copyNewFile(plugin_dir, srcFile, project_dir, destFile, link);
+ }
+ project_ref = 'Plugins/' + fixPathSep(path.relative(project.plugins_dir, destFile));
+ }
+
+ if (type === 'header-file') {
+ project.xcode.addHeaderFile(project_ref);
+ } else if (obj.framework) {
+ var opt = { weak: obj.weak };
+ var project_relative = path.join(path.basename(project.xcode_path), project_ref);
+ project.xcode.addFramework(project_relative, opt);
+ project.xcode.addToLibrarySearchPaths({path: project_ref});
+ } else {
+ project.xcode.addSourceFile(project_ref, obj.compilerFlags ? {compilerFlags: obj.compilerFlags} : {});
+ }
+}
+
+function uninstallHelper (type, obj, project_dir, plugin_id, options, project) {
+ var targetDir = path.resolve(project.plugins_dir, plugin_id, obj.targetDir || '');
+ var destFile = path.join(targetDir, path.basename(obj.src));
+
+ var project_ref;
+ var link = !!(options && options.link);
+ if (link) {
+ var trueSrc = fs.readlinkSync(destFile);
+ project_ref = 'Plugins/' + fixPathSep(path.relative(fs.realpathSync(project.plugins_dir), trueSrc));
+ } else {
+ project_ref = 'Plugins/' + fixPathSep(path.relative(project.plugins_dir, destFile));
+ }
+
+ shell.rm('-rf', targetDir);
+
+ if (type === 'header-file') {
+ project.xcode.removeHeaderFile(project_ref);
+ } else if (obj.framework) {
+ var project_relative = path.join(path.basename(project.xcode_path), project_ref);
+ project.xcode.removeFramework(project_relative);
+ project.xcode.removeFromLibrarySearchPaths({path: project_ref});
+ } else {
+ project.xcode.removeSourceFile(project_ref);
+ }
+}
+
+var pathSepFix = new RegExp(path.sep.replace(/\\/, '\\\\'), 'g');
+function fixPathSep (file) {
+ return file.replace(pathSepFix, '/');
+}
+
+function copyFile (plugin_dir, src, project_dir, dest, link) {
+ src = path.resolve(plugin_dir, src);
+ if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');
+
+ // check that src path is inside plugin directory
+ var real_path = fs.realpathSync(src);
+ var real_plugin_path = fs.realpathSync(plugin_dir);
+ if (real_path.indexOf(real_plugin_path) !== 0) { throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"'); }
+
+ dest = path.resolve(project_dir, dest);
+
+ // check that dest path is located in project directory
+ if (dest.indexOf(project_dir) !== 0) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }
+
+ shell.mkdir('-p', path.dirname(dest));
+
+ if (link) {
+ linkFileOrDirTree(src, dest);
+ } else if (fs.statSync(src).isDirectory()) {
+ // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
+ shell.cp('-Rf', path.join(src, '/*'), dest);
+ } else {
+ shell.cp('-f', src, dest);
+ }
+}
+
+// Same as copy file but throws error if target exists
+function copyNewFile (plugin_dir, src, project_dir, dest, link) {
+ var target_path = path.resolve(project_dir, dest);
+ if (fs.existsSync(target_path)) { throw new CordovaError('"' + target_path + '" already exists!'); }
+
+ copyFile(plugin_dir, src, project_dir, dest, !!link);
+}
+
+function linkFileOrDirTree (src, dest) {
+ if (fs.existsSync(dest)) {
+ shell.rm('-Rf', dest);
+ }
+
+ if (fs.statSync(src).isDirectory()) {
+ shell.mkdir('-p', dest);
+ fs.readdirSync(src).forEach(function (entry) {
+ linkFileOrDirTree(path.join(src, entry), path.join(dest, entry));
+ });
+ } else {
+ fs.linkSync(src, dest);
+ }
+}
+
+// checks if file exists and then deletes. Error if doesn't exist
+function removeFile (project_dir, src) {
+ var file = path.resolve(project_dir, src);
+ shell.rm('-Rf', file);
+}
+
+// deletes file/directory without checking
+function removeFileF (file) {
+ shell.rm('-Rf', file);
+}
+
+function removeFileAndParents (baseDir, destFile, stopper) {
+ stopper = stopper || '.';
+ var file = path.resolve(baseDir, destFile);
+ if (!fs.existsSync(file)) return;
+
+ removeFileF(file);
+
+ // check if directory is empty
+ var curDir = path.dirname(file);
+
+ while (curDir !== path.resolve(baseDir, stopper)) {
+ if (fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
+ fs.rmdirSync(curDir);
+ curDir = path.resolve(curDir, '..');
+ } else {
+ // directory not empty...do nothing
+ break;
+ }
+ }
+}
+
+function generateAttributeError (attribute, element, id) {
+ return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id;
+}
diff --git a/cordova/lib/prepare.js b/cordova/lib/prepare.js
new file mode 100755
index 0000000..de345dd
--- /dev/null
+++ b/cordova/lib/prepare.js
@@ -0,0 +1,1153 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+'use strict';
+var Q = require('q');
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var xcode = require('xcode');
+var unorm = require('unorm');
+var plist = require('plist');
+var URL = require('url');
+var events = require('cordova-common').events;
+var xmlHelpers = require('cordova-common').xmlHelpers;
+var ConfigParser = require('cordova-common').ConfigParser;
+var CordovaError = require('cordova-common').CordovaError;
+var PlatformJson = require('cordova-common').PlatformJson;
+var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
+var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
+var FileUpdater = require('cordova-common').FileUpdater;
+var projectFile = require('./projectFile');
+
+// launch storyboard and related constants
+var LAUNCHIMAGE_BUILD_SETTING = 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME';
+var LAUNCHIMAGE_BUILD_SETTING_VALUE = 'LaunchImage';
+var UI_LAUNCH_STORYBOARD_NAME = 'UILaunchStoryboardName';
+var CDV_LAUNCH_STORYBOARD_NAME = 'CDVLaunchScreen';
+var IMAGESET_COMPACT_SIZE_CLASS = 'compact';
+var CDV_ANY_SIZE_CLASS = 'any';
+
+module.exports.prepare = function (cordovaProject, options) {
+ var self = this;
+
+ var platformJson = PlatformJson.load(this.locations.root, 'ios');
+ var munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider());
+
+ this._config = updateConfigFile(cordovaProject.projectConfig, munger, this.locations);
+
+ // Update own www dir with project's www assets and plugins' assets and js-files
+ return Q.when(updateWww(cordovaProject, this.locations))
+ .then(function () {
+ // update project according to config.xml changes.
+ return updateProject(self._config, self.locations);
+ })
+ .then(function () {
+ updateIcons(cordovaProject, self.locations);
+ updateSplashScreens(cordovaProject, self.locations);
+ updateLaunchStoryboardImages(cordovaProject, self.locations);
+ updateFileResources(cordovaProject, self.locations);
+ })
+ .then(function () {
+ events.emit('verbose', 'Prepared iOS project successfully');
+ });
+};
+
+module.exports.clean = function (options) {
+ // A cordovaProject isn't passed into the clean() function, because it might have
+ // been called from the platform shell script rather than the CLI. Check for the
+ // noPrepare option passed in by the non-CLI clean script. If that's present, or if
+ // there's no config.xml found at the project root, then don't clean prepared files.
+ var projectRoot = path.resolve(this.root, '../..');
+ var projectConfigFile = path.join(projectRoot, 'config.xml');
+ if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
+ !fs.existsSync(this.locations.configXml)) {
+ return Q();
+ }
+
+ var projectConfig = new ConfigParser(this.locations.configXml);
+
+ var self = this;
+ return Q().then(function () {
+ cleanWww(projectRoot, self.locations);
+ cleanIcons(projectRoot, projectConfig, self.locations);
+ cleanSplashScreens(projectRoot, projectConfig, self.locations);
+ cleanLaunchStoryboardImages(projectRoot, projectConfig, self.locations);
+ cleanFileResources(projectRoot, projectConfig, self.locations);
+ });
+};
+
+/**
+ * Updates config files in project based on app's config.xml and config munge,
+ * generated by plugins.
+ *
+ * @param {ConfigParser} sourceConfig A project's configuration that will
+ * be merged into platform's config.xml
+ * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
+ * for this platform.
+ * @param {Object} locations A map of locations for this platform
+ *
+ * @return {ConfigParser} An instance of ConfigParser, that
+ * represents current project's configuration. When returned, the
+ * configuration is already dumped to appropriate config.xml file.
+ */
+function updateConfigFile (sourceConfig, configMunger, locations) {
+ events.emit('verbose', 'Generating platform-specific config.xml from defaults for iOS at ' + locations.configXml);
+
+ // First cleanup current config and merge project's one into own
+ // Overwrite platform config.xml with defaults.xml.
+ shell.cp('-f', locations.defaultConfigXml, locations.configXml);
+
+ // Then apply config changes from global munge to all config files
+ // in project (including project's config)
+ configMunger.reapply_global_munge().save_all();
+
+ events.emit('verbose', 'Merging project\'s config.xml into platform-specific iOS config.xml');
+ // Merge changes from app's config.xml into platform's one
+ var config = new ConfigParser(locations.configXml);
+ xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
+ config.doc.getroot(), 'ios', /* clobber= */true);
+
+ config.write();
+ return config;
+}
+
+/**
+ * Logs all file operations via the verbose event stream, indented.
+ */
+function logFileOp (message) {
+ events.emit('verbose', ' ' + message);
+}
+
+/**
+ * Updates platform 'www' directory by replacing it with contents of
+ * 'platform_www' and app www. Also copies project's overrides' folder into
+ * the platform 'www' folder
+ *
+ * @param {Object} cordovaProject An object which describes cordova project.
+ * @param {boolean} destinations An object that contains destinations
+ * paths for www files.
+ */
+function updateWww (cordovaProject, destinations) {
+ var sourceDirs = [
+ path.relative(cordovaProject.root, cordovaProject.locations.www),
+ path.relative(cordovaProject.root, destinations.platformWww)
+ ];
+
+ // If project contains 'merges' for our platform, use them as another overrides
+ var merges_path = path.join(cordovaProject.root, 'merges', 'ios');
+ if (fs.existsSync(merges_path)) {
+ events.emit('verbose', 'Found "merges/ios" folder. Copying its contents into the iOS project.');
+ sourceDirs.push(path.join('merges', 'ios'));
+ }
+
+ var targetDir = path.relative(cordovaProject.root, destinations.www);
+ events.emit(
+ 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
+ FileUpdater.mergeAndUpdateDir(
+ sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+/**
+ * Cleans all files from the platform 'www' directory.
+ */
+function cleanWww (projectRoot, locations) {
+ var targetDir = path.relative(projectRoot, locations.www);
+ events.emit('verbose', 'Cleaning ' + targetDir);
+
+ // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
+ FileUpdater.mergeAndUpdateDir(
+ [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
+}
+
+/**
+ * Updates project structure and AndroidManifest according to project's configuration.
+ *
+ * @param {ConfigParser} platformConfig A project's configuration that will
+ * be used to update project
+ * @param {Object} locations A map of locations for this platform (In/Out)
+ */
+function updateProject (platformConfig, locations) {
+
+ // CB-6992 it is necessary to normalize characters
+ // because node and shell scripts handles unicode symbols differently
+ // We need to normalize the name to NFD form since iOS uses NFD unicode form
+ var name = unorm.nfd(platformConfig.name());
+ var pkg = platformConfig.getAttribute('ios-CFBundleIdentifier') || platformConfig.packageName();
+ var version = platformConfig.version();
+ var displayName = platformConfig.shortName && platformConfig.shortName();
+
+ var originalName = path.basename(locations.xcodeCordovaProj);
+
+ // Update package id (bundle id)
+ var plistFile = path.join(locations.xcodeCordovaProj, originalName + '-Info.plist');
+ var infoPlist = plist.parse(fs.readFileSync(plistFile, 'utf8'));
+ infoPlist['CFBundleIdentifier'] = pkg;
+
+ // Update version (bundle version)
+ infoPlist['CFBundleShortVersionString'] = version;
+ var CFBundleVersion = platformConfig.getAttribute('ios-CFBundleVersion') || default_CFBundleVersion(version);
+ infoPlist['CFBundleVersion'] = CFBundleVersion;
+
+ if (platformConfig.getAttribute('defaultlocale')) {
+ infoPlist['CFBundleDevelopmentRegion'] = platformConfig.getAttribute('defaultlocale');
+ }
+
+ if (displayName) {
+ infoPlist['CFBundleDisplayName'] = displayName;
+ }
+
+ // replace Info.plist ATS entries according to <access> and <allow-navigation> config.xml entries
+ var ats = writeATSEntries(platformConfig);
+ if (Object.keys(ats).length > 0) {
+ infoPlist['NSAppTransportSecurity'] = ats;
+ } else {
+ delete infoPlist['NSAppTransportSecurity'];
+ }
+
+ handleOrientationSettings(platformConfig, infoPlist);
+ updateProjectPlistForLaunchStoryboard(platformConfig, infoPlist);
+
+ var info_contents = plist.build(infoPlist);
+ info_contents = info_contents.replace(/<string>[\s\r\n]*<\/string>/g, '<string></string>');
+ fs.writeFileSync(plistFile, info_contents, 'utf-8');
+ events.emit('verbose', 'Wrote out iOS Bundle Identifier "' + pkg + '" and iOS Bundle Version "' + version + '" to ' + plistFile);
+
+ return handleBuildSettings(platformConfig, locations, infoPlist).then(function () {
+ if (name === originalName) {
+ events.emit('verbose', 'iOS Product Name has not changed (still "' + originalName + '")');
+ return Q();
+ } else { // CB-11712 <name> was changed, we don't support it'
+ var errorString =
+ 'The product name change (<name> tag) in config.xml is not supported dynamically.\n' +
+ 'To change your product name, you have to remove, then add your ios platform again.\n' +
+ 'Make sure you save your plugins beforehand using `cordova plugin save`.\n' +
+ '\tcordova plugin save\n' +
+ '\tcordova platform rm ios\n' +
+ '\tcordova platform add ios\n'
+ ;
+
+ return Q.reject(new CordovaError(errorString));
+ }
+ });
+}
+
+function handleOrientationSettings (platformConfig, infoPlist) {
+
+ switch (getOrientationValue(platformConfig)) {
+ case 'portrait':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
+ break;
+ case 'landscape':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationLandscapeLeft' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ break;
+ case 'all':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ break;
+ case 'default':
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ delete infoPlist['UIInterfaceOrientation'];
+ }
+}
+
+function handleBuildSettings (platformConfig, locations, infoPlist) {
+ var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios'));
+ var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios');
+ var needUpdatedBuildSettingsForLaunchStoryboard = checkIfBuildSettingsNeedUpdatedForLaunchStoryboard(platformConfig, infoPlist);
+
+ // no build settings provided and we don't need to update build settings for launch storyboards,
+ // then we don't need to parse and update .pbxproj file
+ if (!targetDevice && !deploymentTarget && !needUpdatedBuildSettingsForLaunchStoryboard) {
+ return Q();
+ }
+
+ var proj = new xcode.project(locations.pbxproj); /* eslint new-cap : 0 */
+
+ try {
+ proj.parseSync();
+ } catch (err) {
+ return Q.reject(new CordovaError('Could not parse project.pbxproj: ' + err));
+ }
+
+ if (targetDevice) {
+ events.emit('verbose', 'Set TARGETED_DEVICE_FAMILY to ' + targetDevice + '.');
+ proj.updateBuildProperty('TARGETED_DEVICE_FAMILY', targetDevice);
+ }
+
+ if (deploymentTarget) {
+ events.emit('verbose', 'Set IPHONEOS_DEPLOYMENT_TARGET to "' + deploymentTarget + '".');
+ proj.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget);
+ }
+
+ updateBuildSettingsForLaunchStoryboard(proj, platformConfig, infoPlist);
+
+ fs.writeFileSync(locations.pbxproj, proj.writeSync(), 'utf-8');
+
+ return Q();
+}
+
+function mapIconResources (icons, iconsDir) {
+ // See https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
+ // for launch images sizes reference.
+ var platformIcons = [
+ {dest: 'icon-20.png', width: 20, height: 20},
+ {dest: '[email protected]', width: 40, height: 40},
+ {dest: '[email protected]', width: 60, height: 60},
+ {dest: 'icon-40.png', width: 40, height: 40},
+ {dest: '[email protected]', width: 80, height: 80},
+ {dest: 'icon-50.png', width: 50, height: 50},
+ {dest: '[email protected]', width: 100, height: 100},
+ {dest: '[email protected]', width: 120, height: 120},
+ {dest: '[email protected]', width: 180, height: 180},
+ {dest: 'icon-72.png', width: 72, height: 72},
+ {dest: '[email protected]', width: 144, height: 144},
+ {dest: 'icon-76.png', width: 76, height: 76},
+ {dest: '[email protected]', width: 152, height: 152},
+ {dest: '[email protected]', width: 167, height: 167},
+ {dest: 'icon-1024.png', width: 1024, height: 1024},
+ {dest: 'icon-small.png', width: 29, height: 29},
+ {dest: '[email protected]', width: 58, height: 58},
+ {dest: '[email protected]', width: 87, height: 87},
+ {dest: 'icon.png', width: 57, height: 57},
+ {dest: '[email protected]', width: 114, height: 114},
+ {dest: '[email protected]', width: 48, height: 48},
+ {dest: '[email protected]', width: 55, height: 55},
+ {dest: '[email protected]', width: 58, height: 58},
+ {dest: '[email protected]', width: 87, height: 87},
+ {dest: '[email protected]', width: 80, height: 80},
+ {dest: '[email protected]', width: 88, height: 88},
+ {dest: '[email protected]', width: 172, height: 172},
+ {dest: '[email protected]', width: 196, height: 196}
+ ];
+
+ var pathMap = {};
+ platformIcons.forEach(function (item) {
+ var icon = icons.getBySize(item.width, item.height) || icons.getDefault();
+ if (icon) {
+ var target = path.join(iconsDir, item.dest);
+ pathMap[target] = icon.src;
+ }
+ });
+ return pathMap;
+}
+
+function getIconsDir (projectRoot, platformProjDir) {
+ var iconsDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ iconsDir = path.join(platformProjDir, 'Images.xcassets/AppIcon.appiconset/');
+ } else {
+ iconsDir = path.join(platformProjDir, 'Resources/icons/');
+ }
+
+ return iconsDir;
+}
+
+function updateIcons (cordovaProject, locations) {
+ var icons = cordovaProject.projectConfig.getIcons('ios');
+
+ if (icons.length === 0) {
+ events.emit('verbose', 'This app does not have icons defined');
+ return;
+ }
+
+ var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
+ var iconsDir = getIconsDir(cordovaProject.root, platformProjDir);
+ var resourceMap = mapIconResources(icons, iconsDir);
+ events.emit('verbose', 'Updating icons at ' + iconsDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanIcons (projectRoot, projectConfig, locations) {
+ var icons = projectConfig.getIcons('ios');
+ if (icons.length > 0) {
+ var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+ var iconsDir = getIconsDir(projectRoot, platformProjDir);
+ var resourceMap = mapIconResources(icons, iconsDir);
+ Object.keys(resourceMap).forEach(function (targetIconPath) {
+ resourceMap[targetIconPath] = null;
+ });
+ events.emit('verbose', 'Cleaning icons at ' + iconsDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+function mapSplashScreenResources (splashScreens, splashScreensDir) {
+ var platformSplashScreens = [
+ {dest: 'Default~iphone.png', width: 320, height: 480},
+ {dest: 'Default@2x~iphone.png', width: 640, height: 960},
+ {dest: 'Default-Portrait~ipad.png', width: 768, height: 1024},
+ {dest: 'Default-Portrait@2x~ipad.png', width: 1536, height: 2048},
+ {dest: 'Default-Landscape~ipad.png', width: 1024, height: 768},
+ {dest: 'Default-Landscape@2x~ipad.png', width: 2048, height: 1536},
+ {dest: 'Default-568h@2x~iphone.png', width: 640, height: 1136},
+ {dest: 'Default-667h.png', width: 750, height: 1334},
+ {dest: 'Default-736h.png', width: 1242, height: 2208},
+ {dest: 'Default-Landscape-736h.png', width: 2208, height: 1242},
+ {dest: 'Default-2436h.png', width: 1125, height: 2436},
+ {dest: 'Default-Landscape-2436h.png', width: 2436, height: 1125}
+ ];
+
+ var pathMap = {};
+ platformSplashScreens.forEach(function (item) {
+ var splash = splashScreens.getBySize(item.width, item.height);
+ if (splash) {
+ var target = path.join(splashScreensDir, item.dest);
+ pathMap[target] = splash.src;
+ }
+ });
+ return pathMap;
+}
+
+function getSplashScreensDir (projectRoot, platformProjDir) {
+ var splashScreensDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ splashScreensDir = path.join(platformProjDir, 'Images.xcassets/LaunchImage.launchimage/');
+ } else {
+ splashScreensDir = path.join(platformProjDir, 'Resources/splash/');
+ }
+
+ return splashScreensDir;
+}
+
+function updateSplashScreens (cordovaProject, locations) {
+ var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
+
+ if (splashScreens.length === 0) {
+ events.emit('verbose', 'This app does not have splash screens defined');
+ return;
+ }
+
+ var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
+ var splashScreensDir = getSplashScreensDir(cordovaProject.root, platformProjDir);
+ var resourceMap = mapSplashScreenResources(splashScreens, splashScreensDir);
+ events.emit('verbose', 'Updating splash screens at ' + splashScreensDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanSplashScreens (projectRoot, projectConfig, locations) {
+ var splashScreens = projectConfig.getSplashScreens('ios');
+ if (splashScreens.length > 0) {
+ var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+ var splashScreensDir = getSplashScreensDir(projectRoot, platformProjDir);
+ var resourceMap = mapIconResources(splashScreens, splashScreensDir);
+ Object.keys(resourceMap).forEach(function (targetSplashPath) {
+ resourceMap[targetSplashPath] = null;
+ });
+ events.emit('verbose', 'Cleaning splash screens at ' + splashScreensDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+function updateFileResources (cordovaProject, locations) {
+ const platformDir = path.relative(cordovaProject.root, locations.root);
+ const files = cordovaProject.projectConfig.getFileResources('ios');
+
+ const project = projectFile.parse(locations);
+
+ // if there are resource-file elements in config.xml
+ if (files.length === 0) {
+ events.emit('verbose', 'This app does not have additional resource files defined');
+ return;
+ }
+
+ let resourceMap = {};
+ files.forEach(function (res) {
+ let src = res.src;
+ let target = res.target;
+
+ if (!target) {
+ target = src;
+ }
+
+ let targetPath = path.join(project.resources_dir, target);
+ targetPath = path.relative(cordovaProject.root, targetPath);
+
+ project.xcode.addResourceFile(target);
+
+ resourceMap[targetPath] = src;
+ });
+
+ events.emit('verbose', 'Updating resource files at ' + platformDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+
+ project.write();
+}
+
+function cleanFileResources (projectRoot, projectConfig, locations) {
+ const platformDir = path.relative(projectRoot, locations.root);
+ const files = projectConfig.getFileResources('ios', true);
+ if (files.length > 0) {
+ events.emit('verbose', 'Cleaning resource files at ' + platformDir);
+
+ const project = projectFile.parse(locations);
+
+ var resourceMap = {};
+ files.forEach(function (res) {
+ let src = res.src;
+ let target = res.target;
+
+ if (!target) {
+ target = src;
+ }
+
+ let targetPath = path.join(project.resources_dir, target);
+ targetPath = path.relative(projectRoot, targetPath);
+ const resfile = path.join('Resources', path.basename(targetPath));
+ project.xcode.removeResourceFile(resfile);
+
+ resourceMap[targetPath] = null;
+ });
+
+ FileUpdater.updatePaths(
+ resourceMap, {rootDir: projectRoot, all: true}, logFileOp);
+
+ project.write();
+ }
+}
+
+/**
+ * Returns an array of images for each possible idiom, scale, and size class. The images themselves are
+ * located in the platform's splash images by their pattern (@scale~idiom~sizesize). All possible
+ * combinations are returned, but not all will have a `filename` property. If the latter isn't present,
+ * the device won't attempt to load an image matching the same traits. If the filename is present,
+ * the device will try to load the image if it corresponds to the traits.
+ *
+ * The resulting return looks like this:
+ *
+ * [
+ * {
+ * idiom: 'universal|ipad|iphone',
+ * scale: '1x|2x|3x',
+ * width: 'any|com',
+ * height: 'any|com',
+ * filename: undefined|'Default@scale~idiom~widthheight.png',
+ * src: undefined|'path/to/original/matched/image/from/splash/screens.png',
+ * target: undefined|'path/to/asset/library/Default@scale~idiom~widthheight.png'
+ * }, ...
+ * ]
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Array<Object>}
+ */
+function mapLaunchStoryboardContents (splashScreens, launchStoryboardImagesDir) {
+ var platformLaunchStoryboardImages = [];
+ var idioms = ['universal', 'ipad', 'iphone'];
+ var scalesForIdiom = {
+ universal: ['1x', '2x', '3x'],
+ ipad: ['1x', '2x'],
+ iphone: ['1x', '2x', '3x']
+ };
+ var sizes = ['com', 'any'];
+
+ idioms.forEach(function (idiom) {
+ scalesForIdiom[idiom].forEach(function (scale) {
+ sizes.forEach(function (width) {
+ sizes.forEach(function (height) {
+ var item = {
+ idiom: idiom,
+ scale: scale,
+ width: width,
+ height: height
+ };
+
+ /* examples of the search pattern:
+ * scale ~ idiom ~ width height
+ * @2x ~ universal ~ any any
+ * @3x ~ iphone ~ com any
+ * @2x ~ ipad ~ com any
+ */
+ var searchPattern = '@' + scale + '~' + idiom + '~' + width + height;
+
+ /* because old node versions don't have Array.find, the below is
+ * functionally equivalent to this:
+ * var launchStoryboardImage = splashScreens.find(function(item) {
+ * return item.src.indexOf(searchPattern) >= 0;
+ * });
+ */
+ var launchStoryboardImage = splashScreens.reduce(function (p, c) {
+ return (c.src.indexOf(searchPattern) >= 0) ? c : p;
+ }, undefined);
+
+ if (launchStoryboardImage) {
+ item.filename = 'Default' + searchPattern + '.png';
+ item.src = launchStoryboardImage.src;
+ item.target = path.join(launchStoryboardImagesDir, item.filename);
+ }
+
+ platformLaunchStoryboardImages.push(item);
+ });
+ });
+ });
+ });
+ return platformLaunchStoryboardImages;
+}
+
+/**
+ * Returns a dictionary representing the source and destination paths for the launch storyboard images
+ * that need to be copied.
+ *
+ * The resulting return looks like this:
+ *
+ * {
+ * 'target-path': 'source-path',
+ * ...
+ * }
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Object}
+ */
+function mapLaunchStoryboardResources (splashScreens, launchStoryboardImagesDir) {
+ var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
+ var pathMap = {};
+ platformLaunchStoryboardImages.forEach(function (item) {
+ if (item.target) {
+ pathMap[item.target] = item.src;
+ }
+ });
+ return pathMap;
+}
+
+/**
+ * Builds the object that represents the contents.json file for the LaunchStoryboard image set.
+ *
+ * The resulting return looks like this:
+ *
+ * {
+ * images: [
+ * {
+ * idiom: 'universal|ipad|iphone',
+ * scale: '1x|2x|3x',
+ * width-class: undefined|'compact',
+ * height-class: undefined|'compact'
+ * }, ...
+ * ],
+ * info: {
+ * author: 'Xcode',
+ * version: 1
+ * }
+ * }
+ *
+ * A bit of minor logic is used to map from the array of images returned from mapLaunchStoryboardContents
+ * to the format requried by Xcode.
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Object}
+ */
+function getLaunchStoryboardContentsJSON (splashScreens, launchStoryboardImagesDir) {
+
+ var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = {
+ images: [],
+ info: {
+ author: 'Xcode',
+ version: 1
+ }
+ };
+ contentsJSON.images = platformLaunchStoryboardImages.map(function (item) {
+ var newItem = {
+ idiom: item.idiom,
+ scale: item.scale
+ };
+
+ // Xcode doesn't want any size class property if the class is "any"
+ // If our size class is "com", Xcode wants "compact".
+ if (item.width !== CDV_ANY_SIZE_CLASS) {
+ newItem['width-class'] = IMAGESET_COMPACT_SIZE_CLASS;
+ }
+ if (item.height !== CDV_ANY_SIZE_CLASS) {
+ newItem['height-class'] = IMAGESET_COMPACT_SIZE_CLASS;
+ }
+
+ // Xcode doesn't want a filename property if there's no image for these traits
+ if (item.filename) {
+ newItem.filename = item.filename;
+ }
+ return newItem;
+ });
+ return contentsJSON;
+}
+
+/**
+ * Determines if the project's build settings may need to be updated for launch storyboard support
+ *
+ */
+function checkIfBuildSettingsNeedUpdatedForLaunchStoryboard (platformConfig, infoPlist) {
+ var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
+ var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
+ var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
+
+ if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
+ // don't need legacy launch images if we are using our launch storyboard
+ // so we do need to update the project file
+ events.emit('verbose', 'Need to update build settings because project is using our launch storyboard.');
+ return true;
+ } else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
+ // we do need to ensure legacy launch images are used if there's no launch storyboard present
+ // so we do need to update the project file
+ events.emit('verbose', 'Need to update build settings because project is using legacy launch images and no storyboard.');
+ return true;
+ }
+ events.emit('verbose', 'No need to update build settings for launch storyboard support.');
+ return false;
+}
+
+function updateBuildSettingsForLaunchStoryboard (proj, platformConfig, infoPlist) {
+ var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
+ var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
+ var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
+
+ if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
+ // don't need legacy launch images if we are using our launch storyboard
+ events.emit('verbose', 'Removed ' + LAUNCHIMAGE_BUILD_SETTING + ' because project is using our launch storyboard.');
+ proj.removeBuildProperty(LAUNCHIMAGE_BUILD_SETTING);
+ } else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
+ // we do need to ensure legacy launch images are used if there's no launch storyboard present
+ events.emit('verbose', 'Set ' + LAUNCHIMAGE_BUILD_SETTING + ' to ' + LAUNCHIMAGE_BUILD_SETTING_VALUE + ' because project is using legacy launch images and no storyboard.');
+ proj.updateBuildProperty(LAUNCHIMAGE_BUILD_SETTING, LAUNCHIMAGE_BUILD_SETTING_VALUE);
+ } else {
+ events.emit('verbose', 'Did not update build settings for launch storyboard support.');
+ }
+}
+
+function splashScreensHaveLaunchStoryboardImages (contentsJSON) {
+ /* do we have any launch images do we have for our launch storyboard?
+ * Again, for old Node versions, the below code is equivalent to this:
+ * return !!contentsJSON.images.find(function (item) {
+ * return item.filename !== undefined;
+ * });
+ */
+ return !!contentsJSON.images.reduce(function (p, c) {
+ return (c.filename !== undefined) ? c : p;
+ }, undefined);
+}
+
+function platformHasLaunchStoryboardImages (platformConfig) {
+ var splashScreens = platformConfig.getSplashScreens('ios');
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, ''); // note: we don't need a file path here; we're just counting
+ return splashScreensHaveLaunchStoryboardImages(contentsJSON);
+}
+
+function platformHasLegacyLaunchImages (platformConfig) {
+ var splashScreens = platformConfig.getSplashScreens('ios');
+ return !!splashScreens.reduce(function (p, c) {
+ return (c.width !== undefined || c.height !== undefined) ? c : p;
+ }, undefined);
+}
+
+/**
+ * Updates the project's plist based upon our launch storyboard images. If there are no images, then we should
+ * fall back to the regular launch images that might be supplied (that is, our app will be scaled on an iPad Pro),
+ * and if there are some images, we need to alter the UILaunchStoryboardName property to point to
+ * CDVLaunchScreen.
+ *
+ * There's some logic here to avoid overwriting changes the user might have made to their plist if they are using
+ * their own launch storyboard.
+ */
+function updateProjectPlistForLaunchStoryboard (platformConfig, infoPlist) {
+ var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
+ events.emit('verbose', 'Current launch storyboard ' + currentLaunchStoryboard);
+
+ var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
+
+ if (hasLaunchStoryboardImages && !currentLaunchStoryboard) {
+ // only change the launch storyboard if we have images to use AND the current value is blank
+ // if it's not blank, we've either done this before, or the user has their own launch storyboard
+ events.emit('verbose', 'Changing info plist to use our launch storyboard');
+ infoPlist[UI_LAUNCH_STORYBOARD_NAME] = CDV_LAUNCH_STORYBOARD_NAME;
+ return;
+ }
+
+ if (!hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME) {
+ // only revert to using the launch images if we have don't have any images for the launch storyboard
+ // but only clear it if current launch storyboard is our storyboard; the user might be using their
+ // own storyboard instead.
+ events.emit('verbose', 'Changing info plist to use legacy launch images');
+ delete infoPlist[UI_LAUNCH_STORYBOARD_NAME];
+ return;
+ }
+ events.emit('verbose', 'Not changing launch storyboard setting in info plist.');
+}
+
+/**
+ * Returns the directory for the Launch Storyboard image set, if image sets are being used. If they aren't
+ * being used, returns null.
+ *
+ * @param {string} projectRoot The project's root directory
+ * @param {string} platformProjDir The platform's project directory
+ */
+function getLaunchStoryboardImagesDir (projectRoot, platformProjDir) {
+ var launchStoryboardImagesDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ launchStoryboardImagesDir = path.join(platformProjDir, 'Images.xcassets/LaunchStoryboard.imageset/');
+ } else {
+ // if we don't have a asset library for images, we can't do the storyboard.
+ launchStoryboardImagesDir = null;
+ }
+
+ return launchStoryboardImagesDir;
+}
+
+/**
+ * Update the images for the Launch Storyboard and updates the image set's contents.json file appropriately.
+ *
+ * @param {Object} cordovaProject The cordova project
+ * @param {Object} locations A dictionary containing useful location paths
+ */
+function updateLaunchStoryboardImages (cordovaProject, locations) {
+ var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
+ var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
+ var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(cordovaProject.root, platformProjDir);
+
+ if (launchStoryboardImagesDir) {
+ var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
+
+ events.emit('verbose', 'Updating launch storyboard images at ' + launchStoryboardImagesDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+
+ events.emit('verbose', 'Updating Storyboard image set contents.json');
+ fs.writeFileSync(path.join(cordovaProject.root, launchStoryboardImagesDir, 'Contents.json'),
+ JSON.stringify(contentsJSON, null, 2));
+ }
+}
+
+/**
+ * Removes the images from the launch storyboard's image set and updates the image set's contents.json
+ * file appropriately.
+ *
+ * @param {string} projectRoot Path to the project root
+ * @param {Object} projectConfig The project's config.xml
+ * @param {Object} locations A dictionary containing useful location paths
+ */
+function cleanLaunchStoryboardImages (projectRoot, projectConfig, locations) {
+ var splashScreens = projectConfig.getSplashScreens('ios');
+ var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+ var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(projectRoot, platformProjDir);
+ if (launchStoryboardImagesDir) {
+ var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
+
+ Object.keys(resourceMap).forEach(function (targetPath) {
+ resourceMap[targetPath] = null;
+ });
+ events.emit('verbose', 'Cleaning storyboard image set at ' + launchStoryboardImagesDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+
+ // delete filename from contents.json
+ contentsJSON.images.forEach(function (image) {
+ image.filename = undefined;
+ });
+
+ events.emit('verbose', 'Updating Storyboard image set contents.json');
+ fs.writeFileSync(path.join(projectRoot, launchStoryboardImagesDir, 'Contents.json'),
+ JSON.stringify(contentsJSON, null, 2));
+ }
+}
+
+/**
+ * Queries ConfigParser object for the orientation <preference> value. Warns if
+ * global preference value is not supported by platform.
+ *
+ * @param {Object} platformConfig ConfigParser object
+ *
+ * @return {String} Global/platform-specific orientation in lower-case
+ * (or empty string if both are undefined).
+ */
+function getOrientationValue (platformConfig) {
+
+ var ORIENTATION_DEFAULT = 'default';
+
+ var orientation = platformConfig.getPreference('orientation');
+ if (!orientation) {
+ return '';
+ }
+
+ orientation = orientation.toLowerCase();
+
+ // Check if the given global orientation is supported
+ if (['default', 'portrait', 'landscape', 'all'].indexOf(orientation) >= 0) {
+ return orientation;
+ }
+
+ events.emit('warn', 'Unrecognized value for Orientation preference: ' + orientation +
+ '. Defaulting to value: ' + ORIENTATION_DEFAULT + '.');
+
+ return ORIENTATION_DEFAULT;
+}
+
+/*
+ Parses all <access> and <allow-navigation> entries and consolidates duplicates (for ATS).
+ Returns an object with a Hostname as the key, and the value an object with properties:
+ {
+ Hostname, // String
+ NSExceptionAllowsInsecureHTTPLoads, // boolean
+ NSIncludesSubdomains, // boolean
+ NSExceptionMinimumTLSVersion, // String
+ NSExceptionRequiresForwardSecrecy, // boolean
+ NSRequiresCertificateTransparency, // boolean
+
+ // the three below _only_ show when the Hostname is '*'
+ // if any of the three are set, it disables setting NSAllowsArbitraryLoads
+ // (Apple already enforces this in ATS)
+ NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
+ NSAllowsLocalNetworking, // boolean (default: false)
+ NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
+
+ }
+*/
+function processAccessAndAllowNavigationEntries (config) {
+ var accesses = config.getAccesses();
+ var allow_navigations = config.getAllowNavigations();
+
+ return allow_navigations
+ // we concat allow_navigations and accesses, after processing accesses
+ .concat(accesses.map(function (obj) {
+ // map accesses to a common key interface using 'href', not origin
+ obj.href = obj.origin;
+ delete obj.origin;
+ return obj;
+ }))
+ // we reduce the array to an object with all the entries processed (key is Hostname)
+ .reduce(function (previousReturn, currentElement) {
+ var options = {
+ minimum_tls_version: currentElement.minimum_tls_version,
+ requires_forward_secrecy: currentElement.requires_forward_secrecy,
+ requires_certificate_transparency: currentElement.requires_certificate_transparency,
+ allows_arbitrary_loads_for_media: currentElement.allows_arbitrary_loads_in_media || currentElement.allows_arbitrary_loads_for_media,
+ allows_arbitrary_loads_in_web_content: currentElement.allows_arbitrary_loads_in_web_content,
+ allows_local_networking: currentElement.allows_local_networking
+ };
+ var obj = parseWhitelistUrlForATS(currentElement.href, options);
+
+ if (obj) {
+ // we 'union' duplicate entries
+ var item = previousReturn[obj.Hostname];
+ if (!item) {
+ item = {};
+ }
+ for (var o in obj) {
+ if (obj.hasOwnProperty(o)) {
+ item[o] = obj[o];
+ }
+ }
+ previousReturn[obj.Hostname] = item;
+ }
+ return previousReturn;
+ }, {});
+}
+
+/*
+ Parses a URL and returns an object with these keys:
+ {
+ Hostname, // String
+ NSExceptionAllowsInsecureHTTPLoads, // boolean (default: false)
+ NSIncludesSubdomains, // boolean (default: false)
+ NSExceptionMinimumTLSVersion, // String (default: 'TLSv1.2')
+ NSExceptionRequiresForwardSecrecy, // boolean (default: true)
+ NSRequiresCertificateTransparency, // boolean (default: false)
+
+ // the three below _only_ apply when the Hostname is '*'
+ // if any of the three are set, it disables setting NSAllowsArbitraryLoads
+ // (Apple already enforces this in ATS)
+ NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
+ NSAllowsLocalNetworking, // boolean (default: false)
+ NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
+ }
+
+ null is returned if the URL cannot be parsed, or is to be skipped for ATS.
+*/
+function parseWhitelistUrlForATS (url, options) {
+ var href = URL.parse(url);
+ var retObj = {};
+ retObj.Hostname = href.hostname;
+
+ // Guiding principle: we only set values in retObj if they are NOT the default
+
+ if (url === '*') {
+ retObj.Hostname = '*';
+ var val;
+
+ val = (options.allows_arbitrary_loads_in_web_content === 'true');
+ if (options.allows_arbitrary_loads_in_web_content && val) { // default is false
+ retObj.NSAllowsArbitraryLoadsInWebContent = true;
+ }
+
+ val = (options.allows_arbitrary_loads_for_media === 'true');
+ if (options.allows_arbitrary_loads_for_media && val) { // default is false
+ retObj.NSAllowsArbitraryLoadsForMedia = true;
+ }
+
+ val = (options.allows_local_networking === 'true');
+ if (options.allows_local_networking && val) { // default is false
+ retObj.NSAllowsLocalNetworking = true;
+ }
+
+ return retObj;
+ }
+
+ if (!retObj.Hostname) {
+ // check origin, if it allows subdomains (wildcard in hostname), we set NSIncludesSubdomains to YES. Default is NO
+ var subdomain1 = '/*.'; // wildcard in hostname
+ var subdomain2 = '*://*.'; // wildcard in hostname and protocol
+ var subdomain3 = '*://'; // wildcard in protocol only
+ if (href.pathname.indexOf(subdomain1) === 0) {
+ retObj.NSIncludesSubdomains = true;
+ retObj.Hostname = href.pathname.substring(subdomain1.length);
+ } else if (href.pathname.indexOf(subdomain2) === 0) {
+ retObj.NSIncludesSubdomains = true;
+ retObj.Hostname = href.pathname.substring(subdomain2.length);
+ } else if (href.pathname.indexOf(subdomain3) === 0) {
+ retObj.Hostname = href.pathname.substring(subdomain3.length);
+ } else {
+ // Handling "scheme:*" case to avoid creating of a blank key in NSExceptionDomains.
+ return null;
+ }
+ }
+
+ if (options.minimum_tls_version && options.minimum_tls_version !== 'TLSv1.2') { // default is TLSv1.2
+ retObj.NSExceptionMinimumTLSVersion = options.minimum_tls_version;
+ }
+
+ var rfs = (options.requires_forward_secrecy === 'true');
+ if (options.requires_forward_secrecy && !rfs) { // default is true
+ retObj.NSExceptionRequiresForwardSecrecy = false;
+ }
+
+ var rct = (options.requires_certificate_transparency === 'true');
+ if (options.requires_certificate_transparency && rct) { // default is false
+ retObj.NSRequiresCertificateTransparency = true;
+ }
+
+ // if the scheme is HTTP, we set NSExceptionAllowsInsecureHTTPLoads to YES. Default is NO
+ if (href.protocol === 'http:') {
+ retObj.NSExceptionAllowsInsecureHTTPLoads = true;
+ } else if (!href.protocol && href.pathname.indexOf('*:/') === 0) { // wilcard in protocol
+ retObj.NSExceptionAllowsInsecureHTTPLoads = true;
+ }
+
+ return retObj;
+}
+
+/*
+ App Transport Security (ATS) writer from <access> and <allow-navigation> tags
+ in config.xml
+*/
+function writeATSEntries (config) {
+ var pObj = processAccessAndAllowNavigationEntries(config);
+
+ var ats = {};
+
+ for (var hostname in pObj) {
+ if (pObj.hasOwnProperty(hostname)) {
+ var entry = pObj[hostname];
+
+ // Guiding principle: we only set values if they are available
+
+ if (hostname === '*') {
+ // always write this, for iOS 9, since in iOS 10 it will be overriden if
+ // any of the other three keys are written
+ ats['NSAllowsArbitraryLoads'] = true;
+
+ // at least one of the overriding keys is present
+ if (entry.NSAllowsArbitraryLoadsInWebContent) {
+ ats['NSAllowsArbitraryLoadsInWebContent'] = true;
+ }
+ if (entry.NSAllowsArbitraryLoadsForMedia) {
+ ats['NSAllowsArbitraryLoadsForMedia'] = true;
+ }
+ if (entry.NSAllowsLocalNetworking) {
+ ats['NSAllowsLocalNetworking'] = true;
+ }
+
+ continue;
+ }
+
+ var exceptionDomain = {};
+
+ for (var key in entry) {
+ if (entry.hasOwnProperty(key) && key !== 'Hostname') {
+ exceptionDomain[key] = entry[key];
+ }
+ }
+
+ if (!ats['NSExceptionDomains']) {
+ ats['NSExceptionDomains'] = {};
+ }
+
+ ats['NSExceptionDomains'][hostname] = exceptionDomain;
+ }
+ }
+
+ return ats;
+}
+
+function folderExists (folderPath) {
+ try {
+ var stat = fs.statSync(folderPath);
+ return stat && stat.isDirectory();
+ } catch (e) {
+ return false;
+ }
+}
+
+// Construct a default value for CFBundleVersion as the version with any
+// -rclabel stripped=.
+function default_CFBundleVersion (version) {
+ return version.split('-')[0];
+}
+
+// Converts cordova specific representation of target device to XCode value
+function parseTargetDevicePreference (value) {
+ if (!value) return null;
+ var map = {'universal': '"1,2"', 'handset': '"1"', 'tablet': '"2"'};
+ if (map[value.toLowerCase()]) {
+ return map[value.toLowerCase()];
+ }
+ events.emit('warn', 'Unrecognized value for target-device preference: ' + value + '.');
+ return null;
+}
diff --git a/cordova/lib/projectFile.js b/cordova/lib/projectFile.js
new file mode 100755
index 0000000..8a3f7e5
--- /dev/null
+++ b/cordova/lib/projectFile.js
@@ -0,0 +1,134 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var xcode = require('xcode');
+var plist = require('plist');
+var _ = require('underscore');
+var path = require('path');
+var fs = require('fs');
+var shell = require('shelljs');
+
+var pluginHandlers = require('./plugman/pluginHandlers');
+var CordovaError = require('cordova-common').CordovaError;
+
+var cachedProjectFiles = {};
+
+function parseProjectFile (locations) {
+ var project_dir = locations.root;
+ var pbxPath = locations.pbxproj;
+
+ if (cachedProjectFiles[project_dir]) {
+ return cachedProjectFiles[project_dir];
+ }
+
+ var xcodeproj = xcode.project(pbxPath);
+ xcodeproj.parseSync();
+
+ var xcBuildConfiguration = xcodeproj.pbxXCBuildConfigurationSection();
+ var plist_file_entry = _.find(xcBuildConfiguration, function (entry) { return entry.buildSettings && entry.buildSettings.INFOPLIST_FILE; });
+ var plist_file = path.join(project_dir, plist_file_entry.buildSettings.INFOPLIST_FILE.replace(/^"(.*)"$/g, '$1').replace(/\\&/g, '&'));
+ var config_file = path.join(path.dirname(plist_file), 'config.xml');
+
+ if (!fs.existsSync(plist_file) || !fs.existsSync(config_file)) {
+ throw new CordovaError('Could not find *-Info.plist file, or config.xml file.');
+ }
+
+ var frameworks_file = path.join(project_dir, 'frameworks.json');
+ var frameworks = {};
+ try {
+ frameworks = require(frameworks_file);
+ } catch (e) { }
+
+ var xcode_dir = path.dirname(plist_file);
+ var pluginsDir = path.resolve(xcode_dir, 'Plugins');
+ var resourcesDir = path.resolve(xcode_dir, 'Resources');
+
+ cachedProjectFiles[project_dir] = {
+ plugins_dir: pluginsDir,
+ resources_dir: resourcesDir,
+ xcode: xcodeproj,
+ xcode_path: xcode_dir,
+ pbx: pbxPath,
+ projectDir: project_dir,
+ platformWww: path.join(project_dir, 'platform_www'),
+ www: path.join(project_dir, 'www'),
+ write: function () {
+ fs.writeFileSync(pbxPath, xcodeproj.writeSync());
+ if (Object.keys(this.frameworks).length === 0) {
+ // If there is no framework references remain in the project, just remove this file
+ shell.rm('-rf', frameworks_file);
+ return;
+ }
+ fs.writeFileSync(frameworks_file, JSON.stringify(this.frameworks, null, 4));
+ },
+ getPackageName: function () {
+ return plist.parse(fs.readFileSync(plist_file, 'utf8')).CFBundleIdentifier;
+ },
+ getInstaller: function (name) {
+ return pluginHandlers.getInstaller(name);
+ },
+ getUninstaller: function (name) {
+ return pluginHandlers.getUninstaller(name);
+ },
+ frameworks: frameworks
+ };
+ return cachedProjectFiles[project_dir];
+}
+
+function purgeProjectFileCache (project_dir) {
+ delete cachedProjectFiles[project_dir];
+}
+
+module.exports = {
+ parse: parseProjectFile,
+ purgeProjectFileCache: purgeProjectFileCache
+};
+
+xcode.project.prototype.pbxEmbedFrameworksBuildPhaseObj = function (target) {
+ return this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Embed Frameworks', target);
+};
+
+xcode.project.prototype.addToPbxEmbedFrameworksBuildPhase = function (file) {
+ var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target);
+ if (sources) {
+ sources.files.push(pbxBuildPhaseObj(file));
+ }
+};
+xcode.project.prototype.removeFromPbxEmbedFrameworksBuildPhase = function (file) {
+ var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target);
+ if (sources) {
+ sources.files = _.reject(sources.files, function (file) {
+ return file.comment === longComment(file);
+ });
+ }
+};
+
+// special handlers to add frameworks to the 'Embed Frameworks' build phase, needed for custom frameworks
+// see CB-9517. should probably be moved to node-xcode.
+var util = require('util');
+function pbxBuildPhaseObj (file) {
+ var obj = Object.create(null);
+ obj.value = file.uuid;
+ obj.comment = longComment(file);
+ return obj;
+}
+
+function longComment (file) {
+ return util.format('%s in %s', file.basename, file.group);
+}
diff --git a/cordova/lib/run.js b/cordova/lib/run.js
new file mode 100755
index 0000000..3a6246e
--- /dev/null
+++ b/cordova/lib/run.js
@@ -0,0 +1,244 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var Q = require('q');
+var path = require('path');
+var iossim = require('ios-sim');
+var build = require('./build');
+var spawn = require('./spawn');
+var check_reqs = require('./check_reqs');
+
+var events = require('cordova-common').events;
+
+var cordovaPath = path.join(__dirname, '..');
+var projectPath = path.join(__dirname, '..', '..');
+
+module.exports.run = function (runOptions) {
+
+ // Validate args
+ if (runOptions.device && runOptions.emulator) {
+ return Q.reject('Only one of "device"/"emulator" options should be specified');
+ }
+
+ // support for CB-8168 `cordova/run --list`
+ if (runOptions.list) {
+ if (runOptions.device) return module.exports.listDevices();
+ if (runOptions.emulator) return module.exports.listEmulators();
+ // if no --device or --emulator flag is specified, list both devices and emulators
+ return module.exports.listDevices().then(function () {
+ return module.exports.listEmulators();
+ });
+ }
+
+ var useDevice = !!runOptions.device;
+
+ return require('./list-devices').run()
+ .then(function (devices) {
+ if (devices.length > 0 && !(runOptions.emulator)) {
+ useDevice = true;
+ // we also explicitly set device flag in options as we pass
+ // those parameters to other api (build as an example)
+ runOptions.device = true;
+ return check_reqs.check_ios_deploy();
+ }
+ }).then(function () {
+ if (!runOptions.nobuild) {
+ return build.run(runOptions);
+ } else {
+ return Q.resolve();
+ }
+ }).then(function () {
+ return build.findXCodeProjectIn(projectPath);
+ }).then(function (projectName) {
+ var appPath = path.join(projectPath, 'build', 'emulator', projectName + '.app');
+ var buildOutputDir = path.join(projectPath, 'build', 'device');
+
+ // select command to run and arguments depending whether
+ // we're running on device/emulator
+ if (useDevice) {
+ return module.exports.checkDeviceConnected()
+ .then(function () {
+ // Unpack IPA
+ var ipafile = path.join(buildOutputDir, projectName + '.ipa');
+
+ // unpack the existing platform/ios/build/device/appname.ipa (zipfile), will create a Payload folder
+ return spawn('unzip', [ '-o', '-qq', ipafile ], buildOutputDir);
+ })
+ .then(function () {
+ // Uncompress IPA (zip file)
+ var appFileInflated = path.join(buildOutputDir, 'Payload', projectName + '.app');
+ var appFile = path.join(buildOutputDir, projectName + '.app');
+ var payloadFolder = path.join(buildOutputDir, 'Payload');
+
+ // delete the existing platform/ios/build/device/appname.app
+ return spawn('rm', [ '-rf', appFile ], buildOutputDir)
+ .then(function () {
+ // move the platform/ios/build/device/Payload/appname.app to parent
+ return spawn('mv', [ '-f', appFileInflated, buildOutputDir ], buildOutputDir);
+ })
+ .then(function () {
+ // delete the platform/ios/build/device/Payload folder
+ return spawn('rm', [ '-rf', payloadFolder ], buildOutputDir);
+ });
+ })
+ .then(function () {
+ appPath = path.join(projectPath, 'build', 'device', projectName + '.app');
+ var extraArgs = [];
+ if (runOptions.argv) {
+ // argv.slice(2) removes node and run.js, filterSupportedArgs removes the run.js args
+ extraArgs = module.exports.filterSupportedArgs(runOptions.argv.slice(2));
+ }
+ return module.exports.deployToDevice(appPath, runOptions.target, extraArgs);
+ }, function () {
+ // if device connection check failed use emulator then
+ return module.exports.deployToSim(appPath, runOptions.target);
+ });
+ } else {
+ return module.exports.deployToSim(appPath, runOptions.target);
+ }
+ });
+};
+
+module.exports.filterSupportedArgs = filterSupportedArgs;
+module.exports.checkDeviceConnected = checkDeviceConnected;
+module.exports.deployToDevice = deployToDevice;
+module.exports.deployToSim = deployToSim;
+module.exports.startSim = startSim;
+module.exports.listDevices = listDevices;
+module.exports.listEmulators = listEmulators;
+
+/**
+ * Filters the args array and removes supported args for the 'run' command.
+ *
+ * @return {Array} array with unsupported args for the 'run' command
+ */
+function filterSupportedArgs (args) {
+ var filtered = [];
+ var sargs = ['--device', '--emulator', '--nobuild', '--list', '--target', '--debug', '--release'];
+ var re = new RegExp(sargs.join('|'));
+
+ args.forEach(function (element) {
+ // supported args not found, we add
+ // we do a regex search because --target can be "--target=XXX"
+ if (element.search(re) === -1) {
+ filtered.push(element);
+ }
+ }, this);
+
+ return filtered;
+}
+
+/**
+ * Checks if any iOS device is connected
+ * @return {Promise} Fullfilled when any device is connected, rejected otherwise
+ */
+function checkDeviceConnected () {
+ return spawn('ios-deploy', ['-c', '-t', '1']);
+}
+
+/**
+ * Deploy specified app package to connected device
+ * using ios-deploy command
+ * @param {String} appPath Path to application package
+ * @return {Promise} Resolves when deploy succeeds otherwise rejects
+ */
+function deployToDevice (appPath, target, extraArgs) {
+ // Deploying to device...
+ if (target) {
+ return spawn('ios-deploy', ['--justlaunch', '-d', '-b', appPath, '-i', target].concat(extraArgs));
+ } else {
+ return spawn('ios-deploy', ['--justlaunch', '--no-wifi', '-d', '-b', appPath].concat(extraArgs));
+ }
+}
+
+/**
+ * Deploy specified app package to ios-sim simulator
+ * @param {String} appPath Path to application package
+ * @param {String} target Target device type
+ * @return {Promise} Resolves when deploy succeeds otherwise rejects
+ */
+function deployToSim (appPath, target) {
+ // Select target device for emulator. Default is 'iPhone-6'
+ if (!target) {
+ return require('./list-emulator-images').run()
+ .then(function (emulators) {
+ if (emulators.length > 0) {
+ target = emulators[0];
+ }
+ emulators.forEach(function (emulator) {
+ if (emulator.indexOf('iPhone') === 0) {
+ target = emulator;
+ }
+ });
+ events.emit('log', 'No target specified for emulator. Deploying to ' + target + ' simulator');
+ return startSim(appPath, target);
+ });
+ } else {
+ return startSim(appPath, target);
+ }
+}
+
+function startSim (appPath, target) {
+ var logPath = path.join(cordovaPath, 'console.log');
+
+ return iossim.launch(appPath, 'com.apple.CoreSimulator.SimDeviceType.' + target, logPath, '--exit');
+}
+
+function listDevices () {
+ return require('./list-devices').run()
+ .then(function (devices) {
+ events.emit('log', 'Available iOS Devices:');
+ devices.forEach(function (device) {
+ events.emit('log', '\t' + device);
+ });
+ });
+}
+
+function listEmulators () {
+ return require('./list-emulator-images').run()
+ .then(function (emulators) {
+ events.emit('log', 'Available iOS Simulators:');
+ emulators.forEach(function (emulator) {
+ events.emit('log', '\t' + emulator);
+ });
+ });
+}
+
+module.exports.help = function () {
+ console.log('\nUsage: run [ --device | [ --emulator [ --target=<id> ] ] ] [ --debug | --release | --nobuild ]');
+ // TODO: add support for building different archs
+ // console.log(" [ --archs=\"<list of target architectures>\" ] ");
+ console.log(' --device : Deploys and runs the project on the connected device.');
+ console.log(' --emulator : Deploys and runs the project on an emulator.');
+ console.log(' --target=<id> : Deploys and runs the project on the specified target.');
+ console.log(' --debug : Builds project in debug mode. (Passed down to build command, if necessary)');
+ console.log(' --release : Builds project in release mode. (Passed down to build command, if necessary)');
+ console.log(' --nobuild : Uses pre-built package, or errors if project is not built.');
+ // TODO: add support for building different archs
+ // console.log(" --archs : Specific chip architectures (`anycpu`, `arm`, `x86`, `x64`).");
+ console.log('');
+ console.log('Examples:');
+ console.log(' run');
+ console.log(' run --device');
+ console.log(' run --emulator --target=\"iPhone-6-Plus\"'); /* eslint no-useless-escape : 0 */
+ console.log(' run --device --release');
+ console.log(' run --emulator --debug');
+ console.log('');
+ process.exit(0);
+};
diff --git a/cordova/lib/spawn.js b/cordova/lib/spawn.js
new file mode 100755
index 0000000..b5a5685
--- /dev/null
+++ b/cordova/lib/spawn.js
@@ -0,0 +1,47 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var Q = require('q');
+var proc = require('child_process');
+
+/**
+ * Run specified command with arguments
+ * @param {String} cmd Command
+ * @param {Array} args Array of arguments that should be passed to command
+ * @param {String} opt_cwd Working directory for command
+ * @param {String} opt_verbosity Verbosity level for command stdout output, "verbose" by default
+ * @return {Promise} Promise either fullfilled or rejected with error code
+ */
+module.exports = function (cmd, args, opt_cwd) {
+ var d = Q.defer();
+ try {
+ var child = proc.spawn(cmd, args, {cwd: opt_cwd, stdio: 'inherit'});
+
+ child.on('exit', function (code) {
+ if (code) {
+ d.reject('Error code ' + code + ' for command: ' + cmd + ' with args: ' + args);
+ } else {
+ d.resolve();
+ }
+ });
+ } catch (e) {
+ d.reject(e);
+ }
+ return d.promise;
+};
diff --git a/cordova/lib/start-emulator b/cordova/lib/start-emulator
new file mode 100755
index 0000000..624335b
--- /dev/null
+++ b/cordova/lib/start-emulator
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Run the below to get the device targets:
+# xcrun instruments -s
+
+set -e
+
+
+DEFAULT_TARGET="iPhone 5s"
+TARGET=${1:-$DEFAULT_TARGET}
+LIB_PATH=$( cd "$( dirname "$0" )" && pwd -P)
+
+xcrun instruments -w "$TARGET" &> /dev/null \ No newline at end of file
diff --git a/cordova/lib/versions.js b/cordova/lib/versions.js
new file mode 100755
index 0000000..c6a41b8
--- /dev/null
+++ b/cordova/lib/versions.js
@@ -0,0 +1,194 @@
+#!/usr/bin/env node
+
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+var child_process = require('child_process');
+var Q = require('q');
+
+exports.get_apple_ios_version = function () {
+ var d = Q.defer();
+ child_process.exec('xcodebuild -showsdks', function (error, stdout, stderr) {
+ if (error) {
+ d.reject(stderr);
+ } else {
+ d.resolve(stdout);
+ }
+ });
+
+ return d.promise.then(function (output) {
+ var regex = /[0-9]*\.[0-9]*/;
+ var versions = [];
+ var regexIOS = /^iOS \d+/;
+ output = output.split('\n');
+ for (var i = 0; i < output.length; i++) {
+ if (output[i].trim().match(regexIOS)) {
+ versions[versions.length] = parseFloat(output[i].match(regex)[0]);
+ }
+ }
+ versions.sort();
+ console.log(versions[0]);
+ return Q();
+ }, function (stderr) {
+ return Q.reject(stderr);
+ });
+};
+
+exports.get_apple_osx_version = function () {
+ var d = Q.defer();
+ child_process.exec('xcodebuild -showsdks', function (error, stdout, stderr) {
+ if (error) {
+ d.reject(stderr);
+ } else {
+ d.resolve(stdout);
+ }
+ });
+
+ return d.promise.then(function (output) {
+ var regex = /[0-9]*\.[0-9]*/;
+ var versions = [];
+ var regexOSX = /^OS X \d+/;
+ output = output.split('\n');
+ for (var i = 0; i < output.length; i++) {
+ if (output[i].trim().match(regexOSX)) {
+ versions[versions.length] = parseFloat(output[i].match(regex)[0]);
+ }
+ }
+ versions.sort();
+ console.log(versions[0]);
+ return Q();
+ }, function (stderr) {
+ return Q.reject(stderr);
+ });
+};
+
+exports.get_apple_xcode_version = function () {
+ var d = Q.defer();
+ child_process.exec('xcodebuild -version', function (error, stdout, stderr) {
+ var versionMatch = /Xcode (.*)/.exec(stdout);
+ if (error || !versionMatch) {
+ d.reject(stderr);
+ } else {
+ d.resolve(versionMatch[1]);
+ }
+ });
+ return d.promise;
+};
+
+/**
+ * Gets ios-deploy util version
+ * @return {Promise} Promise that either resolved with ios-deploy version
+ * or rejected in case of error
+ */
+exports.get_ios_deploy_version = function () {
+ var d = Q.defer();
+ child_process.exec('ios-deploy --version', function (error, stdout, stderr) {
+ if (error) {
+ d.reject(stderr);
+ } else {
+ d.resolve(stdout);
+ }
+ });
+ return d.promise;
+};
+
+/**
+ * Gets pod (CocoaPods) util version
+ * @return {Promise} Promise that either resolved with pod version
+ * or rejected in case of error
+ */
+exports.get_cocoapods_version = function () {
+ var d = Q.defer();
+ child_process.exec('pod --version', function (error, stdout, stderr) {
+ if (error) {
+ d.reject(stderr);
+ } else {
+ d.resolve(stdout);
+ }
+ });
+ return d.promise;
+};
+
+/**
+ * Gets ios-sim util version
+ * @return {Promise} Promise that either resolved with ios-sim version
+ * or rejected in case of error
+ */
+exports.get_ios_sim_version = function () {
+ var d = Q.defer();
+ child_process.exec('ios-sim --version', function (error, stdout, stderr) {
+ if (error) {
+ d.reject(stderr);
+ } else {
+ d.resolve(stdout);
+ }
+ });
+ return d.promise;
+};
+
+/**
+ * Gets specific tool version
+ * @param {String} toolName Tool name to check. Known tools are 'xcodebuild', 'ios-sim' and 'ios-deploy'
+ * @return {Promise} Promise that either resolved with tool version
+ * or rejected in case of error
+ */
+exports.get_tool_version = function (toolName) {
+ switch (toolName) {
+ case 'xcodebuild': return exports.get_apple_xcode_version();
+ case 'ios-sim': return exports.get_ios_sim_version();
+ case 'ios-deploy': return exports.get_ios_deploy_version();
+ case 'pod': return exports.get_cocoapods_version();
+ default: return Q.reject(toolName + ' is not valid tool name. Valid names are: \'xcodebuild\', \'ios-sim\', \'ios-deploy\', and \'pod\'');
+ }
+};
+
+/**
+ * Compares two semver-notated version strings. Returns number
+ * that indicates equality of provided version strings.
+ * @param {String} version1 Version to compare
+ * @param {String} version2 Another version to compare
+ * @return {Number} Negative number if first version is lower than the second,
+ * positive otherwise and 0 if versions are equal.
+ */
+exports.compareVersions = function (version1, version2) {
+ function parseVer (version) {
+ return version.split('.').map(function (value) {
+ // try to convert version segment to Number
+ var parsed = Number(value);
+ // Number constructor is strict enough and will return NaN
+ // if conversion fails. In this case we won't be able to compare versions properly
+ if (isNaN(parsed)) {
+ throw 'Version should contain only numbers and dots';
+ }
+ return parsed;
+ });
+ }
+ var parsedVer1 = parseVer(version1);
+ var parsedVer2 = parseVer(version2);
+
+ // Compare corresponding segments of each version
+ for (var i = 0; i < Math.max(parsedVer1.length, parsedVer2.length); i++) {
+ // if segment is not specified, assume that it is 0
+ // E.g. 3.1 is equal to 3.1.0
+ var ret = (parsedVer1[i] || 0) - (parsedVer2[i] || 0);
+ // if segments are not equal, we're finished
+ if (ret !== 0) return ret;
+ }
+ return 0;
+};