build.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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. var Q = require('q');
  20. var path = require('path');
  21. var shell = require('shelljs');
  22. var spawn = require('./spawn');
  23. var fs = require('fs');
  24. var plist = require('plist');
  25. var util = require('util');
  26. var check_reqs = require('./check_reqs');
  27. var projectFile = require('./projectFile');
  28. var events = require('cordova-common').events;
  29. var projectPath = path.join(__dirname, '..', '..');
  30. var projectName = null;
  31. // These are regular expressions to detect if the user is changing any of the built-in xcodebuildArgs
  32. /* eslint-disable no-useless-escape */
  33. var buildFlagMatchers = {
  34. 'xcconfig': /^\-xcconfig\s*(.*)$/,
  35. 'workspace': /^\-workspace\s*(.*)/,
  36. 'scheme': /^\-scheme\s*(.*)/,
  37. 'configuration': /^\-configuration\s*(.*)/,
  38. 'sdk': /^\-sdk\s*(.*)/,
  39. 'destination': /^\-destination\s*(.*)/,
  40. 'archivePath': /^\-archivePath\s*(.*)/,
  41. 'configuration_build_dir': /^(CONFIGURATION_BUILD_DIR=.*)/,
  42. 'shared_precomps_dir': /^(SHARED_PRECOMPS_DIR=.*)/
  43. };
  44. /* eslint-enable no-useless-escape */
  45. /**
  46. * Returns a promise that resolves to the default simulator target; the logic here
  47. * matches what `cordova emulate ios` does.
  48. *
  49. * The return object has two properties: `name` (the Xcode destination name),
  50. * `identifier` (the simctl identifier), and `simIdentifier` (essentially the cordova emulate target)
  51. *
  52. * @return {Promise}
  53. */
  54. function getDefaultSimulatorTarget () {
  55. return require('./list-emulator-build-targets').run()
  56. .then(function (emulators) {
  57. var targetEmulator;
  58. if (emulators.length > 0) {
  59. targetEmulator = emulators[0];
  60. }
  61. emulators.forEach(function (emulator) {
  62. if (emulator.name.indexOf('iPhone') === 0) {
  63. targetEmulator = emulator;
  64. }
  65. });
  66. return targetEmulator;
  67. });
  68. }
  69. module.exports.run = function (buildOpts) {
  70. var emulatorTarget = '';
  71. buildOpts = buildOpts || {};
  72. if (buildOpts.debug && buildOpts.release) {
  73. return Q.reject('Cannot specify "debug" and "release" options together.');
  74. }
  75. if (buildOpts.device && buildOpts.emulator) {
  76. return Q.reject('Cannot specify "device" and "emulator" options together.');
  77. }
  78. if (buildOpts.buildConfig) {
  79. if (!fs.existsSync(buildOpts.buildConfig)) {
  80. return Q.reject('Build config file does not exist:' + buildOpts.buildConfig);
  81. }
  82. events.emit('log', 'Reading build config file:', path.resolve(buildOpts.buildConfig));
  83. var contents = fs.readFileSync(buildOpts.buildConfig, 'utf-8');
  84. var buildConfig = JSON.parse(contents.replace(/^\ufeff/, '')); // Remove BOM
  85. if (buildConfig.ios) {
  86. var buildType = buildOpts.release ? 'release' : 'debug';
  87. var config = buildConfig.ios[buildType];
  88. if (config) {
  89. ['codeSignIdentity', 'codeSignResourceRules', 'provisioningProfile', 'developmentTeam', 'packageType', 'buildFlag', 'iCloudContainerEnvironment', 'automaticProvisioning'].forEach(
  90. function (key) {
  91. buildOpts[key] = buildOpts[key] || config[key];
  92. });
  93. }
  94. }
  95. }
  96. return require('./list-devices').run()
  97. .then(function (devices) {
  98. if (devices.length > 0 && !(buildOpts.emulator)) {
  99. // we also explicitly set device flag in options as we pass
  100. // those parameters to other api (build as an example)
  101. buildOpts.device = true;
  102. return check_reqs.check_ios_deploy();
  103. }
  104. }).then(function () {
  105. // CB-12287: Determine the device we should target when building for a simulator
  106. if (!buildOpts.device) {
  107. var newTarget = buildOpts.target || '';
  108. if (newTarget) {
  109. // only grab the device name, not the runtime specifier
  110. newTarget = newTarget.split(',')[0];
  111. }
  112. // a target was given to us, find the matching Xcode destination name
  113. var promise = require('./list-emulator-build-targets').targetForSimIdentifier(newTarget);
  114. return promise.then(function (theTarget) {
  115. if (!theTarget) {
  116. return getDefaultSimulatorTarget().then(function (defaultTarget) {
  117. emulatorTarget = defaultTarget.name;
  118. events.emit('log', 'Building for ' + emulatorTarget + ' Simulator');
  119. return emulatorTarget;
  120. });
  121. } else {
  122. emulatorTarget = theTarget.name;
  123. events.emit('log', 'Building for ' + emulatorTarget + ' Simulator');
  124. return emulatorTarget;
  125. }
  126. });
  127. }
  128. }).then(function () {
  129. return check_reqs.run();
  130. }).then(function () {
  131. return findXCodeProjectIn(projectPath);
  132. }).then(function (name) {
  133. projectName = name;
  134. var extraConfig = '';
  135. if (buildOpts.codeSignIdentity) {
  136. extraConfig += 'CODE_SIGN_IDENTITY = ' + buildOpts.codeSignIdentity + '\n';
  137. extraConfig += 'CODE_SIGN_IDENTITY[sdk=iphoneos*] = ' + buildOpts.codeSignIdentity + '\n';
  138. }
  139. if (buildOpts.codeSignResourceRules) {
  140. extraConfig += 'CODE_SIGN_RESOURCE_RULES_PATH = ' + buildOpts.codeSignResourceRules + '\n';
  141. }
  142. if (buildOpts.provisioningProfile) {
  143. extraConfig += 'PROVISIONING_PROFILE = ' + buildOpts.provisioningProfile + '\n';
  144. }
  145. if (buildOpts.developmentTeam) {
  146. extraConfig += 'DEVELOPMENT_TEAM = ' + buildOpts.developmentTeam + '\n';
  147. }
  148. return Q.nfcall(fs.writeFile, path.join(__dirname, '..', 'build-extras.xcconfig'), extraConfig, 'utf-8');
  149. }).then(function () {
  150. var configuration = buildOpts.release ? 'Release' : 'Debug';
  151. events.emit('log', 'Building project: ' + path.join(projectPath, projectName + '.xcworkspace'));
  152. events.emit('log', '\tConfiguration: ' + configuration);
  153. events.emit('log', '\tPlatform: ' + (buildOpts.device ? 'device' : 'emulator'));
  154. var buildOutputDir = path.join(projectPath, 'build', (buildOpts.device ? 'device' : 'emulator'));
  155. // remove the build/device folder before building
  156. return spawn('rm', [ '-rf', buildOutputDir ], projectPath)
  157. .then(function () {
  158. var xcodebuildArgs = getXcodeBuildArgs(projectName, projectPath, configuration, buildOpts.device, buildOpts.buildFlag, emulatorTarget);
  159. return spawn('xcodebuild', xcodebuildArgs, projectPath);
  160. });
  161. }).then(function () {
  162. if (!buildOpts.device || buildOpts.noSign) {
  163. return;
  164. }
  165. var locations = {
  166. root: projectPath,
  167. pbxproj: path.join(projectPath, projectName + '.xcodeproj', 'project.pbxproj')
  168. };
  169. var bundleIdentifier = projectFile.parse(locations).getPackageName();
  170. var exportOptions = {'compileBitcode': false, 'method': 'development'};
  171. if (buildOpts.packageType) {
  172. exportOptions.method = buildOpts.packageType;
  173. }
  174. if (buildOpts.iCloudContainerEnvironment) {
  175. exportOptions.iCloudContainerEnvironment = buildOpts.iCloudContainerEnvironment;
  176. }
  177. if (buildOpts.developmentTeam) {
  178. exportOptions.teamID = buildOpts.developmentTeam;
  179. }
  180. if (buildOpts.provisioningProfile && bundleIdentifier) {
  181. exportOptions.provisioningProfiles = { [ bundleIdentifier ]: String(buildOpts.provisioningProfile) };
  182. exportOptions.signingStyle = 'manual';
  183. }
  184. if (buildOpts.codeSignIdentity) {
  185. exportOptions.signingCertificate = buildOpts.codeSignIdentity;
  186. }
  187. var exportOptionsPlist = plist.build(exportOptions);
  188. var exportOptionsPath = path.join(projectPath, 'exportOptions.plist');
  189. var buildOutputDir = path.join(projectPath, 'build', 'device');
  190. function checkSystemRuby () {
  191. var ruby_cmd = shell.which('ruby');
  192. if (ruby_cmd !== '/usr/bin/ruby') {
  193. events.emit('warn', 'Non-system Ruby in use. This may cause packaging to fail.\n' +
  194. 'If you use RVM, please run `rvm use system`.\n' +
  195. 'If you use chruby, please run `chruby system`.');
  196. }
  197. }
  198. function packageArchive () {
  199. var xcodearchiveArgs = getXcodeArchiveArgs(projectName, projectPath, buildOutputDir, exportOptionsPath, buildOpts.automaticProvisioning);
  200. return spawn('xcodebuild', xcodearchiveArgs, projectPath);
  201. }
  202. return Q.nfcall(fs.writeFile, exportOptionsPath, exportOptionsPlist, 'utf-8')
  203. .then(checkSystemRuby)
  204. .then(packageArchive);
  205. });
  206. };
  207. /**
  208. * Searches for first XCode project in specified folder
  209. * @param {String} projectPath Path where to search project
  210. * @return {Promise} Promise either fulfilled with project name or rejected
  211. */
  212. function findXCodeProjectIn (projectPath) {
  213. // 'Searching for Xcode project in ' + projectPath);
  214. var xcodeProjFiles = shell.ls(projectPath).filter(function (name) {
  215. return path.extname(name) === '.xcodeproj';
  216. });
  217. if (xcodeProjFiles.length === 0) {
  218. return Q.reject('No Xcode project found in ' + projectPath);
  219. }
  220. if (xcodeProjFiles.length > 1) {
  221. events.emit('warn', 'Found multiple .xcodeproj directories in \n' +
  222. projectPath + '\nUsing first one');
  223. }
  224. var projectName = path.basename(xcodeProjFiles[0], '.xcodeproj');
  225. return Q.resolve(projectName);
  226. }
  227. module.exports.findXCodeProjectIn = findXCodeProjectIn;
  228. /**
  229. * Returns array of arguments for xcodebuild
  230. * @param {String} projectName Name of xcode project
  231. * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
  232. * @param {String} configuration Configuration name: debug|release
  233. * @param {Boolean} isDevice Flag that specify target for package (device/emulator)
  234. * @param {Array} buildFlags
  235. * @param {String} emulatorTarget Target for emulator (rather than default)
  236. * @return {Array} Array of arguments that could be passed directly to spawn method
  237. */
  238. function getXcodeBuildArgs (projectName, projectPath, configuration, isDevice, buildFlags, emulatorTarget) {
  239. var xcodebuildArgs;
  240. var options;
  241. var buildActions;
  242. var settings;
  243. var customArgs = {};
  244. customArgs.otherFlags = [];
  245. if (buildFlags) {
  246. if (typeof buildFlags === 'string' || buildFlags instanceof String) {
  247. parseBuildFlag(buildFlags, customArgs);
  248. } else { // buildFlags is an Array of strings
  249. buildFlags.forEach(function (flag) {
  250. parseBuildFlag(flag, customArgs);
  251. });
  252. }
  253. }
  254. if (isDevice) {
  255. options = [
  256. '-xcconfig', customArgs.xcconfig || path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'),
  257. '-workspace', customArgs.workspace || projectName + '.xcworkspace',
  258. '-scheme', customArgs.scheme || projectName,
  259. '-configuration', customArgs.configuration || configuration,
  260. '-destination', customArgs.destination || 'generic/platform=iOS',
  261. '-archivePath', customArgs.archivePath || projectName + '.xcarchive'
  262. ];
  263. buildActions = [ 'archive' ];
  264. settings = [
  265. customArgs.configuration_build_dir || 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'device'),
  266. customArgs.shared_precomps_dir || 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch')
  267. ];
  268. // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
  269. // This is preferable to just ignoring the flags that the user has passed in.
  270. if (customArgs.sdk) {
  271. customArgs.otherFlags = customArgs.otherFlags.concat(['-sdk', customArgs.sdk]);
  272. }
  273. } else { // emulator
  274. options = [
  275. '-xcconfig', customArgs.xcconfig || path.join(__dirname, '..', 'build-' + configuration.toLowerCase() + '.xcconfig'),
  276. '-workspace', customArgs.project || projectName + '.xcworkspace',
  277. '-scheme', customArgs.scheme || projectName,
  278. '-configuration', customArgs.configuration || configuration,
  279. '-sdk', customArgs.sdk || 'iphonesimulator',
  280. '-destination', customArgs.destination || 'platform=iOS Simulator,name=' + emulatorTarget
  281. ];
  282. buildActions = [ 'build' ];
  283. settings = [
  284. customArgs.configuration_build_dir || 'CONFIGURATION_BUILD_DIR=' + path.join(projectPath, 'build', 'emulator'),
  285. customArgs.shared_precomps_dir || 'SHARED_PRECOMPS_DIR=' + path.join(projectPath, 'build', 'sharedpch')
  286. ];
  287. // Add other matched flags to otherFlags to let xcodebuild present an appropriate error.
  288. // This is preferable to just ignoring the flags that the user has passed in.
  289. if (customArgs.archivePath) {
  290. customArgs.otherFlags = customArgs.otherFlags.concat(['-archivePath', customArgs.archivePath]);
  291. }
  292. }
  293. xcodebuildArgs = options.concat(buildActions).concat(settings).concat(customArgs.otherFlags);
  294. return xcodebuildArgs;
  295. }
  296. /**
  297. * Returns array of arguments for xcodebuild
  298. * @param {String} projectName Name of xcode project
  299. * @param {String} projectPath Path to project file. Will be used to set CWD for xcodebuild
  300. * @param {String} outputPath Output directory to contain the IPA
  301. * @param {String} exportOptionsPath Path to the exportOptions.plist file
  302. * @param {Boolean} autoProvisioning Whether to allow Xcode to automatically update provisioning
  303. * @return {Array} Array of arguments that could be passed directly to spawn method
  304. */
  305. function getXcodeArchiveArgs (projectName, projectPath, outputPath, exportOptionsPath, autoProvisioning) {
  306. return [
  307. '-exportArchive',
  308. '-archivePath', projectName + '.xcarchive',
  309. '-exportOptionsPlist', exportOptionsPath,
  310. '-exportPath', outputPath
  311. ].concat(autoProvisioning ? ['-allowProvisioningUpdates'] : []);
  312. }
  313. function parseBuildFlag (buildFlag, args) {
  314. var matched;
  315. for (var key in buildFlagMatchers) {
  316. var found = buildFlag.match(buildFlagMatchers[key]);
  317. if (found) {
  318. matched = true;
  319. // found[0] is the whole match, found[1] is the first match in parentheses.
  320. args[key] = found[1];
  321. events.emit('warn', util.format('Overriding xcodebuildArg: %s', buildFlag));
  322. }
  323. }
  324. if (!matched) {
  325. // If the flag starts with a '-' then it is an xcodebuild built-in option or a
  326. // user-defined setting. The regex makes sure that we don't split a user-defined
  327. // setting that is wrapped in quotes.
  328. /* eslint-disable no-useless-escape */
  329. if (buildFlag[0] === '-' && !buildFlag.match(/^.*=(\".*\")|(\'.*\')$/)) {
  330. args.otherFlags = args.otherFlags.concat(buildFlag.split(' '));
  331. events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag.split(' ')));
  332. } else {
  333. args.otherFlags.push(buildFlag);
  334. events.emit('warn', util.format('Adding xcodebuildArg: %s', buildFlag));
  335. }
  336. }
  337. }
  338. // help/usage function
  339. module.exports.help = function help () {
  340. console.log('');
  341. console.log('Usage: build [--debug | --release] [--archs=\"<list of architectures...>\"]');
  342. console.log(' [--device | --simulator] [--codeSignIdentity=\"<identity>\"]');
  343. console.log(' [--codeSignResourceRules=\"<resourcerules path>\"]');
  344. console.log(' [--developmentTeam=\"<Team ID>\"]');
  345. console.log(' [--provisioningProfile=\"<provisioning profile>\"]');
  346. console.log(' --help : Displays this dialog.');
  347. console.log(' --debug : Builds project in debug mode. (Default)');
  348. console.log(' --release : Builds project in release mode.');
  349. console.log(' -r : Shortcut :: builds project in release mode.');
  350. /* eslint-enable no-useless-escape */
  351. // TODO: add support for building different archs
  352. // console.log(" --archs : Builds project binaries for specific chip architectures (`anycpu`, `arm`, `x86`, `x64`).");
  353. console.log(' --device, --simulator');
  354. console.log(' : Specifies, what type of project to build');
  355. console.log(' --codeSignIdentity : Type of signing identity used for code signing.');
  356. console.log(' --codeSignResourceRules : Path to ResourceRules.plist.');
  357. console.log(' --developmentTeam : New for Xcode 8. The development team (Team ID)');
  358. console.log(' to use for code signing.');
  359. console.log(' --provisioningProfile : UUID of the profile.');
  360. console.log(' --device --noSign : Builds project without application signing.');
  361. console.log('');
  362. console.log('examples:');
  363. console.log(' build ');
  364. console.log(' build --debug');
  365. console.log(' build --release');
  366. console.log(' build --codeSignIdentity="iPhone Distribution" --provisioningProfile="926c2bd6-8de9-4c2f-8407-1016d2d12954"');
  367. // TODO: add support for building different archs
  368. // console.log(" build --release --archs=\"armv7\"");
  369. console.log('');
  370. process.exit(0);
  371. };