diff options
| author | Kumar Priyansh <[email protected]> | 2019-01-19 12:37:14 +0530 |
|---|---|---|
| committer | Kumar Priyansh <[email protected]> | 2019-01-19 12:37:14 +0530 |
| commit | dcdfc94cb39dfe2c39925a0145ffa45e2d061c30 (patch) | |
| tree | 4f6379d955555b298c0e7b83a67e264240ee5614 /cordova/lib | |
| parent | 76f7b3678d3f1ff99c3935a774d420453b0c3cb9 (diff) | |
| download | WeatherApp-dcdfc94cb39dfe2c39925a0145ffa45e2d061c30.tar.xz WeatherApp-dcdfc94cb39dfe2c39925a0145ffa45e2d061c30.zip | |
Initial Upload via GIT
Diffstat (limited to 'cordova/lib')
| -rwxr-xr-x | cordova/lib/Podfile.js | 245 | ||||
| -rwxr-xr-x | cordova/lib/PodsJson.js | 115 | ||||
| -rwxr-xr-x | cordova/lib/build.js | 412 | ||||
| -rwxr-xr-x | cordova/lib/check_reqs.js | 228 | ||||
| -rwxr-xr-x | cordova/lib/clean.js | 42 | ||||
| -rwxr-xr-x | cordova/lib/copy-www-build-step.js | 73 | ||||
| -rwxr-xr-x | cordova/lib/list-devices | 67 | ||||
| -rwxr-xr-x | cordova/lib/list-emulator-build-targets | 107 | ||||
| -rwxr-xr-x | cordova/lib/list-emulator-images | 47 | ||||
| -rwxr-xr-x | cordova/lib/list-started-emulators | 50 | ||||
| -rwxr-xr-x | cordova/lib/plugman/pluginHandlers.js | 400 | ||||
| -rwxr-xr-x | cordova/lib/prepare.js | 1153 | ||||
| -rwxr-xr-x | cordova/lib/projectFile.js | 134 | ||||
| -rwxr-xr-x | cordova/lib/run.js | 244 | ||||
| -rwxr-xr-x | cordova/lib/spawn.js | 47 | ||||
| -rwxr-xr-x | cordova/lib/start-emulator | 30 | ||||
| -rwxr-xr-x | cordova/lib/versions.js | 194 |
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; +}; |
