/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var Q = require('q'); var path = require('path'); var shell = require('shelljs'); var superspawn = require('cordova-common').superspawn; var fs = require('fs'); var plist = require('plist'); var util = require('util'); var check_reqs = require('./check_reqs'); var projectFile = require('./projectFile'); var events = require('cordova-common').events; // These are regular expressions to detect if the user is changing any of the built-in xcodebuildArgs /* eslint-disable no-useless-escape */ var buildFlagMatchers = { 'workspace': /^\-workspace\s*(.*)/, 'scheme': /^\-scheme\s*(.*)/, 'configuration': /^\-configuration\s*(.*)/, 'sdk': /^\-sdk\s*(.*)/, 'destination': /^\-destination\s*(.*)/, 'archivePath': /^\-archivePath\s*(.*)/, 'configuration_build_dir': /^(CONFIGURATION_BUILD_DIR=.*)/, 'shared_precomps_dir': /^(SHARED_PRECOMPS_DIR=.*)/ }; /* eslint-enable no-useless-escape */ /** * Creates a project object (see projectFile.js/parseProjectFile) from * a project path and name * * @param {*} projectPath * @param {*} projectName */ function createProjectObject (projectPath, projectName) { var locations = { root: projectPath, pbxproj: path.join(projectPath, projectName + '.xcodeproj', 'project.pbxproj') }; return projectFile.parse(locations); } /** * Gets the resolved bundle identifier from a project. * Resolves the variable set in INFO.plist, if any (simple case) * * @param {*} projectObject */ function getBundleIdentifier (projectObject) { var packageName = projectObject.getPackageName(); var bundleIdentifier = packageName; var variables = packageName.match(/\$\((\w+)\)/); // match $(VARIABLE), if any if (variables && variables.length >= 2) { bundleIdentifier = projectObject.xcode.getBuildProperty(variables[1]); } return bundleIdentifier; } /** * Returns a promise that resolves to the default simulator target; the logic here * matches what `cordova emulate ios` does. * * The return object has two properties: `name` (the Xcode destination name), * `identifier` (the simctl identifier), and `simIdentifier` (essentially the cordova emulate target) * * @return {Promise} */ function getDefaultSimulatorTarget () { return require('./list-emulator-build-targets').run() .then(function (emulators) { var targetEmulator; if (emulators.length > 0) { targetEmulator = emulators[0]; } emulators.forEach(function (emulator) { if (emulator.name.indexOf('iPhone') === 0) { targetEmulator = emulator; } }); return targetEmulator; }); } module.exports.run = function (buildOpts) { var emulatorTarget = ''; var projectPath = path.join(__dirname, '..', '..'); var projectName = ''; buildOpts = buildOpts || {}; if (buildOpts.debug && buildOpts.release) { return Q.reject('Cannot specify "debug" and "release" options together.'); } if (buildOpts.device && buildOpts.emulator) { return Q.reject('Cannot specify "device" and "emulator" options together.'); } if (buildOpts.buildConfig) { if (!fs.existsSync(buildOpts.buildConfig)) { return Q.reject('Build config file does not exist: ' + buildOpts.buildConfig); } events.emit('log', 'Reading build config file: ' + path.resolve(buildOpts.buildConfig)); var contents = fs.readFileSync(buildOpts.buildConfig, 'utf-8'); var buildConfig = JSON.parse(contents.replace(/^\ufeff/, '')); // Remove BOM if (buildConfig.ios) { var buildType = buildOpts.release ? 'release' : 'debug'; var config = buildConfig.ios[buildType]; if (config) { ['codeSignIdentity', 'codeSignResourceRules', 'provisioningProfile', 'developmentTeam', 'packageType', 'buildFlag', 'iCloudContainerEnvironment', 'automaticProvisioning'].forEach( function (key) { buildOpts[key] = buildOpts[key] || config[key]; }); } } } return require('./list-devices').run() .then(function (devices) { if (devices.length > 0 && !(buildOpts.emulator)) { // we also explicitly set device flag in options as we pass // those parameters to other api (build as an example) buildOpts.device = true; return check_reqs.check_ios_deploy(); } }).then(function () { // CB-12287: Determine the device we should target when building for a simulator if (!buildOpts.device) { var newTarget = buildOpts.target || ''; if (newTarget) { // only grab the device name, not the runtime specifier newTarget = newTarget.split(',')[0]; } // a target was given to us, find the matching Xcode destination name var promise = require('./list-emulator-build-targets').targetForSimIdentifier(newTarget); return promise.then(function (theTarget) { if (!theTarget) { return getDefaultSimulatorTarget().then(function (defaultTarget) { emulatorTarget = defaultTarget.name; events.emit('warn', `No simulator found for "${newTarget}. Falling back to the default target.`); events.emit('log', `Building for "${emulatorTarget}" Simulator (${defaultTarget.identifier}, ${defaultTarget.simIdentifier}).`); return emulatorTarget; }); } else { emulatorTarget = theTarget.name; events.emit('log', `Building for "${emulatorTarget}" Simulator (${theTarget.identifier}, ${theTarget.simIdentifier}).`); return emulatorTarget; } }); } }).then(function () { return check_reqs.run(); }).then(function () { return findXCodeProjectIn(projectPath); }).then(function (name) { projectName = name; var extraConfig = ''; if (buildOpts.codeSignIdentity) { extraConfig += 'CODE_SIGN_IDENTITY = ' + buildOpts.codeSignIdentity + '\n'; extraConfig += 'CODE_SIGN_IDENTITY[sdk=iphoneos*] = ' + buildOpts.codeSignIdentity + '\n'; } if (buildOpts.codeSignResourceRules) { extraConfig += 'CODE_SIGN_RESOURCE_RULES_PATH = ' + buildOpts.codeSignResourceRules + '\n'; } if (buildOpts.provisioningProfile) { extraConfig += 'PROVISIONING_PROFILE = ' + buildOpts.provisioningProfile + '\n'; } if (buildOpts.developmentTeam) { extraConfig += 'DEVELOPMENT_TEAM = ' + buildOpts.developmentTeam + '\n'; } function writeCodeSignStyle (value) { var project = createProjectObject(projectPath, projectName); events.emit('verbose', `Set CODE_SIGN_STYLE Build Property to ${value}.`); project.xcode.updateBuildProperty('CODE_SIGN_STYLE', value); events.emit('verbose', `Set ProvisioningStyle Target Attribute to ${value}.`); project.xcode.addTargetAttribute('ProvisioningStyle', value); project.write(); } if (buildOpts.provisioningProfile) { events.emit('verbose', 'ProvisioningProfile build option set, changing project settings to Manual.'); writeCodeSignStyle('Manual'); } else if (buildOpts.automaticProvisioning) { events.emit('verbose', 'ProvisioningProfile build option NOT set, changing project settings to Automatic.'); writeCodeSignStyle('Automatic'); } return Q.nfcall(fs.writeFile, path.join(__dirname, '..', 'build-extras.xcconfig'), extraConfig, 'utf-8'); }).then(function () { var configuration = buildOpts.release ? 'Release' : 'Debug'; events.emit('log', 'Building project: ' + path.join(projectPath, projectName + '.xcworkspace')); events.emit('log', '\tConfiguration: ' + configuration); events.emit('log', '\tPlatform: ' + (buildOpts.device ? 'device' : 'emulator')); events.emit('log', '\tTarget: ' + emulatorTarget); var buildOutputDir = path.join(projectPath, 'build', (buildOpts.device ? 'device' : 'emulator')); // remove the build/device folder before building shell.rm('-rf', buildOutputDir); var xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, buildOpts.device, buildOpts.buildFlag, emulatorTarget, buildOpts.automaticProvisioning); return superspawn.spawn('xcodebuild', xcodebuildArgs, { cwd: projectPath, printCommand: true, stdio: 'inherit' }); }).then(function () { if (!buildOpts.device || buildOpts.noSign) { return; } var project = createProjectObject(projectPath, projectName); var bundleIdentifier = getBundleIdentifier(project); var exportOptions = { 'compileBitcode': false, 'method': 'development' }; if (buildOpts.packageType) { exportOptions.method = buildOpts.packageType; } if (buildOpts.iCloudContainerEnvironment) { exportOptions.iCloudContainerEnvironment = buildOpts.iCloudContainerEnvironment; } if (buildOpts.developmentTeam) { exportOptions.teamID = buildOpts.developmentTeam; } if (buildOpts.provisioningProfile && bundleIdentifier) { exportOptions.provisioningProfiles = { [ bundleIdentifier ]: String(buildOpts.provisioningProfile) }; exportOptions.signingStyle = 'manual'; } if (buildOpts.codeSignIdentity) { exportOptions.signingCertificate = buildOpts.codeSignIdentity; } var exportOptionsPlist = plist.build(exportOptions); var exportOptionsPath = path.join(projectPath, 'exportOptions.plist'); var buildOutputDir = path.join(projectPath, 'build', 'device'); function checkSystemRuby () { var ruby_cmd = shell.which('ruby'); if (ruby_cmd !== '/usr/bin/ruby') { events.emit('warn', 'Non-system Ruby in use. This may cause packaging to fail.\n' + 'If you use RVM, please run `rvm use system`.\n' + 'If you use chruby, please run `chruby system`.'); } } function packageArchive () { var xcodearchiveArgs = getXcodeArchiveArgs(projectName, projectPath, buildOutputDir, exportOptionsPath, buildOpts.automaticProvisioning); return superspawn.spawn('xcodebuild', xcodearchiveArgs, { cwd: projectPath, printCommand: true, stdio: 'inherit' }); } return Q.nfcall(fs.writeFile, exportOptionsPath, exportOptionsPlist, 'utf-8') .then(checkSystemRuby) .then(packageArchive); }); }; /** * Searches for first XCode project in specified folder * @param {String} projectPath Path where to search project * @return {Promise} Promise either fulfilled with project name or rejected */ function findXCodeProjectIn (projectPath) { // 'Searching for Xcode project in ' + projectPath); var xcodeProjFiles = shell.ls(projectPath).filter(function (name) { return path.extname(name) === '.xcodeproj'; }); if (xcodeProjFiles.length === 0) { return Q.reject('No Xcode project found in ' + projectPath); } if (xcodeProjFiles.length > 1) { events.emit('warn', 'Found multiple .xcodeproj directories in \n' + projectPath + '\nUsing first one'); } var projectName = path.basename(xcodeProjFiles[0], '.xcodeproj'); return Q.resolve(projectName); } module.exports.findXCodeProjectIn = findXCodeProjectIn; /** * Returns array of arguments for xcodebuild * @param {String} projectName Name of xcode project * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild * @param {String} configuration Configuration name: debug|release * @param {Boolean} isDevice Flag that specify target for package (device/emulator) * @param {Array} buildFlags * @param {String} emulatorTarget Target for emulator (rather than default) * @param {Boolean} autoProvisioning Whether to allow Xcode to automatically update provisioning * @return {Array} Array of arguments that could be passed directly to spawn method */ function getXcodeBuildArgs (projectName, projectPath, configuration, isDevice, buildFlags, emulatorTarget, autoProvisioning) { var xcodebuildArgs; var options; var buildActions; var settings; var customArgs = {}; customArgs.otherFlags = []; if (buildFlags) { if (typeof buildFlags === 'string' || buildFlags instanceof String) { parseBuildFlag(buildFlags, customArgs); } else { // buildFlags is an Array of strings buildFlags.forEach(function (flag) { parseBuildFlag(flag, customArgs); }); } } if (isDevice) { options = [ '-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]); } if (autoProvisioning) { options = options.concat(['-allowProvisioningUpdates']); } } else { // emulator options = [ '-workspace', customArgs.project || projectName + '.xcworkspace', '-scheme', customArgs.scheme || projectName, '-configuration', customArgs.configuration || configuration, '-sdk', customArgs.sdk || 'iphonesimulator', '-destination', customArgs.destination || 'platform=iOS Simulator,name=' + emulatorTarget ]; buildActions = [ 'build' ]; settings = [ customArgs.configuration_build_dir || 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'emulator'), customArgs.shared_precomps_dir || 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch') ]; // Add other matched flags to otherFlags to let xcodebuild present an appropriate error. // This is preferable to just ignoring the flags that the user has passed in. if (customArgs.archivePath) { customArgs.otherFlags = customArgs.otherFlags.concat(['-archivePath', customArgs.archivePath]); } } xcodebuildArgs = options.concat(buildActions).concat(settings).concat(customArgs.otherFlags); return xcodebuildArgs; } /** * Returns array of arguments for xcodebuild * @param {String} projectName Name of xcode project * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild * @param {String} outputPath Output directory to contain the IPA * @param {String} exportOptionsPath Path to the exportOptions.plist file * @param {Boolean} autoProvisioning Whether to allow Xcode to automatically update provisioning * @return {Array} Array of arguments that could be passed directly to spawn method */ function getXcodeArchiveArgs (projectName, projectPath, outputPath, exportOptionsPath, autoProvisioning) { return [ '-exportArchive', '-archivePath', projectName + '.xcarchive', '-exportOptionsPlist', exportOptionsPath, '-exportPath', outputPath ].concat(autoProvisioning ? ['-allowProvisioningUpdates'] : []); } function parseBuildFlag (buildFlag, args) { var matched; for (var key in buildFlagMatchers) { var found = buildFlag.match(buildFlagMatchers[key]); if (found) { matched = true; // found[0] is the whole match, found[1] is the first match in parentheses. args[key] = found[1]; events.emit('warn', util.format('Overriding xcodebuildArg: %s', buildFlag)); } } if (!matched) { // If the flag starts with a '-' then it is an xcodebuild built-in option or a // user-defined setting. The regex makes sure that we don't split a user-defined // setting that is wrapped in quotes. /* eslint-disable no-useless-escape */ if (buildFlag[0] === '-' && !buildFlag.match(/^.*=(\".*\")|(\'.*\')$/)) { args.otherFlags = args.otherFlags.concat(buildFlag.split(' ')); events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag.split(' '))); } else { args.otherFlags.push(buildFlag); events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag)); } } } // help/usage function module.exports.help = function help () { console.log(''); console.log('Usage: build [--debug | --release] [--archs=\"\"]'); console.log(' [--device | --simulator] [--codeSignIdentity=\"\"]'); console.log(' [--codeSignResourceRules=\"\"]'); console.log(' [--developmentTeam=\"\"]'); console.log(' [--provisioningProfile=\"\"]'); console.log(' --help : Displays this dialog.'); console.log(' --debug : Builds project in debug mode. (Default)'); console.log(' --release : Builds project in release mode.'); console.log(' -r : Shortcut :: builds project in release mode.'); /* eslint-enable no-useless-escape */ // TODO: add support for building different archs // console.log(" --archs : Builds project binaries for specific chip architectures (`anycpu`, `arm`, `x86`, `x64`)."); console.log(' --device, --simulator'); console.log(' : Specifies, what type of project to build'); console.log(' --codeSignIdentity : Type of signing identity used for code signing.'); console.log(' --codeSignResourceRules : Path to ResourceRules.plist.'); console.log(' --developmentTeam : New for Xcode 8. The development team (Team ID)'); console.log(' to use for code signing.'); console.log(' --provisioningProfile : UUID of the profile.'); console.log(' --device --noSign : Builds project without application signing.'); console.log(''); console.log('examples:'); console.log(' build '); console.log(' build --debug'); console.log(' build --release'); console.log(' build --codeSignIdentity="iPhone Distribution" --provisioningProfile="926c2bd6-8de9-4c2f-8407-1016d2d12954"'); // TODO: add support for building different archs // console.log(" build --release --archs=\"armv7\""); console.log(''); process.exit(0); };