diff options
| author | Jules Laplace <jules@okfoc.us> | 2016-11-08 11:46:59 -0500 |
|---|---|---|
| committer | Jules Laplace <jules@okfoc.us> | 2016-11-08 11:46:59 -0500 |
| commit | 5fa81da81260d65113f57a293b6256d334fe8e2d (patch) | |
| tree | 01d3dd7ab7a1febccd20de1756d0801a64ae64e9 /StoneIsland/platforms/ios/cordova/lib | |
| parent | e5652e9cd560ccda249819857c207643820b075f (diff) | |
| parent | 7773d1d0686de69504e9b820efdb3e94d72eff04 (diff) | |
le build
Diffstat (limited to 'StoneIsland/platforms/ios/cordova/lib')
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/Podfile.js | 230 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/PodsJson.js | 115 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/build.js | 304 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/check_reqs.js | 96 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/clean.js | 8 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/copy-www-build-step.js | 14 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/list-emulator-images | 10 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/plugman/pluginHandlers.js | 375 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/prepare.js | 1003 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/projectFile.js | 136 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/run.js | 146 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/spawn.js | 1 | ||||
| -rwxr-xr-x | StoneIsland/platforms/ios/cordova/lib/versions.js | 20 |
13 files changed, 2281 insertions, 177 deletions
diff --git a/StoneIsland/platforms/ios/cordova/lib/Podfile.js b/StoneIsland/platforms/ios/cordova/lib/Podfile.js new file mode 100755 index 00000000..2cf254cc --- /dev/null +++ b/StoneIsland/platforms/ios/cordova/lib/Podfile.js @@ -0,0 +1,230 @@ +/* + 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'), + path = require('path'), + util = require('util'), + events = require('cordova-common').events, + Q = require('q'), + superspawn = require('cordova-common').superspawn, + 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 \'(\\w+)\'\\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, ''); + obj[m[1]] = podSpec; // i.e pod 'Foo', '1.2' ==> { 'Foo' : '1.2'} + } + + return obj; + }, {}); +}; + +Podfile.prototype.getTemplate = function() { + 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', + this.projectName, this.projectName, this.podToken); +}; + +Podfile.prototype.addSpec = function(name, spec) { + name = name || ''; + spec = spec; // optional + + 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() { + // 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(); +}; + +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() { + return self.before_install(); + }) + .then(function() { + 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;
\ No newline at end of file diff --git a/StoneIsland/platforms/ios/cordova/lib/PodsJson.js b/StoneIsland/platforms/ios/cordova/lib/PodsJson.js new file mode 100755 index 00000000..b13a1afe --- /dev/null +++ b/StoneIsland/platforms/ios/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'), + path = require('path'), + util = require('util'), + events = require('cordova-common').events, + 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/StoneIsland/platforms/ios/cordova/lib/build.js b/StoneIsland/platforms/ios/cordova/lib/build.js index 2213ef8c..a26f1983 100755 --- a/StoneIsland/platforms/ios/cordova/lib/build.js +++ b/StoneIsland/platforms/ios/cordova/lib/build.js @@ -6,9 +6,9 @@ * 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 @@ -20,98 +20,170 @@ /*jshint node: true*/ var Q = require('q'), - nopt = require('nopt'), path = require('path'), shell = require('shelljs'), spawn = require('./spawn'), - check_reqs = require('./check_reqs'), - fs = require('fs'); + fs = require('fs'), + plist = require('plist'), + util = require('util'); + +var check_reqs; +try { + check_reqs = require('./check_reqs'); +} catch (err) { + // For unit tests, check_reqs.js is not a sibling to build.js + check_reqs = require('../../../../lib/check_reqs'); +} + +var events = require('cordova-common').events; var projectPath = path.join(__dirname, '..', '..'); var projectName = null; -module.exports.run = function (argv) { +// These are regular expressions to detect if the user is changing any of the built-in xcodebuildArgs +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=.*)/ +}; - var args = nopt({ - // "archs": String, // TODO: add support for building different archs - 'debug': Boolean, - 'release': Boolean, - 'device': Boolean, - 'emulator': Boolean, - 'codeSignIdentity': String, - 'codeSignResourceRules': String, - 'provisioningProfile': String, - 'buildConfig' : String - }, {'-r': '--release'}, argv); +module.exports.run = function (buildOpts) { - if (args.debug && args.release) { - return Q.reject('Only one of "debug"/"release" options should be specified'); + buildOpts = buildOpts || {}; + + if (buildOpts.debug && buildOpts.release) { + return Q.reject('Cannot specify "debug" and "release" options together.'); } - if (args.device && args.emulator) { - return Q.reject('Only one of "device"/"emulator" options should be specified'); + if (buildOpts.device && buildOpts.emulator) { + return Q.reject('Cannot specify "device" and "emulator" options together.'); } - if(args.buildConfig) { - if(!fs.existsSync(args.buildConfig)) { - return Q.reject('Build config file does not exist:' + args.buildConfig); + if(buildOpts.buildConfig) { + if(!fs.existsSync(buildOpts.buildConfig)) { + return Q.reject('Build config file does not exist:' + buildOpts.buildConfig); } - console.log('Reading build config file:', path.resolve(args.buildConfig)); - var buildConfig = JSON.parse(fs.readFileSync(args.buildConfig, 'utf-8')); + 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 = args.release ? 'release' : 'debug'; + var buildType = buildOpts.release ? 'release' : 'debug'; var config = buildConfig.ios[buildType]; if(config) { - ['codeSignIdentity', 'codeSignResourceRules', 'provisioningProfile'].forEach( + ['codeSignIdentity', 'codeSignResourceRules', 'provisioningProfile', 'developmentTeam', 'packageType'].forEach( function(key) { - args[key] = args[key] || config[key]; + buildOpts[key] = buildOpts[key] || config[key]; }); } } } - + return check_reqs.run().then(function () { return findXCodeProjectIn(projectPath); }).then(function (name) { projectName = name; var extraConfig = ''; - if (args.codeSignIdentity) { - extraConfig += 'CODE_SIGN_IDENTITY = ' + args.codeSignIdentity + '\n'; - extraConfig += 'CODE_SIGN_IDENTITY[sdk=iphoneos*] = ' + args.codeSignIdentity + '\n'; + if (buildOpts.codeSignIdentity) { + extraConfig += 'CODE_SIGN_IDENTITY = ' + buildOpts.codeSignIdentity + '\n'; + extraConfig += 'CODE_SIGN_IDENTITY[sdk=iphoneos*] = ' + buildOpts.codeSignIdentity + '\n'; } - if (args.codeSignResourceRules) { - extraConfig += 'CODE_SIGN_RESOURCE_RULES_PATH = ' + args.codeSignResourceRules + '\n'; + if (buildOpts.codeSignResourceRules) { + extraConfig += 'CODE_SIGN_RESOURCE_RULES_PATH = ' + buildOpts.codeSignResourceRules + '\n'; } - if (args.provisioningProfile) { - extraConfig += 'PROVISIONING_PROFILE = ' + args.provisioningProfile + '\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 = args.release ? 'Release' : 'Debug'; + 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', 'device'); - console.log('Building project : ' + path.join(projectPath, projectName + '.xcodeproj')); - console.log('\tConfiguration : ' + configuration); - console.log('\tPlatform : ' + (args.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); + return spawn('xcodebuild', xcodebuildArgs, projectPath); + }); - var xcodebuildArgs = getXcodeArgs(projectName, projectPath, configuration, args.device); - return spawn('xcodebuild', xcodebuildArgs, projectPath); }).then(function () { - if (!args.device) { + if (!buildOpts.device || buildOpts.noSign) { return; } + + var exportOptions = {'compileBitcode': false, 'method': 'development'}; + + if (buildOpts.packageType) { + exportOptions.method = buildOpts.packageType; + } + + if (buildOpts.developmentTeam) { + exportOptions.teamID = buildOpts.developmentTeam; + } + + var exportOptionsPlist = plist.build(exportOptions); + var exportOptionsPath = path.join(projectPath, 'exportOptions.plist'); + var buildOutputDir = path.join(projectPath, 'build', 'device'); - var pathToApp = path.join(buildOutputDir, projectName + '.app'); - var pathToIpa = path.join(buildOutputDir, projectName + '.ipa'); - var xcRunArgs = ['-sdk', 'iphoneos', 'PackageApplication', - '-v', pathToApp, - '-o', pathToIpa]; - if (args.codeSignIdentity) { - xcRunArgs.concat('--sign', args.codeSignIdentity); + + + 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); + return spawn('xcodebuild', xcodearchiveArgs, projectPath); + } + + function unpackIPA() { + 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); } - if (args.provisioningProfile) { - xcRunArgs.concat('--embed', args.provisioningProfile); + + function moveApp() { + 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); + }); } - return spawn('xcrun', xcRunArgs, projectPath); + + return Q.nfcall(fs.writeFile, exportOptionsPath, exportOptionsPlist, 'utf-8') + .then(checkSystemRuby) + .then(packageArchive) + .then(unpackIPA) + .then(moveApp); }); }; @@ -125,12 +197,12 @@ function findXCodeProjectIn(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) { - console.warn('Found multiple .xcodeproj directories in \n' + + events.emit('warn','Found multiple .xcodeproj directories in \n' + projectPath + '\nUsing first one'); } @@ -148,38 +220,111 @@ module.exports.findXCodeProjectIn = findXCodeProjectIn; * @param {Boolean} isDevice Flag that specify target for package (device/emulator) * @return {Array} Array of arguments that could be passed directly to spawn method */ -function getXcodeArgs(projectName, projectPath, configuration, isDevice) { +function getXcodeBuildArgs(projectName, projectPath, configuration, isDevice, buildFlags) { 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) { - xcodebuildArgs = [ - '-xcconfig', path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'), - '-project', projectName + '.xcodeproj', - 'ARCHS=armv7 armv7s arm64', - '-target', projectName, - '-configuration', configuration, - '-sdk', 'iphoneos', - 'build', - 'VALID_ARCHS=armv7 armv7s arm64', - 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'device'), - 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch') + 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 - xcodebuildArgs = [ - '-xcconfig', path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'), - '-project', projectName + '.xcodeproj', - 'ARCHS=i386', - '-target', projectName , - '-configuration', configuration, - '-sdk', 'iphonesimulator', - 'build', - 'VALID_ARCHS=i386', - 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'emulator'), - 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch') + 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=iPhone 5s' + ]; + 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 + * @return {Array} Array of arguments that could be passed directly to spawn method + */ +function getXcodeArchiveArgs(projectName, projectPath, outputPath, exportOptionsPath) { + return [ + '-exportArchive', + '-archivePath', projectName + '.xcarchive', + '-exportOptionsPlist', exportOptionsPath, + '-exportPath', outputPath + ]; +} + +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. + 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(''); @@ -198,6 +343,7 @@ module.exports.help = function help() { console.log(' --codeSignIdentity : Type of signing identity used for code signing.'); console.log(' --codeSignResourceRules : Path to ResourceRules.plist.'); console.log(' --provisioningProfile : UUID of the profile.'); + console.log(' --device --noSign : Builds project without application signing.'); console.log(''); console.log('examples:'); console.log(' build '); diff --git a/StoneIsland/platforms/ios/cordova/lib/check_reqs.js b/StoneIsland/platforms/ios/cordova/lib/check_reqs.js index d1f6333c..ae21f989 100755 --- a/StoneIsland/platforms/ios/cordova/lib/check_reqs.js +++ b/StoneIsland/platforms/ios/cordova/lib/check_reqs.js @@ -19,22 +19,28 @@ var Q = require('q'), shell = require('shelljs'), + util = require('util'), versions = require('./versions'); -var XCODEBUILD_MIN_VERSION = '4.6.0'; +var XCODEBUILD_MIN_VERSION = '7.0.0'; var XCODEBUILD_NOT_FOUND_MESSAGE = 'Please install version ' + XCODEBUILD_MIN_VERSION + ' or greater from App Store'; -var IOS_SIM_MIN_VERSION = '3.0.0'; -var IOS_SIM_NOT_FOUND_MESSAGE = - 'Please download, build and install version ' + IOS_SIM_MIN_VERSION + ' or greater' + - ' from https://github.com/phonegap/ios-sim into your path, or do \'npm install -g ios-sim\''; - -var IOS_DEPLOY_MIN_VERSION = '1.4.0'; +var IOS_DEPLOY_MIN_VERSION = '1.9.0'; var 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\''; +var COCOAPODS_MIN_VERSION = '1.0.1'; +var COCOAPODS_NOT_FOUND_MESSAGE = + 'Please install version ' + COCOAPODS_MIN_VERSION + ' or greater from https://cocoapods.org/'; +var 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.'; +var COCOAPODS_SYNCED_MIN_SIZE = 475; // in megabytes +var 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.'; +var 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 @@ -51,14 +57,6 @@ module.exports.check_ios_deploy = function () { return checkTool('ios-deploy', IOS_DEPLOY_MIN_VERSION, IOS_DEPLOY_NOT_FOUND_MESSAGE); }; -/** - * Checks if ios-sim util is available - * @return {Promise} Returns a promise either resolved with ios-sim version or rejected - */ -module.exports.check_ios_sim = function () { - return checkTool('ios-sim', IOS_SIM_MIN_VERSION, IOS_SIM_NOT_FOUND_MESSAGE); -}; - module.exports.check_os = function () { // Build iOS apps available for OSX platform only, so we reject on others platforms return process.platform === 'darwin' ? @@ -66,29 +64,81 @@ module.exports.check_os = function () { Q.reject('Cordova tooling for iOS requires Apple OS X'); }; -module.exports.help = function () { - console.log('Usage: check_reqs or node check_reqs'); +function check_cocoapod_tool() { + return checkTool('pod', COCOAPODS_MIN_VERSION, COCOAPODS_NOT_FOUND_MESSAGE, 'CocoaPods'); +} + +/** + * 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() { + // check size of ~/.cocoapods repo + var commandString = util.format('du -sh %s/.cocoapods', process.env.HOME); + var command = shell.exec(commandString, { silent:true }); + if (command.code !== 0) { // error, perhaps not found + return Q.reject(util.format('%s (%s)', COCOAPODS_REPO_NOT_FOUND_MESSAGE, command.output)); + } else { // success, parse output + // command.output is e.g "750M path/to/.cocoapods", we just scan the number + return Q.resolve(parseFloat(command.output)); + } + }) + .then(function(repoSize) { + if (COCOAPODS_SYNCED_MIN_SIZE > repoSize) { + return Q.reject(COCOAPODS_SYNC_ERROR_MESSAGE); + } else { + return Q.resolve(); + } + }); +}; + +/** + * 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 () { + return check_cocoapod_tool() + // 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() { + var code = shell.exec('pod repo | grep -e "^0 repos"', { silent:true }).code; + return Q.resolve(code !== 0); // non-zero means it is synced (has 1 repo at least) + }) + .then(function(repoIsSynced) { + if (repoIsSynced) { + // return check_cocoapods_repo_size(); + // we could check the repo size above, but it takes too long. + return Q.resolve(); + } 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', 'ios-sim' and 'ios-deploy' + * @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) { +function checkTool (tool, minVersion, message, toolFriendlyName) { + toolFriendlyName = toolFriendlyName || tool; + // Check whether tool command is available at all var tool_command = shell.which(tool); if (!tool_command) { - return Q.reject(tool + ' was not found. ' + (message || '')); + 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) : - Q.reject('Cordova needs ' + tool + ' version ' + minVersion + + Q.reject('Cordova needs ' + toolFriendlyName + ' version ' + minVersion + ' or greater, you have version ' + version + '. ' + (message || '')); }); } @@ -120,7 +170,7 @@ module.exports.check_all = function() { new Requirement('os', 'Apple OS X', true), new Requirement('xcode', 'Xcode'), new Requirement('ios-deploy', 'ios-deploy'), - new Requirement('ios-sim', 'ios-sim') + new Requirement('CocoaPods', 'CocoaPods') ]; var result = []; @@ -130,7 +180,7 @@ module.exports.check_all = function() { module.exports.check_os, module.exports.check_xcodebuild, module.exports.check_ios_deploy, - module.exports.check_ios_sim + module.exports.check_cocoapods ]; // Then execute requirement checks one-by-one diff --git a/StoneIsland/platforms/ios/cordova/lib/clean.js b/StoneIsland/platforms/ios/cordova/lib/clean.js index 6d955cf1..7c8cf56e 100755 --- a/StoneIsland/platforms/ios/cordova/lib/clean.js +++ b/StoneIsland/platforms/ios/cordova/lib/clean.js @@ -22,8 +22,7 @@ var Q = require('q'), path = require('path'), shell = require('shelljs'), - spawn = require('./spawn'), - check_reqs = require('./check_reqs'); + spawn = require('./spawn'); var projectPath = path.join(__dirname, '..', '..'); @@ -36,9 +35,8 @@ module.exports.run = function() { return Q.reject('No Xcode project found in ' + projectPath); } - return check_reqs.run().then(function() { - return spawn('xcodebuild', ['-project', projectName, '-configuration', 'Debug', '-alltargets', 'clean'], projectPath); - }).then(function () { + 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/StoneIsland/platforms/ios/cordova/lib/copy-www-build-step.js b/StoneIsland/platforms/ios/cordova/lib/copy-www-build-step.js index 7a81b93e..7caa200f 100755 --- a/StoneIsland/platforms/ios/cordova/lib/copy-www-build-step.js +++ b/StoneIsland/platforms/ios/cordova/lib/copy-www-build-step.js @@ -32,7 +32,6 @@ var BUILT_PRODUCTS_DIR = process.env.BUILT_PRODUCTS_DIR, var path = require('path'), fs = require('fs'), shell = require('shelljs'), - glob = require('glob'), srcDir = 'www', dstDir = path.join(BUILT_PRODUCTS_DIR, FULL_PRODUCT_NAME), dstWwwDir = path.join(dstDir, 'www'); @@ -46,7 +45,7 @@ try { fs.statSync(srcDir); } catch (e) { console.error('Path does not exist: ' + srcDir); - process.exit(1); + process.exit(2); } // Code signing files must be removed or else there are @@ -57,11 +56,16 @@ shell.rm('-rf', path.join(dstDir, 'PkgInfo')); shell.rm('-rf', path.join(dstDir, 'embedded.mobileprovision')); // Copy www dir recursively +var code; if(!!COPY_HIDDEN) { - shell.mkdir('-p', dstWwwDir); - shell.cp('-r', glob.sync(srcDir + '/**', { dot: true }), dstWwwDir); + code = shell.exec('rsync -Lra "' + srcDir + '" "' + dstDir + '"').code; } else { - shell.cp('-r', srcDir, dstDir); + 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. diff --git a/StoneIsland/platforms/ios/cordova/lib/list-emulator-images b/StoneIsland/platforms/ios/cordova/lib/list-emulator-images index 07dd1a48..87a5ad27 100755 --- a/StoneIsland/platforms/ios/cordova/lib/list-emulator-images +++ b/StoneIsland/platforms/ios/cordova/lib/list-emulator-images @@ -22,6 +22,7 @@ /*jshint node: true*/ var Q = require('q'), + iossim = require('ios-sim'), exec = require('child_process').exec, check_reqs = require('./check_reqs'); @@ -30,15 +31,10 @@ var Q = require('q'), * @return {Promise} Promise fulfilled with list of devices available for simulation */ function listEmulatorImages () { - return check_reqs.check_ios_sim().then(function () { - return Q.nfcall(exec, 'ios-sim showdevicetypes 2>&1 | ' + - 'sed "s/com.apple.CoreSimulator.SimDeviceType.//g"'); - }).then(function (stdio) { - // Exec promise resolves with array [stout, stderr], and we need stdout only - return stdio[0].trim().split('\n'); - }); + return Q.resolve(iossim.getdevicetypes()); } + exports.run = listEmulatorImages; // Check if module is started as separate script. diff --git a/StoneIsland/platforms/ios/cordova/lib/plugman/pluginHandlers.js b/StoneIsland/platforms/ios/cordova/lib/plugman/pluginHandlers.js new file mode 100755 index 00000000..297e3863 --- /dev/null +++ b/StoneIsland/platforms/ios/cordova/lib/plugman/pluginHandlers.js @@ -0,0 +1,375 @@ +/* + 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 shell = require('shelljs'); +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, + srcFile = path.resolve(plugin.dir, src), + destFile = path.resolve(project.resources_dir, path.basename(src)); + 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 detination "' + destFile + '" for resource file specified by plugin ' + plugin.id + ' in iOS platform'); + project.xcode.addResourceFile(path.join('Resources', path.basename(src))); + var link = !!(options && options.link); + copyFile(plugin.dir, src, project.projectDir, destFile, link); + }, + uninstall:function(obj, plugin, project, options) { + var src = obj.src, + destFile = path.resolve(project.resources_dir, path.basename(src)); + project.xcode.removeResourceFile(path.join('Resources', path.basename(src))); + shell.rm('-rf', destFile); + } + }, + 'framework':{ // CB-5238 custom frameworks only + install:function(obj, plugin, project, options) { + var src = obj.src, + custom = obj.custom; + 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]++; + project.xcode.addFramework(src, {weak: obj.weak}); + } + } + return; + } + var srcFile = path.resolve(plugin.dir, src), + 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 link = !!(options && options.link); + copyFile(plugin.dir, src, project.projectDir, targetDir, link); // frameworks are directories + // CB-10773 translate back slashes to forward on win32 + var project_relative = fixPathSep(path.relative(project.projectDir, targetDir)); + var pbxFile = project.xcode.addFramework(project_relative, {customFramework: true}); + if (pbxFile) { + project.xcode.addToPbxEmbedFrameworksBuildPhase(pbxFile); + } + }, + 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))), + 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) { + symlinkFileOrDirTree(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 symlinkFileOrDirTree(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) { + symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry)); + }); + } + else { + fs.symlinkSync(path.relative(fs.realpathSync(path.dirname(dest)), 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/StoneIsland/platforms/ios/cordova/lib/prepare.js b/StoneIsland/platforms/ios/cordova/lib/prepare.js new file mode 100755 index 00000000..8d1cda94 --- /dev/null +++ b/StoneIsland/platforms/ios/cordova/lib/prepare.js @@ -0,0 +1,1003 @@ +/** + 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 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; + +/*jshint sub:true*/ + +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); + }) + .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); + }); +}; + +/** + * 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 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'); + } + + // 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).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) { + var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios')); + var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios'); + + // no build settings provided, we don't need to parse and update .pbxproj file + if (!targetDevice && !deploymentTarget) { + return Q(); + } + + var proj = new xcode.project(locations.pbxproj); + + 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); + } + + 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-60@2x.png', width: 120, height: 120}, + {dest: 'icon-60@3x.png', width: 180, height: 180}, + {dest: 'icon-76.png', width: 76, height: 76}, + {dest: 'icon-76@2x.png', width: 152, height: 152}, + {dest: 'icon-small.png', width: 29, height: 29}, + {dest: 'icon-small@2x.png', width: 58, height: 58}, + {dest: 'icon-40.png', width: 40, height: 40}, + {dest: 'icon-40@2x.png', width: 80, height: 80}, + {dest: 'icon-small@3x.png', width: 87, height: 87}, + {dest: 'icon.png', width: 57, height: 57}, + {dest: 'icon@2x.png', width: 114, height: 114}, + {dest: 'icon-72.png', width: 72, height: 72}, + {dest: 'icon-72@2x.png', width: 144, height: 144}, + {dest: 'icon-50.png', width: 50, height: 50}, + {dest: 'icon-50@2x.png', width: 100, height: 100}, + {dest: 'icon-83.5@2x.png', width: 167, height: 167} + ]; + + 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} + ]; + + 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); + } +} + +/** + * 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 IMAGESET_COMPACT_SIZE_CLASS = 'compact'; + var CDV_ANY_SIZE_CLASS = 'any'; + + 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; +} + +/** + * 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 UI_LAUNCH_STORYBOARD_NAME = 'UILaunchStoryboardName'; + var CDV_LAUNCH_STORYBOARD_NAME = 'CDVLaunchScreen'; + + var splashScreens = platformConfig.getSplashScreens('ios'); + var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, ''); // note: we don't need a file path here; we're just counting + var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME]; + + events.emit('verbose', 'Current launch storyboard ' + currentLaunchStoryboard); + + + /* 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: + * var hasLaunchStoryboardImages = !!contentsJSON.images.find(function (item) { + * return item.filename !== undefined; + * }); + */ + var hasLaunchStoryboardImages = !!contentsJSON.images.reduce(function (p, c) { + return (c.filename !== undefined) ? c : p; + }, undefined); + + 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 project 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 project to use launch images'); + infoPlist[UI_LAUNCH_STORYBOARD_NAME] = undefined; + return; + } + events.emit('verbose', 'Not changing launch storyboard setting.'); +} + +/** + * 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 = 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(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 = 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(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) + NSAllowsArbitraryLoadsInMedia, // 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_in_media : currentElement.allows_arbitrary_loads_in_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) + NSAllowsArbitraryLoadsInMedia, // 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_in_media === 'true'); + if (options.allows_arbitrary_loads_in_media && val) { // default is false + retObj.NSAllowsArbitraryLoadsInMedia = 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.NSAllowsArbitraryLoadsInMedia) { + ats['NSAllowsArbitraryLoadsInMedia'] = 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/StoneIsland/platforms/ios/cordova/lib/projectFile.js b/StoneIsland/platforms/ios/cordova/lib/projectFile.js new file mode 100755 index 00000000..aab38639 --- /dev/null +++ b/StoneIsland/platforms/ios/cordova/lib/projectFile.js @@ -0,0 +1,136 @@ +/* + 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. +*/ + +/*jshint node: true*/ + +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/StoneIsland/platforms/ios/cordova/lib/run.js b/StoneIsland/platforms/ios/cordova/lib/run.js index fcd39015..68c315a5 100755 --- a/StoneIsland/platforms/ios/cordova/lib/run.js +++ b/StoneIsland/platforms/ios/cordova/lib/run.js @@ -20,84 +20,103 @@ /*jshint node: true*/ var Q = require('q'), - nopt = require('nopt'), - path = require('path'), - build = require('./build'), - spawn = require('./spawn'), + path = require('path'), + iossim = require('ios-sim'), + build = require('./build'), + spawn = require('./spawn'), 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 (argv) { - - // parse args here - // --debug and --release args not parsed here - // but still valid since they can be passed down to build command - var args = nopt({ - // "archs": String, // TODO: add support for building different archs - 'list': Boolean, - 'nobuild': Boolean, - 'device': Boolean, 'emulator': Boolean, 'target': String - }, {}, argv); +module.exports.run = function (runOptions) { // Validate args - if (args.device && args.emulator) { + if (runOptions.device && runOptions.emulator) { return Q.reject('Only one of "device"/"emulator" options should be specified'); } - // validate target device for ios-sim - // Valid values for "--target" (case sensitive): - var validTargets = ['iPhone-4s', 'iPhone-5', 'iPhone-5s', 'iPhone-6-Plus', 'iPhone-6', - 'iPad-2', 'iPad-Retina', 'iPad-Air', 'Resizable-iPhone', 'Resizable-iPad']; - if (!(args.device) && args.target && validTargets.indexOf(args.target.split(',')[0]) < 0 ) { - return Q.reject(args.target + ' is not a valid target for emulator'); - } - // support for CB-8168 `cordova/run --list` - if (args.list) { - if (args.device) return listDevices(); - if (args.emulator) return listEmulators(); + if (runOptions.list) { + if (runOptions.device) return listDevices(); + if (runOptions.emulator) return listEmulators(); // if no --device or --emulator flag is specified, list both devices and emulators return listDevices().then(function () { return listEmulators(); }); } - // check for either ios-sim or ios-deploy is available - // depending on arguments provided - var checkTools = args.device ? check_reqs.check_ios_deploy() : check_reqs.check_ios_sim(); + var useDevice = !!runOptions.device; - return checkTools.then(function () { - // if --nobuild isn't specified then build app first - if (!args.nobuild) { - return build.run(argv); + 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', (args.device ? 'device' : 'emulator'), projectName + '.app'); + var appPath = path.join(projectPath, 'build', 'emulator', projectName + '.app'); // select command to run and arguments depending whether // we're running on device/emulator - if (args.device) { + if (useDevice) { return checkDeviceConnected().then(function () { - return deployToDevice(appPath); + 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 = filterSupportedArgs(runOptions.argv.slice(2)); + } + return deployToDevice(appPath, runOptions.target, extraArgs); }, function () { // if device connection check failed use emulator then - return deployToSim(appPath, args.target); + return deployToSim(appPath, runOptions.target); }); } else { - return deployToSim(appPath, args.target); + return deployToSim(appPath, runOptions.target); } }); }; /** + * 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']); + return spawn('ios-deploy', ['-c', '-t', '1']); } /** @@ -106,9 +125,13 @@ function checkDeviceConnected() { * @param {String} appPath Path to application package * @return {Promise} Resolves when deploy succeeds otherwise rejects */ -function deployToDevice(appPath) { +function deployToDevice(appPath, target, extraArgs) { // Deploying to device... - return spawn('ios-deploy', ['-d', '-b', appPath]); + 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)); + } } /** @@ -118,27 +141,38 @@ function deployToDevice(appPath) { * @return {Promise} Resolves when deploy succeeds otherwise rejects */ function deployToSim(appPath, target) { - // Select target device for emulator. Default is 'iPhone-6' + // Select target device for emulator. Default is 'iPhone-6' if (!target) { - target = 'iPhone-6'; - console.log('No target specified for emulator. Deploying to ' + target + ' simulator'); + 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'); - var simArgs = ['launch', appPath, - '--devicetypeid', 'com.apple.CoreSimulator.SimDeviceType.' + target, - // We need to redirect simulator output here to use cordova/log command - // TODO: Is there any other way to get emulator's output to use in log command? - '--stderr', logPath, '--stdout', logPath, - '--exit']; - return spawn('ios-sim', simArgs); + + return iossim.launch(appPath, 'com.apple.CoreSimulator.SimDeviceType.' + target, logPath, '--exit'); } function listDevices() { return require('./list-devices').run() .then(function (devices) { - console.log('Available iOS Devices:'); + events.emit('log','Available iOS Devices:'); devices.forEach(function (device) { - console.log('\t' + device); + events.emit('log','\t' + device); }); }); } @@ -146,9 +180,9 @@ function listDevices() { function listEmulators() { return require('./list-emulator-images').run() .then(function (emulators) { - console.log('Available iOS Virtual Devices:'); + events.emit('log','Available iOS Simulators:'); emulators.forEach(function (emulator) { - console.log('\t' + emulator); + events.emit('log','\t' + emulator); }); }); } diff --git a/StoneIsland/platforms/ios/cordova/lib/spawn.js b/StoneIsland/platforms/ios/cordova/lib/spawn.js index 1cb31615..2162b9c7 100755 --- a/StoneIsland/platforms/ios/cordova/lib/spawn.js +++ b/StoneIsland/platforms/ios/cordova/lib/spawn.js @@ -43,7 +43,6 @@ module.exports = function(cmd, args, opt_cwd) { } }); } catch(e) { - console.error('error caught: ' + e); d.reject(e); } return d.promise; diff --git a/StoneIsland/platforms/ios/cordova/lib/versions.js b/StoneIsland/platforms/ios/cordova/lib/versions.js index e22e499a..da31d4fa 100755 --- a/StoneIsland/platforms/ios/cordova/lib/versions.js +++ b/StoneIsland/platforms/ios/cordova/lib/versions.js @@ -111,6 +111,23 @@ exports.get_ios_deploy_version = function() { }; /** + * 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 @@ -138,7 +155,8 @@ exports.get_tool_version = function (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(); - default: return Q.reject(toolName + ' is not valid tool name. Valid names are: \'xcodebuild\', \'ios-sim\' and \'ios-deploy\''); + 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\''); } }; |
