build.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env node
  2. /*
  3. Licensed to the Apache Software Foundation (ASF) under one
  4. or more contributor license agreements. See the NOTICE file
  5. distributed with this work for additional information
  6. regarding copyright ownership. The ASF licenses this file
  7. to you under the Apache License, Version 2.0 (the
  8. "License"); you may not use this file except in compliance
  9. with the License. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing,
  12. software distributed under the License is distributed on an
  13. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. KIND, either express or implied. See the License for the
  15. specific language governing permissions and limitations
  16. under the License.
  17. */
  18. var Q = require('q');
  19. var path = require('path');
  20. var fs = require('fs');
  21. var nopt = require('nopt');
  22. var Adb = require('./Adb');
  23. var builders = require('./builders/builders');
  24. var events = require('cordova-common').events;
  25. var spawn = require('cordova-common').superspawn.spawn;
  26. var CordovaError = require('cordova-common').CordovaError;
  27. function parseOpts (options, resolvedTarget, projectRoot) {
  28. options = options || {};
  29. options.argv = nopt({
  30. gradle: Boolean,
  31. ant: Boolean,
  32. prepenv: Boolean,
  33. versionCode: String,
  34. minSdkVersion: String,
  35. gradleArg: [String, Array],
  36. keystore: path,
  37. alias: String,
  38. storePassword: String,
  39. password: String,
  40. keystoreType: String
  41. }, {}, options.argv, 0);
  42. var ret = {
  43. buildType: options.release ? 'release' : 'debug',
  44. buildMethod: process.env.ANDROID_BUILD || 'gradle',
  45. prepEnv: options.argv.prepenv,
  46. arch: resolvedTarget && resolvedTarget.arch,
  47. extraArgs: []
  48. };
  49. if (options.argv.ant || options.argv.gradle) { ret.buildMethod = options.argv.ant ? 'ant' : 'gradle'; }
  50. if (options.nobuild) ret.buildMethod = 'none';
  51. if (options.argv.versionCode) { ret.extraArgs.push('-PcdvVersionCode=' + options.argv.versionCode); }
  52. if (options.argv.minSdkVersion) { ret.extraArgs.push('-PcdvMinSdkVersion=' + options.argv.minSdkVersion); }
  53. if (options.argv.gradleArg) {
  54. ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg);
  55. }
  56. var packageArgs = {};
  57. if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }
  58. ['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (flagName) {
  59. if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
  60. });
  61. var buildConfig = options.buildConfig;
  62. // If some values are not specified as command line arguments - use build config to supplement them.
  63. // Command line arguemnts have precedence over build config.
  64. if (buildConfig) {
  65. if (!fs.existsSync(buildConfig)) {
  66. throw new Error('Specified build config file does not exist: ' + buildConfig);
  67. }
  68. events.emit('log', 'Reading build config file: ' + path.resolve(buildConfig));
  69. var buildjson = fs.readFileSync(buildConfig, 'utf8');
  70. var config = JSON.parse(buildjson.replace(/^\ufeff/, '')); // Remove BOM
  71. if (config.android && config.android[ret.buildType]) {
  72. var androidInfo = config.android[ret.buildType];
  73. if (androidInfo.keystore && !packageArgs.keystore) {
  74. if (androidInfo.keystore.substr(0, 1) === '~') {
  75. androidInfo.keystore = process.env.HOME + androidInfo.keystore.substr(1);
  76. }
  77. packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore);
  78. events.emit('log', 'Reading the keystore from: ' + packageArgs.keystore);
  79. }
  80. ['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (key) {
  81. packageArgs[key] = packageArgs[key] || androidInfo[key];
  82. });
  83. }
  84. }
  85. if (packageArgs.keystore && packageArgs.alias) {
  86. ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword,
  87. packageArgs.password, packageArgs.keystoreType);
  88. }
  89. if (!ret.packageInfo) {
  90. if (Object.keys(packageArgs).length > 0) {
  91. events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.');
  92. }
  93. }
  94. return ret;
  95. }
  96. /*
  97. * Builds the project with the specifed options
  98. * Returns a promise.
  99. */
  100. module.exports.runClean = function (options) {
  101. var opts = parseOpts(options, null, this.root);
  102. var builder = builders.getBuilder(opts.buildMethod);
  103. return builder.prepEnv(opts).then(function () {
  104. return builder.clean(opts);
  105. });
  106. };
  107. /**
  108. * Builds the project with the specifed options.
  109. *
  110. * @param {BuildOptions} options A set of options. See PlatformApi.build
  111. * method documentation for reference.
  112. * @param {Object} optResolvedTarget A deployment target. Used to pass
  113. * target architecture from upstream 'run' call. TODO: remove this option in
  114. * favor of setting buildOptions.archs field.
  115. *
  116. * @return {Promise<Object>} Promise, resolved with built packages
  117. * information.
  118. */
  119. module.exports.run = function (options, optResolvedTarget) {
  120. var opts = parseOpts(options, optResolvedTarget, this.root);
  121. var builder = builders.getBuilder(opts.buildMethod);
  122. return builder.prepEnv(opts).then(function () {
  123. if (opts.prepEnv) {
  124. events.emit('verbose', 'Build file successfully prepared.');
  125. return;
  126. }
  127. return builder.build(opts).then(function () {
  128. var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
  129. events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t'));
  130. return {
  131. apkPaths: apkPaths,
  132. buildType: opts.buildType,
  133. buildMethod: opts.buildMethod
  134. };
  135. });
  136. });
  137. };
  138. /*
  139. * Detects the architecture of a device/emulator
  140. * Returns "arm" or "x86".
  141. */
  142. module.exports.detectArchitecture = function (target) {
  143. function helper () {
  144. return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
  145. return /intel/i.exec(output) ? 'x86' : 'arm';
  146. });
  147. }
  148. // It sometimes happens (at least on OS X), that this command will hang forever.
  149. // To fix it, either unplug & replug device, or restart adb server.
  150. return helper().timeout(1000, new CordovaError('Device communication timed out. Try unplugging & replugging the device.')).then(null, function (err) {
  151. if (/timed out/.exec('' + err)) {
  152. // adb kill-server doesn't seem to do the trick.
  153. // Could probably find a x-platform version of killall, but I'm not actually
  154. // sure that this scenario even happens on non-OSX machines.
  155. events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.');
  156. return spawn('killall', ['adb']).then(function () {
  157. return helper().then(null, function () {
  158. // The double kill is sadly often necessary, at least on mac.
  159. events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.');
  160. return spawn('killall', ['adb']).then(function () {
  161. return helper().then(null, function () {
  162. return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.'));
  163. });
  164. });
  165. });
  166. }, function () {
  167. // For non-killall OS's.
  168. return Q.reject(err);
  169. });
  170. }
  171. throw err;
  172. });
  173. };
  174. module.exports.findBestApkForArchitecture = function (buildResults, arch) {
  175. var paths = buildResults.apkPaths.filter(function (p) {
  176. var apkName = path.basename(p);
  177. if (buildResults.buildType === 'debug') {
  178. return /-debug/.exec(apkName);
  179. }
  180. return !/-debug/.exec(apkName);
  181. });
  182. var archPattern = new RegExp('-' + arch);
  183. var hasArchPattern = /-x86|-arm/;
  184. for (var i = 0; i < paths.length; ++i) {
  185. var apkName = path.basename(paths[i]);
  186. if (hasArchPattern.exec(apkName)) {
  187. if (archPattern.exec(apkName)) {
  188. return paths[i];
  189. }
  190. } else {
  191. return paths[i];
  192. }
  193. }
  194. throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType);
  195. };
  196. function PackageInfo (keystore, alias, storePassword, password, keystoreType) {
  197. this.keystore = {
  198. 'name': 'key.store',
  199. 'value': keystore
  200. };
  201. this.alias = {
  202. 'name': 'key.alias',
  203. 'value': alias
  204. };
  205. if (storePassword) {
  206. this.storePassword = {
  207. 'name': 'key.store.password',
  208. 'value': storePassword
  209. };
  210. }
  211. if (password) {
  212. this.password = {
  213. 'name': 'key.alias.password',
  214. 'value': password
  215. };
  216. }
  217. if (keystoreType) {
  218. this.keystoreType = {
  219. 'name': 'key.store.type',
  220. 'value': keystoreType
  221. };
  222. }
  223. }
  224. PackageInfo.prototype = {
  225. toProperties: function () {
  226. var self = this;
  227. var result = '';
  228. Object.keys(self).forEach(function (key) {
  229. result += self[key].name;
  230. result += '=';
  231. result += self[key].value.replace(/\\/g, '\\\\');
  232. result += '\n';
  233. });
  234. return result;
  235. }
  236. };
  237. module.exports.help = function () {
  238. console.log('Usage: ' + path.relative(process.cwd(), path.join('../build')) + ' [flags] [Signed APK flags]');
  239. console.log('Flags:');
  240. console.log(' \'--debug\': will build project in debug mode (default)');
  241. console.log(' \'--release\': will build project for release');
  242. console.log(' \'--ant\': will build project with ant');
  243. console.log(' \'--gradle\': will build project with gradle (default)');
  244. console.log(' \'--nobuild\': will skip build process (useful when using run command)');
  245. console.log(' \'--prepenv\': don\'t build, but copy in build scripts where necessary');
  246. console.log(' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs. Requires --gradle.');
  247. console.log(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.');
  248. console.log(' \'--gradleArg=<gradle command line arg>\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true');
  249. console.log('');
  250. console.log('Signed APK flags (overwrites debug/release-signing.proprties) :');
  251. console.log(' \'--keystore=<path to keystore>\': Key store used to build a signed archive. (Required)');
  252. console.log(' \'--alias=\': Alias for the key store. (Required)');
  253. console.log(' \'--storePassword=\': Password for the key store. (Optional - prompted)');
  254. console.log(' \'--password=\': Password for the key. (Optional - prompted)');
  255. console.log(' \'--keystoreType\': Type of the keystore. (Optional)');
  256. process.exit(0);
  257. };