UNPKG

19.7 kBJavaScriptView Raw
1/**
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20const path = require('path');
21const which = require('which');
22const execa = require('execa');
23const { CordovaError, events } = require('cordova-common');
24const fs = require('fs-extra');
25const plist = require('plist');
26const util = require('util');
27
28const check_reqs = require('./check_reqs');
29const projectFile = require('./projectFile');
30
31const buildConfigProperties = [
32 'codeSignIdentity',
33 'provisioningProfile',
34 'developmentTeam',
35 'packageType',
36 'buildFlag',
37 'iCloudContainerEnvironment',
38 'automaticProvisioning',
39 'authenticationKeyPath',
40 'authenticationKeyID',
41 'authenticationKeyIssuerID'
42];
43
44// These are regular expressions to detect if the user is changing any of the built-in xcodebuildArgs
45/* eslint-disable no-useless-escape */
46const buildFlagMatchers = {
47 workspace: /^\-workspace\s*(.*)/,
48 scheme: /^\-scheme\s*(.*)/,
49 configuration: /^\-configuration\s*(.*)/,
50 sdk: /^\-sdk\s*(.*)/,
51 destination: /^\-destination\s*(.*)/,
52 archivePath: /^\-archivePath\s*(.*)/,
53 configuration_build_dir: /^(CONFIGURATION_BUILD_DIR=.*)/,
54 shared_precomps_dir: /^(SHARED_PRECOMPS_DIR=.*)/
55};
56/* eslint-enable no-useless-escape */
57
58/**
59 * Creates a project object (see projectFile.js/parseProjectFile) from
60 * a project path and name
61 *
62 * @param {*} projectPath
63 * @param {*} projectName
64 */
65function createProjectObject (projectPath, projectName) {
66 const locations = {
67 root: projectPath,
68 pbxproj: path.join(projectPath, `${projectName}.xcodeproj`, 'project.pbxproj')
69 };
70
71 return projectFile.parse(locations);
72}
73
74/**
75 * Returns a promise that resolves to the default simulator target; the logic here
76 * matches what `cordova emulate ios` does.
77 *
78 * The return object has two properties: `name` (the Xcode destination name),
79 * `identifier` (the simctl identifier), and `simIdentifier` (essentially the cordova emulate target)
80 *
81 * @return {Promise}
82 */
83function getDefaultSimulatorTarget () {
84 events.emit('log', 'Select last emulator from list as default.');
85 return require('./listEmulatorBuildTargets').run()
86 .then(emulators => {
87 let targetEmulator;
88 if (emulators.length > 0) {
89 targetEmulator = emulators[0];
90 }
91 emulators.forEach(emulator => {
92 if (emulator.name.indexOf('iPhone') === 0) {
93 targetEmulator = emulator;
94 }
95 });
96 return targetEmulator;
97 });
98}
99
100/** @returns {Promise<void>} */
101module.exports.run = function (buildOpts) {
102 const projectPath = this.root;
103 let emulatorTarget = '';
104 let projectName = '';
105
106 buildOpts = buildOpts || {};
107
108 if (buildOpts.debug && buildOpts.release) {
109 return Promise.reject(new CordovaError('Cannot specify "debug" and "release" options together.'));
110 }
111
112 if (buildOpts.device && buildOpts.emulator) {
113 return Promise.reject(new CordovaError('Cannot specify "device" and "emulator" options together.'));
114 }
115
116 if (buildOpts.buildConfig) {
117 if (!fs.existsSync(buildOpts.buildConfig)) {
118 return Promise.reject(new CordovaError(`Build config file does not exist: ${buildOpts.buildConfig}`));
119 }
120 events.emit('log', `Reading build config file: ${path.resolve(buildOpts.buildConfig)}`);
121 const contents = fs.readFileSync(buildOpts.buildConfig, 'utf-8');
122 const buildConfig = JSON.parse(contents.replace(/^\ufeff/, '')); // Remove BOM
123 if (buildConfig.ios) {
124 const buildType = buildOpts.release ? 'release' : 'debug';
125 const config = buildConfig.ios[buildType];
126 if (config) {
127 buildConfigProperties.forEach(key => {
128 buildOpts[key] = buildOpts[key] || config[key];
129 });
130 }
131 }
132 }
133
134 return require('./listDevices').run()
135 .then(devices => {
136 if (devices.length > 0 && !(buildOpts.emulator)) {
137 // we also explicitly set device flag in options as we pass
138 // those parameters to other api (build as an example)
139 buildOpts.device = true;
140 return check_reqs.check_ios_deploy();
141 }
142 }).then(() => {
143 // CB-12287: Determine the device we should target when building for a simulator
144 if (!buildOpts.device) {
145 let newTarget = buildOpts.target || '';
146
147 if (newTarget) {
148 // only grab the device name, not the runtime specifier
149 newTarget = newTarget.split(',')[0];
150 }
151 // a target was given to us, find the matching Xcode destination name
152 const promise = require('./listEmulatorBuildTargets').targetForSimIdentifier(newTarget);
153 return promise.then(theTarget => {
154 if (!theTarget) {
155 return getDefaultSimulatorTarget().then(defaultTarget => {
156 emulatorTarget = defaultTarget.name;
157 events.emit('warn', `No simulator found for "${newTarget}. Falling back to the default target.`);
158 events.emit('log', `Building for "${emulatorTarget}" Simulator (${defaultTarget.identifier}, ${defaultTarget.simIdentifier}).`);
159 return emulatorTarget;
160 });
161 } else {
162 emulatorTarget = theTarget.name;
163 events.emit('log', `Building for "${emulatorTarget}" Simulator (${theTarget.identifier}, ${theTarget.simIdentifier}).`);
164 return emulatorTarget;
165 }
166 });
167 }
168 })
169 .then(() => check_reqs.run())
170 .then(() => findXCodeProjectIn(projectPath))
171 .then(name => {
172 projectName = name;
173 let extraConfig = '';
174 if (buildOpts.codeSignIdentity) {
175 extraConfig += `CODE_SIGN_IDENTITY = ${buildOpts.codeSignIdentity}\n`;
176 extraConfig += `CODE_SIGN_IDENTITY[sdk=iphoneos*] = ${buildOpts.codeSignIdentity}\n`;
177 }
178 if (buildOpts.provisioningProfile) {
179 if (typeof buildOpts.provisioningProfile === 'string') {
180 extraConfig += `PROVISIONING_PROFILE = ${buildOpts.provisioningProfile}\n`;
181 } else {
182 const keys = Object.keys(buildOpts.provisioningProfile);
183 extraConfig += `PROVISIONING_PROFILE = ${buildOpts.provisioningProfile[keys[0]]}\n`;
184 }
185 }
186 if (buildOpts.developmentTeam) {
187 extraConfig += `DEVELOPMENT_TEAM = ${buildOpts.developmentTeam}\n`;
188 }
189
190 function writeCodeSignStyle (value) {
191 const project = createProjectObject(projectPath, projectName);
192
193 events.emit('verbose', `Set CODE_SIGN_STYLE Build Property to ${value}.`);
194 project.xcode.updateBuildProperty('CODE_SIGN_STYLE', value);
195 events.emit('verbose', `Set ProvisioningStyle Target Attribute to ${value}.`);
196 project.xcode.addTargetAttribute('ProvisioningStyle', value);
197
198 project.write();
199 }
200
201 if (buildOpts.provisioningProfile) {
202 events.emit('verbose', 'ProvisioningProfile build option set, changing project settings to Manual.');
203 writeCodeSignStyle('Manual');
204 } else if (buildOpts.automaticProvisioning) {
205 events.emit('verbose', 'ProvisioningProfile build option NOT set, changing project settings to Automatic.');
206 writeCodeSignStyle('Automatic');
207 }
208
209 return fs.writeFile(path.join(projectPath, 'cordova/build-extras.xcconfig'), extraConfig, 'utf-8');
210 }).then(() => {
211 const configuration = buildOpts.release ? 'Release' : 'Debug';
212
213 events.emit('log', `Building project: ${path.join(projectPath, `${projectName}.xcworkspace`)}`);
214 events.emit('log', `\tConfiguration: ${configuration}`);
215 events.emit('log', `\tPlatform: ${buildOpts.device ? 'device' : 'emulator'}`);
216 events.emit('log', `\tTarget: ${emulatorTarget}`);
217
218 const buildOutputDir = path.join(projectPath, 'build', `${configuration}-${(buildOpts.device ? 'iphoneos' : 'iphonesimulator')}`);
219
220 // remove the build output folder before building
221 fs.removeSync(buildOutputDir);
222
223 const xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, emulatorTarget, buildOpts);
224 return execa('xcodebuild', xcodebuildArgs, { cwd: projectPath, stdio: 'inherit' });
225 }).then(() => {
226 if (!buildOpts.device || buildOpts.noSign) {
227 return;
228 }
229
230 const project = createProjectObject(projectPath, projectName);
231 const bundleIdentifier = project.getPackageName();
232 const exportOptions = { ...buildOpts.exportOptions, compileBitcode: false, method: 'development' };
233
234 if (buildOpts.packageType) {
235 exportOptions.method = buildOpts.packageType;
236 }
237
238 if (buildOpts.iCloudContainerEnvironment) {
239 exportOptions.iCloudContainerEnvironment = buildOpts.iCloudContainerEnvironment;
240 }
241
242 if (buildOpts.developmentTeam) {
243 exportOptions.teamID = buildOpts.developmentTeam;
244 }
245
246 if (buildOpts.provisioningProfile && bundleIdentifier) {
247 if (typeof buildOpts.provisioningProfile === 'string') {
248 exportOptions.provisioningProfiles = { [bundleIdentifier]: String(buildOpts.provisioningProfile) };
249 } else {
250 events.emit('log', 'Setting multiple provisioning profiles for signing');
251 exportOptions.provisioningProfiles = buildOpts.provisioningProfile;
252 }
253 exportOptions.signingStyle = 'manual';
254 }
255
256 if (buildOpts.codeSignIdentity) {
257 exportOptions.signingCertificate = buildOpts.codeSignIdentity;
258 }
259
260 const exportOptionsPlist = plist.build(exportOptions);
261 const exportOptionsPath = path.join(projectPath, 'exportOptions.plist');
262
263 const configuration = buildOpts.release ? 'Release' : 'Debug';
264 const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);
265
266 function checkSystemRuby () {
267 const ruby_cmd = which.sync('ruby', { nothrow: true });
268
269 if (ruby_cmd !== '/usr/bin/ruby') {
270 events.emit('warn', 'Non-system Ruby in use. This may cause packaging to fail.\n' +
271 'If you use RVM, please run `rvm use system`.\n' +
272 'If you use chruby, please run `chruby system`.');
273 }
274 }
275
276 function packageArchive () {
277 const xcodearchiveArgs = getXcodeArchiveArgs(projectName, projectPath, buildOutputDir, exportOptionsPath, buildOpts);
278 return execa('xcodebuild', xcodearchiveArgs, { cwd: projectPath, stdio: 'inherit' });
279 }
280
281 return fs.writeFile(exportOptionsPath, exportOptionsPlist, 'utf-8')
282 .then(checkSystemRuby)
283 .then(packageArchive);
284 })
285 .then(() => {}); // resolve to undefined
286};
287
288/**
289 * Searches for first XCode project in specified folder
290 * @param {String} projectPath Path where to search project
291 * @return {Promise} Promise either fulfilled with project name or rejected
292 */
293function findXCodeProjectIn (projectPath) {
294 // 'Searching for Xcode project in ' + projectPath);
295 const xcodeProjFiles = fs.readdirSync(projectPath).filter(name => path.extname(name) === '.xcodeproj');
296
297 if (xcodeProjFiles.length === 0) {
298 return Promise.reject(new CordovaError(`No Xcode project found in ${projectPath}`));
299 }
300 if (xcodeProjFiles.length > 1) {
301 events.emit('warn', `Found multiple .xcodeproj directories in \n${projectPath}\nUsing first one`);
302 }
303
304 const projectName = path.basename(xcodeProjFiles[0], '.xcodeproj');
305 return Promise.resolve(projectName);
306}
307
308module.exports.findXCodeProjectIn = findXCodeProjectIn;
309
310/**
311 * Returns array of arguments for xcodebuild
312 * @param {String} projectName Name of xcode project
313 * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
314 * @param {String} configuration Configuration name: debug|release
315 * @param {String} emulatorTarget Target for emulator (rather than default)
316 * @param {Object} buildConfig The build configuration options
317 * @return {Array} Array of arguments that could be passed directly to spawn method
318 */
319function getXcodeBuildArgs (projectName, projectPath, configuration, emulatorTarget, buildConfig = {}) {
320 let options;
321 let buildActions;
322 let settings;
323 const buildFlags = buildConfig.buildFlag;
324 const customArgs = {};
325 customArgs.otherFlags = [];
326
327 if (buildFlags) {
328 if (typeof buildFlags === 'string' || buildFlags instanceof String) {
329 parseBuildFlag(buildFlags, customArgs);
330 } else { // buildFlags is an Array of strings
331 buildFlags.forEach(flag => {
332 parseBuildFlag(flag, customArgs);
333 });
334 }
335 }
336
337 if (buildConfig.device) {
338 options = [
339 '-workspace', customArgs.workspace || `${projectName}.xcworkspace`,
340 '-scheme', customArgs.scheme || projectName,
341 '-configuration', customArgs.configuration || configuration,
342 '-destination', customArgs.destination || 'generic/platform=iOS',
343 '-archivePath', customArgs.archivePath || `${projectName}.xcarchive`
344 ];
345 buildActions = ['archive'];
346 settings = [];
347
348 if (customArgs.configuration_build_dir) {
349 settings.push(customArgs.configuration_build_dir);
350 }
351
352 if (customArgs.shared_precomps_dir) {
353 settings.push(customArgs.shared_precomps_dir);
354 }
355
356 // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
357 // This is preferable to just ignoring the flags that the user has passed in.
358 if (customArgs.sdk) {
359 customArgs.otherFlags = customArgs.otherFlags.concat(['-sdk', customArgs.sdk]);
360 }
361
362 if (buildConfig.automaticProvisioning) {
363 options.push('-allowProvisioningUpdates');
364 }
365 if (buildConfig.authenticationKeyPath) {
366 options.push('-authenticationKeyPath', buildConfig.authenticationKeyPath);
367 }
368 if (buildConfig.authenticationKeyID) {
369 options.push('-authenticationKeyID', buildConfig.authenticationKeyID);
370 }
371 if (buildConfig.authenticationKeyIssuerID) {
372 options.push('-authenticationKeyIssuerID', buildConfig.authenticationKeyIssuerID);
373 }
374 } else { // emulator
375 options = [
376 '-workspace', customArgs.workspace || `${projectName}.xcworkspace`,
377 '-scheme', customArgs.scheme || projectName,
378 '-configuration', customArgs.configuration || configuration,
379 '-sdk', customArgs.sdk || 'iphonesimulator',
380 '-destination', customArgs.destination || `platform=iOS Simulator,name=${emulatorTarget}`
381 ];
382 buildActions = ['build'];
383 settings = [`SYMROOT=${path.join(projectPath, 'build')}`];
384
385 if (customArgs.configuration_build_dir) {
386 settings.push(customArgs.configuration_build_dir);
387 }
388
389 if (customArgs.shared_precomps_dir) {
390 settings.push(customArgs.shared_precomps_dir);
391 }
392
393 // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
394 // This is preferable to just ignoring the flags that the user has passed in.
395 if (customArgs.archivePath) {
396 customArgs.otherFlags = customArgs.otherFlags.concat(['-archivePath', customArgs.archivePath]);
397 }
398 }
399
400 return options.concat(buildActions).concat(settings).concat(customArgs.otherFlags);
401}
402
403/**
404 * Returns array of arguments for xcodebuild
405 * @param {String} projectName Name of xcode project
406 * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
407 * @param {String} outputPath Output directory to contain the IPA
408 * @param {String} exportOptionsPath Path to the exportOptions.plist file
409 * @param {Object} buildConfig Build configuration options
410 * @return {Array} Array of arguments that could be passed directly to spawn method
411 */
412function getXcodeArchiveArgs (projectName, projectPath, outputPath, exportOptionsPath, buildConfig = {}) {
413 const options = [];
414
415 if (buildConfig.automaticProvisioning) {
416 options.push('-allowProvisioningUpdates');
417 }
418 if (buildConfig.authenticationKeyPath) {
419 options.push('-authenticationKeyPath', buildConfig.authenticationKeyPath);
420 }
421 if (buildConfig.authenticationKeyID) {
422 options.push('-authenticationKeyID', buildConfig.authenticationKeyID);
423 }
424 if (buildConfig.authenticationKeyIssuerID) {
425 options.push('-authenticationKeyIssuerID', buildConfig.authenticationKeyIssuerID);
426 }
427
428 return [
429 '-exportArchive',
430 '-archivePath', `${projectName}.xcarchive`,
431 '-exportOptionsPlist', exportOptionsPath,
432 '-exportPath', outputPath
433 ].concat(options);
434}
435
436function parseBuildFlag (buildFlag, args) {
437 let matched;
438 for (const key in buildFlagMatchers) {
439 const found = buildFlag.match(buildFlagMatchers[key]);
440 if (found) {
441 matched = true;
442 // found[0] is the whole match, found[1] is the first match in parentheses.
443 args[key] = found[1];
444 events.emit('warn', util.format('Overriding xcodebuildArg: %s', buildFlag));
445 }
446 }
447
448 if (!matched) {
449 // If the flag starts with a '-' then it is an xcodebuild built-in option or a
450 // user-defined setting. The regex makes sure that we don't split a user-defined
451 // setting that is wrapped in quotes.
452 /* eslint-disable no-useless-escape */
453 if (buildFlag[0] === '-' && !buildFlag.match(/^.*=(\".*\")|(\'.*\')$/)) {
454 args.otherFlags = args.otherFlags.concat(buildFlag.split(' '));
455 events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag.split(' ')));
456 } else {
457 args.otherFlags.push(buildFlag);
458 events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag));
459 }
460 }
461}