ProjectBuilder.js 16 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. http://www.apache.org/licenses/LICENSE-2.0
  10. Unless required by applicable law or agreed to in writing,
  11. software distributed under the License is distributed on an
  12. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13. KIND, either express or implied. See the License for the
  14. specific language governing permissions and limitations
  15. under the License.
  16. */
  17. /* eslint no-self-assign: 0 */
  18. /* eslint no-unused-vars: 0 */
  19. var Q = require('q');
  20. var fs = require('fs');
  21. var path = require('path');
  22. var shell = require('shelljs');
  23. var spawn = require('cordova-common').superspawn.spawn;
  24. var events = require('cordova-common').events;
  25. var CordovaError = require('cordova-common').CordovaError;
  26. var check_reqs = require('../check_reqs');
  27. var PackageType = require('../PackageType');
  28. const compareFunc = require('compare-func');
  29. const MARKER = 'YOUR CHANGES WILL BE ERASED!';
  30. const SIGNING_PROPERTIES = '-signing.properties';
  31. const TEMPLATE =
  32. '# This file is automatically generated.\n' +
  33. '# Do not modify this file -- ' + MARKER + '\n';
  34. class ProjectBuilder {
  35. constructor (rootDirectory) {
  36. this.root = rootDirectory || path.resolve(__dirname, '../../..');
  37. this.apkDir = path.join(this.root, 'app', 'build', 'outputs', 'apk');
  38. this.aabDir = path.join(this.root, 'app', 'build', 'outputs', 'bundle');
  39. }
  40. getArgs (cmd, opts) {
  41. let args;
  42. let buildCmd = cmd;
  43. if (opts.packageType === PackageType.BUNDLE) {
  44. if (cmd === 'release') {
  45. buildCmd = ':app:bundleRelease';
  46. } else if (cmd === 'debug') {
  47. buildCmd = ':app:bundleDebug';
  48. }
  49. args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
  50. } else {
  51. if (cmd === 'release') {
  52. buildCmd = 'cdvBuildRelease';
  53. } else if (cmd === 'debug') {
  54. buildCmd = 'cdvBuildDebug';
  55. }
  56. args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
  57. if (opts.arch) {
  58. args.push('-PcdvBuildArch=' + opts.arch);
  59. }
  60. args.push.apply(args, opts.extraArgs);
  61. }
  62. return args;
  63. }
  64. /*
  65. * This returns a promise
  66. */
  67. runGradleWrapper (gradle_cmd) {
  68. var gradlePath = path.join(this.root, 'gradlew');
  69. var wrapperGradle = path.join(this.root, 'wrapper.gradle');
  70. if (fs.existsSync(gradlePath)) {
  71. // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
  72. } else {
  73. return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
  74. }
  75. }
  76. readProjectProperties () {
  77. function findAllUniq (data, r) {
  78. var s = {};
  79. var m;
  80. while ((m = r.exec(data))) {
  81. s[m[1]] = 1;
  82. }
  83. return Object.keys(s);
  84. }
  85. var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
  86. return {
  87. libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
  88. gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
  89. systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
  90. };
  91. }
  92. extractRealProjectNameFromManifest () {
  93. var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
  94. var manifestData = fs.readFileSync(manifestPath, 'utf8');
  95. var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData);
  96. if (!m) {
  97. throw new CordovaError('Could not find package name in ' + manifestPath);
  98. }
  99. var packageName = m[1];
  100. var lastDotIndex = packageName.lastIndexOf('.');
  101. return packageName.substring(lastDotIndex + 1);
  102. }
  103. // Makes the project buildable, minus the gradle wrapper.
  104. prepBuildFiles () {
  105. // Update the version of build.gradle in each dependent library.
  106. var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle');
  107. var propertiesObj = this.readProjectProperties();
  108. var subProjects = propertiesObj.libs;
  109. // Check and copy the gradle file into the subproject
  110. // Called by the loop before this function def
  111. var checkAndCopy = function (subProject, root) {
  112. var subProjectGradle = path.join(root, subProject, 'build.gradle');
  113. // This is the future-proof way of checking if a file exists
  114. // This must be synchronous to satisfy a Travis test
  115. try {
  116. fs.accessSync(subProjectGradle, fs.F_OK);
  117. } catch (e) {
  118. shell.cp('-f', pluginBuildGradle, subProjectGradle);
  119. }
  120. };
  121. for (var i = 0; i < subProjects.length; ++i) {
  122. if (subProjects[i] !== 'CordovaLib') {
  123. checkAndCopy(subProjects[i], this.root);
  124. }
  125. }
  126. var name = this.extractRealProjectNameFromManifest();
  127. // Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
  128. var settingsGradlePaths = subProjects.map(function (p) {
  129. var realDir = p.replace(/[/\\]/g, ':');
  130. var libName = realDir.replace(name + '-', '');
  131. var str = 'include ":' + libName + '"\n';
  132. if (realDir.indexOf(name + '-') !== -1) {
  133. str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n';
  134. }
  135. return str;
  136. });
  137. fs.writeFileSync(path.join(this.root, 'settings.gradle'),
  138. '// GENERATED FILE - DO NOT EDIT\n' +
  139. 'include ":"\n' + settingsGradlePaths.join(''));
  140. // Update dependencies within build.gradle.
  141. var buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
  142. var depsList = '';
  143. var root = this.root;
  144. var insertExclude = function (p) {
  145. var gradlePath = path.join(root, p, 'build.gradle');
  146. var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
  147. if (projectGradleFile.indexOf('CordovaLib') !== -1) {
  148. depsList += '{\n exclude module:("CordovaLib")\n }\n';
  149. } else {
  150. depsList += '\n';
  151. }
  152. };
  153. subProjects.forEach(function (p) {
  154. events.emit('log', 'Subproject Path: ' + p);
  155. var libName = p.replace(/[/\\]/g, ':').replace(name + '-', '');
  156. if (libName !== 'app') {
  157. depsList += ' implementation(project(path: ":' + libName + '"))';
  158. insertExclude(p);
  159. }
  160. });
  161. // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
  162. var SYSTEM_LIBRARY_MAPPINGS = [
  163. [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'],
  164. [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+']
  165. ];
  166. propertiesObj.systemLibs.forEach(function (p) {
  167. var mavenRef;
  168. // It's already in gradle form if it has two ':'s
  169. if (/:.*:/.exec(p)) {
  170. mavenRef = p;
  171. } else {
  172. for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
  173. var pair = SYSTEM_LIBRARY_MAPPINGS[i];
  174. if (pair[0].exec(p)) {
  175. mavenRef = p.replace(pair[0], pair[1]);
  176. break;
  177. }
  178. }
  179. if (!mavenRef) {
  180. throw new CordovaError('Unsupported system library (does not work with gradle): ' + p);
  181. }
  182. }
  183. depsList += ' implementation "' + mavenRef + '"\n';
  184. });
  185. buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2');
  186. var includeList = '';
  187. propertiesObj.gradleIncludes.forEach(function (includePath) {
  188. includeList += 'apply from: "../' + includePath + '"\n';
  189. });
  190. buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2');
  191. // This needs to be stored in the app gradle, not the root grade
  192. fs.writeFileSync(path.join(this.root, 'app', 'build.gradle'), buildGradle);
  193. }
  194. prepEnv (opts) {
  195. var self = this;
  196. return check_reqs.check_gradle()
  197. .then(function (gradlePath) {
  198. return self.runGradleWrapper(gradlePath);
  199. }).then(function () {
  200. return self.prepBuildFiles();
  201. }).then(function () {
  202. // If the gradle distribution URL is set, make sure it points to version we want.
  203. // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with.
  204. // For some reason, using ^ and $ don't work. This does the job, though.
  205. var distributionUrlRegex = /distributionUrl.*zip/;
  206. var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-4.10.3-all.zip';
  207. var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties');
  208. shell.chmod('u+w', gradleWrapperPropertiesPath);
  209. shell.sed('-i', distributionUrlRegex, 'distributionUrl=' + distributionUrl, gradleWrapperPropertiesPath);
  210. var propertiesFile = opts.buildType + SIGNING_PROPERTIES;
  211. var propertiesFilePath = path.join(self.root, propertiesFile);
  212. if (opts.packageInfo) {
  213. fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties());
  214. } else if (isAutoGenerated(propertiesFilePath)) {
  215. shell.rm('-f', propertiesFilePath);
  216. }
  217. });
  218. }
  219. /*
  220. * Builds the project with gradle.
  221. * Returns a promise.
  222. */
  223. build (opts) {
  224. var wrapper = path.join(this.root, 'gradlew');
  225. var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
  226. return spawn(wrapper, args, { stdio: 'pipe' })
  227. .progress(function (stdio) {
  228. if (stdio.stderr) {
  229. /*
  230. * Workaround for the issue with Java printing some unwanted information to
  231. * stderr instead of stdout.
  232. * This function suppresses 'Picked up _JAVA_OPTIONS' message from being
  233. * printed to stderr. See https://issues.apache.org/jira/browse/CB-9971 for
  234. * explanation.
  235. */
  236. var suppressThisLine = /^Picked up _JAVA_OPTIONS: /i.test(stdio.stderr.toString());
  237. if (suppressThisLine) {
  238. return;
  239. }
  240. process.stderr.write(stdio.stderr);
  241. } else {
  242. process.stdout.write(stdio.stdout);
  243. }
  244. }).catch(function (error) {
  245. if (error.toString().indexOf('failed to find target with hash string') >= 0) {
  246. return check_reqs.check_android_target(error).then(function () {
  247. // If due to some odd reason - check_android_target succeeds
  248. // we should still fail here.
  249. return Q.reject(error);
  250. });
  251. }
  252. return Q.reject(error);
  253. });
  254. }
  255. clean (opts) {
  256. var builder = this;
  257. var wrapper = path.join(this.root, 'gradlew');
  258. var args = builder.getArgs('clean', opts);
  259. return Q().then(function () {
  260. return spawn(wrapper, args, { stdio: 'inherit' });
  261. })
  262. .then(function () {
  263. shell.rm('-rf', path.join(builder.root, 'out'));
  264. ['debug', 'release'].forEach(function (config) {
  265. var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES);
  266. if (isAutoGenerated(propertiesFilePath)) {
  267. shell.rm('-f', propertiesFilePath);
  268. }
  269. });
  270. });
  271. }
  272. findOutputApks (build_type, arch) {
  273. return findOutputApksHelper(this.apkDir, build_type, arch).sort(apkSorter);
  274. }
  275. findOutputBundles (build_type) {
  276. return findOutputBundlesHelper(this.aabDir, build_type);
  277. }
  278. fetchBuildResults (build_type, arch) {
  279. return {
  280. apkPaths: this.findOutputApks(build_type, arch),
  281. buildType: build_type
  282. };
  283. }
  284. }
  285. module.exports = ProjectBuilder;
  286. const apkSorter = compareFunc([
  287. // Sort arch specific builds after generic ones
  288. apkPath => /-x86|-arm/.test(apkPath),
  289. // Sort unsigned builds after signed ones
  290. apkPath => /-unsigned/.test(apkPath),
  291. // Sort by file modification time, latest first
  292. apkPath => -fs.statSync(apkPath).mtime.getTime(),
  293. // Sort by file name length, ascending
  294. 'length'
  295. ]);
  296. function findOutputApksHelper (dir, build_type, arch) {
  297. var shellSilent = shell.config.silent;
  298. shell.config.silent = true;
  299. // list directory recursively
  300. var ret = shell.ls('-R', dir).map(function (file) {
  301. // ls does not include base directory
  302. return path.join(dir, file);
  303. }).filter(function (file) {
  304. // find all APKs
  305. return file.match(/\.apk?$/i);
  306. }).filter(function (candidate) {
  307. var apkName = path.basename(candidate);
  308. // Need to choose between release and debug .apk.
  309. if (build_type === 'debug') {
  310. return /-debug/.exec(apkName) && !/-unaligned|-unsigned/.exec(apkName);
  311. }
  312. if (build_type === 'release') {
  313. return /-release/.exec(apkName) && !/-unaligned/.exec(apkName);
  314. }
  315. return true;
  316. }).sort(apkSorter);
  317. shellSilent = shellSilent;
  318. if (ret.length === 0) {
  319. return ret;
  320. }
  321. // Assume arch-specific build if newest apk has -x86 or -arm.
  322. var archSpecific = !!/-x86|-arm/.exec(path.basename(ret[0]));
  323. // And show only arch-specific ones (or non-arch-specific)
  324. ret = ret.filter(function (p) {
  325. return !!/-x86|-arm/.exec(path.basename(p)) === archSpecific;
  326. });
  327. if (archSpecific && ret.length > 1 && arch) {
  328. ret = ret.filter(function (p) {
  329. return path.basename(p).indexOf('-' + arch) !== -1;
  330. });
  331. }
  332. return ret;
  333. }
  334. // This method was a copy of findOutputApksHelper and modified to look for bundles
  335. // While replacing shell with fs-extra, it might be a good idea to see if we can
  336. // generalise these findOutput methods.
  337. function findOutputBundlesHelper (dir, build_type) {
  338. // This is an unused variable that was copied from findOutputApksHelper
  339. // we are pretty sure it was meant to reset shell.config.silent back to
  340. // the original value. However shell is planned to be replaced,
  341. // it was left as is to avoid unintended consequences.
  342. const shellSilent = shell.config.silent;
  343. shell.config.silent = true;
  344. // list directory recursively
  345. const ret = shell.ls('-R', dir).map(function (file) {
  346. return path.join(dir, file); // ls does not include base directory
  347. }).filter(function (file) {
  348. return file.match(/\.aab?$/i); // find all bundles
  349. }).filter(function (candidate) {
  350. // Need to choose between release and debug bundle.
  351. if (build_type === 'debug') {
  352. return /debug/.exec(candidate);
  353. }
  354. if (build_type === 'release') {
  355. return /release/.exec(candidate);
  356. }
  357. return true;
  358. });
  359. return ret;
  360. }
  361. function isAutoGenerated (file) {
  362. return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0;
  363. }