1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const path = require('path');
|
21 | const which = require('which');
|
22 | const execa = require('execa');
|
23 | const { CordovaError, events } = require('cordova-common');
|
24 | const fs = require('fs-extra');
|
25 | const plist = require('plist');
|
26 | const util = require('util');
|
27 |
|
28 | const check_reqs = require('./check_reqs');
|
29 | const projectFile = require('./projectFile');
|
30 |
|
31 | const buildConfigProperties = [
|
32 | 'codeSignIdentity',
|
33 | 'provisioningProfile',
|
34 | 'developmentTeam',
|
35 | 'packageType',
|
36 | 'buildFlag',
|
37 | 'iCloudContainerEnvironment',
|
38 | 'automaticProvisioning',
|
39 | 'authenticationKeyPath',
|
40 | 'authenticationKeyID',
|
41 | 'authenticationKeyIssuerID'
|
42 | ];
|
43 |
|
44 |
|
45 |
|
46 | const 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 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | function 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 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | function 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 |
|
101 | module.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/, ''));
|
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 |
|
138 |
|
139 | buildOpts.device = true;
|
140 | return check_reqs.check_ios_deploy();
|
141 | }
|
142 | }).then(() => {
|
143 |
|
144 | if (!buildOpts.device) {
|
145 | let newTarget = buildOpts.target || '';
|
146 |
|
147 | if (newTarget) {
|
148 |
|
149 | newTarget = newTarget.split(',')[0];
|
150 | }
|
151 |
|
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 |
|
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(() => {});
|
286 | };
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | function findXCodeProjectIn (projectPath) {
|
294 |
|
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 |
|
308 | module.exports.findXCodeProjectIn = findXCodeProjectIn;
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 | function 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 {
|
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 |
|
357 |
|
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 {
|
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 |
|
394 |
|
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 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 | function 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 |
|
436 | function 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 |
|
443 | args[key] = found[1];
|
444 | events.emit('warn', util.format('Overriding xcodebuildArg: %s', buildFlag));
|
445 | }
|
446 | }
|
447 |
|
448 | if (!matched) {
|
449 |
|
450 |
|
451 |
|
452 |
|
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 | }
|