2017-03-31 03:41:44 +08:00
/ *
Licensed to the Apache Software Foundation ( ASF ) under one
or more contributor license agreements . See the NOTICE file
distributed with this work for additional information
regarding copyright ownership . The ASF licenses this file
to you under the Apache License , Version 2.0 ( the
"License" ) ; you may not use this file except in compliance
with the License . You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing ,
software distributed under the License is distributed on an
"AS IS" BASIS , WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND , either express or implied . See the License for the
specific language governing permissions and limitations
under the License .
* /
2025-01-29 09:39:11 +08:00
const fs = require ( 'node:fs' ) ;
2025-01-28 11:13:36 +08:00
const path = require ( 'node:path' ) ;
2020-01-07 06:15:22 +08:00
const execa = require ( 'execa' ) ;
2020-11-21 17:44:56 +08:00
const glob = require ( 'fast-glob' ) ;
2022-04-18 09:39:54 +08:00
const events = require ( 'cordova-common' ) . events ;
const CordovaError = require ( 'cordova-common' ) . CordovaError ;
const check _reqs = require ( '../check_reqs' ) ;
const PackageType = require ( '../PackageType' ) ;
2024-05-13 21:28:57 +08:00
const { compareByAll , isWindows } = require ( '../utils' ) ;
2020-01-29 09:12:55 +08:00
const { createEditor } = require ( 'properties-parser' ) ;
2023-04-12 13:39:47 +08:00
const CordovaGradleConfigParserFactory = require ( '../config/CordovaGradleConfigParserFactory' ) ;
2017-03-31 03:41:44 +08:00
2018-06-28 07:53:36 +08:00
const MARKER = 'YOUR CHANGES WILL BE ERASED!' ;
const SIGNING _PROPERTIES = '-signing.properties' ;
const TEMPLATE =
2017-03-31 03:41:44 +08:00
'# This file is automatically generated.\n' +
'# Do not modify this file -- ' + MARKER + '\n' ;
2020-11-21 17:44:56 +08:00
const isPathArchSpecific = p => / - x86 | - arm / . test ( path . basename ( p ) ) ;
2020-01-29 09:12:55 +08:00
2020-11-21 17:44:56 +08:00
const outputFileComparator = compareByAll ( [
// Sort arch specific builds after generic ones
isPathArchSpecific ,
2020-01-29 09:12:55 +08:00
2020-11-21 17:44:56 +08:00
// Sort unsigned builds after signed ones
filePath => / - unsigned / . test ( path . basename ( filePath ) ) ,
2020-04-01 11:43:36 +08:00
2020-11-21 17:44:56 +08:00
// Sort by file modification time, latest first
filePath => - fs . statSync ( filePath ) . mtime . getTime ( ) ,
2020-01-29 09:12:55 +08:00
2020-11-21 17:44:56 +08:00
// Sort by file name length, ascending
filePath => filePath . length
] ) ;
2020-01-29 09:12:55 +08:00
/ * *
2020-11-21 17:44:56 +08:00
* @ param { 'apk' | 'aab' } bundleType
* @ param { 'debug' | 'release' } buildType
* @ param { { arch ? : string } } options
2020-01-29 09:12:55 +08:00
* /
2021-01-20 09:33:06 +08:00
function findOutputFiles ( bundleType , buildType , { arch } = { } ) {
2020-11-21 17:44:56 +08:00
let files = glob . sync ( ` **/*. ${ bundleType } ` , {
absolute : true ,
cwd : path . resolve ( this [ ` ${ bundleType } Dir ` ] , buildType )
} ) . map ( path . normalize ) ;
2020-01-29 09:12:55 +08:00
if ( files . length === 0 ) return files ;
// Assume arch-specific build if newest apk has -x86 or -arm.
2020-11-21 17:44:56 +08:00
const archSpecific = isPathArchSpecific ( files [ 0 ] ) ;
2020-01-29 09:12:55 +08:00
// And show only arch-specific ones (or non-arch-specific)
2020-11-21 17:44:56 +08:00
files = files . filter ( p => isPathArchSpecific ( p ) === archSpecific ) ;
2020-01-29 09:12:55 +08:00
if ( archSpecific && files . length > 1 && arch ) {
2020-11-21 17:44:56 +08:00
files = files . filter ( p => path . basename ( p ) . includes ( '-' + arch ) ) ;
2020-01-29 09:12:55 +08:00
}
2020-11-21 17:44:56 +08:00
return files . sort ( outputFileComparator ) ;
2020-01-29 09:12:55 +08:00
}
2018-06-29 08:16:57 +08:00
class ProjectBuilder {
2018-09-06 10:06:18 +08:00
constructor ( rootDirectory ) {
2021-07-13 14:51:20 +08:00
this . root = rootDirectory ;
2019-08-09 00:53:10 +08:00
this . apkDir = path . join ( this . root , 'app' , 'build' , 'outputs' , 'apk' ) ;
this . aabDir = path . join ( this . root , 'app' , 'build' , 'outputs' , 'bundle' ) ;
2017-03-31 03:41:44 +08:00
}
2018-06-29 08:16:57 +08:00
getArgs ( cmd , opts ) {
2024-05-13 21:28:57 +08:00
let args = [ ] ;
2023-04-23 04:00:51 +08:00
if ( opts . extraArgs ) {
args = args . concat ( opts . extraArgs ) ;
}
2019-09-07 12:54:33 +08:00
let buildCmd = cmd ;
2019-08-09 00:53:10 +08:00
if ( opts . packageType === PackageType . BUNDLE ) {
if ( cmd === 'release' ) {
buildCmd = ':app:bundleRelease' ;
} else if ( cmd === 'debug' ) {
buildCmd = ':app:bundleDebug' ;
}
} else {
if ( cmd === 'release' ) {
buildCmd = 'cdvBuildRelease' ;
} else if ( cmd === 'debug' ) {
buildCmd = 'cdvBuildDebug' ;
}
2018-07-11 11:14:04 +08:00
2019-08-09 00:53:10 +08:00
if ( opts . arch ) {
args . push ( '-PcdvBuildArch=' + opts . arch ) ;
}
}
2018-07-11 11:14:04 +08:00
2023-04-23 04:00:51 +08:00
args . push ( buildCmd ) ;
2020-04-01 12:59:39 +08:00
2018-06-29 08:16:57 +08:00
return args ;
2017-03-31 03:41:44 +08:00
}
2024-05-13 21:28:57 +08:00
getGradleWrapperPath ( ) {
let wrapper = path . join ( this . root , 'tools' , 'gradlew' ) ;
if ( isWindows ( ) ) {
wrapper += '.bat' ;
}
return wrapper ;
}
/ * *
* Installs / updates the gradle wrapper
* @ param { string } gradleVersion The gradle version to install . Ignored if CORDOVA _ANDROID _GRADLE _DISTRIBUTION _URL environment variable is defined
* @ returns { Promise < void > }
* /
async installGradleWrapper ( gradleVersion ) {
if ( process . env . CORDOVA _ANDROID _GRADLE _DISTRIBUTION _URL ) {
events . emit ( 'verbose' , ` Overriding Gradle Version via CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL ( ${ process . env . CORDOVA _ANDROID _GRADLE _DISTRIBUTION _URL } ) ` ) ;
await execa ( 'gradle' , [ '-p' , path . join ( this . root , 'tools' ) , 'wrapper' , '--gradle-distribution-url' , process . env . CORDOVA _ANDROID _GRADLE _DISTRIBUTION _URL ] , { stdio : 'inherit' } ) ;
2018-06-29 08:16:57 +08:00
} else {
2024-05-13 21:28:57 +08:00
await execa ( 'gradle' , [ '-p' , path . join ( this . root , 'tools' ) , 'wrapper' , '--gradle-version' , gradleVersion ] , { stdio : 'inherit' } ) ;
2017-03-31 04:38:18 +08:00
}
}
2018-06-29 08:16:57 +08:00
readProjectProperties ( ) {
function findAllUniq ( data , r ) {
2022-04-18 09:39:54 +08:00
const s = { } ;
let m ;
2018-06-29 08:16:57 +08:00
while ( ( m = r . exec ( data ) ) ) {
s [ m [ 1 ] ] = 1 ;
}
return Object . keys ( s ) ;
}
2017-03-31 04:38:18 +08:00
2022-04-18 09:39:54 +08:00
const data = fs . readFileSync ( path . join ( this . root , 'project.properties' ) , 'utf8' ) ;
2018-06-29 08:16:57 +08:00
return {
libs : findAllUniq ( data , /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg ) ,
gradleIncludes : findAllUniq ( data , /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg ) ,
2022-05-18 12:10:15 +08:00
systemLibs : findAllUniq ( data , /^\s*cordova\.system\.library\.\d+=((?!.*\().*)(?:\s|$)/mg ) ,
bomPlatforms : findAllUniq ( data , /^\s*cordova\.system\.library\.\d+=platform\((?:'|")(.*)(?:'|")\)/mg )
2018-06-29 08:16:57 +08:00
} ;
2017-03-31 04:38:18 +08:00
}
2018-06-29 08:16:57 +08:00
// Makes the project buildable, minus the gradle wrapper.
prepBuildFiles ( ) {
// Update the version of build.gradle in each dependent library.
2022-04-18 09:39:54 +08:00
const pluginBuildGradle = path . join ( _ _dirname , 'plugin-build.gradle' ) ;
const propertiesObj = this . readProjectProperties ( ) ;
const subProjects = propertiesObj . libs ;
2017-04-12 04:47:40 +08:00
2018-06-29 08:16:57 +08:00
// Check and copy the gradle file into the subproject
// Called by the loop before this function def
2017-11-02 04:22:22 +08:00
2022-04-18 09:39:54 +08:00
const checkAndCopy = function ( subProject , root ) {
const subProjectGradle = path . join ( root , subProject , 'build.gradle' ) ;
2018-06-29 08:16:57 +08:00
// This is the future-proof way of checking if a file exists
// This must be synchronous to satisfy a Travis test
try {
fs . accessSync ( subProjectGradle , fs . F _OK ) ;
} catch ( e ) {
2025-01-29 09:39:11 +08:00
fs . cpSync ( pluginBuildGradle , subProjectGradle ) ;
2017-03-31 03:41:44 +08:00
}
2018-06-29 08:16:57 +08:00
} ;
2022-04-18 09:39:54 +08:00
for ( let i = 0 ; i < subProjects . length ; ++ i ) {
2018-06-29 08:16:57 +08:00
if ( subProjects [ i ] !== 'CordovaLib' ) {
checkAndCopy ( subProjects [ i ] , this . root ) ;
2017-03-31 03:41:44 +08:00
}
}
2023-04-12 13:39:47 +08:00
// get project name cdv-gradle-config.
const cdvGradleConfig = CordovaGradleConfigParserFactory . create ( this . root ) ;
const projectName = cdvGradleConfig . getProjectNameFromPackageName ( ) ;
2018-06-29 08:16:57 +08:00
// Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
2022-04-18 09:39:54 +08:00
const settingsGradlePaths = subProjects . map ( function ( p ) {
const realDir = p . replace ( /[/\\]/g , ':' ) ;
const libName = realDir . replace ( projectName + '-' , '' ) ;
let str = 'include ":' + libName + '"\n' ;
2021-08-13 11:08:18 +08:00
if ( realDir . indexOf ( projectName + '-' ) !== - 1 ) {
2018-06-29 08:16:57 +08:00
str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n' ;
2018-03-20 04:20:09 +08:00
}
2018-06-29 08:16:57 +08:00
return str ;
2018-03-20 04:20:09 +08:00
} ) ;
2017-03-31 03:41:44 +08:00
2021-08-13 11:08:18 +08:00
// Update subprojects within settings.gradle.
2018-06-29 08:16:57 +08:00
fs . writeFileSync ( path . join ( this . root , 'settings.gradle' ) ,
'// GENERATED FILE - DO NOT EDIT\n' +
2021-08-13 11:08:18 +08:00
'apply from: "cdv-gradle-name.gradle"\n' +
'include ":"\n' +
settingsGradlePaths . join ( '' ) ) ;
// Touch empty cdv-gradle-name.gradle file if missing.
2025-01-29 09:39:11 +08:00
if ( ! fs . existsSync ( path . join ( this . root , 'cdv-gradle-name.gradle' ) ) ) {
2021-08-13 11:08:18 +08:00
fs . writeFileSync ( path . join ( this . root , 'cdv-gradle-name.gradle' ) , '' ) ;
}
2017-03-31 03:41:44 +08:00
2018-06-29 08:16:57 +08:00
// Update dependencies within build.gradle.
2022-04-18 09:39:54 +08:00
let buildGradle = fs . readFileSync ( path . join ( this . root , 'app' , 'build.gradle' ) , 'utf8' ) ;
let depsList = '' ;
const root = this . root ;
const insertExclude = function ( p ) {
const gradlePath = path . join ( root , p , 'build.gradle' ) ;
const projectGradleFile = fs . readFileSync ( gradlePath , 'utf-8' ) ;
2018-06-29 08:16:57 +08:00
if ( projectGradleFile . indexOf ( 'CordovaLib' ) !== - 1 ) {
depsList += '{\n exclude module:("CordovaLib")\n }\n' ;
2018-03-20 04:20:09 +08:00
} else {
2018-06-29 08:16:57 +08:00
depsList += '\n' ;
2017-03-31 03:41:44 +08:00
}
2018-06-29 08:16:57 +08:00
} ;
subProjects . forEach ( function ( p ) {
events . emit ( 'log' , 'Subproject Path: ' + p ) ;
2022-04-18 09:39:54 +08:00
const libName = p . replace ( /[/\\]/g , ':' ) . replace ( projectName + '-' , '' ) ;
2018-06-29 08:16:57 +08:00
if ( libName !== 'app' ) {
depsList += ' implementation(project(path: ":' + libName + '"))' ;
insertExclude ( p ) ;
}
} ) ;
// For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
2022-04-18 09:39:54 +08:00
const SYSTEM _LIBRARY _MAPPINGS = [
2018-06-29 08:16:57 +08:00
[ /^\/?extras\/android\/support\/(.*)$/ , 'com.android.support:support-$1:+' ] ,
[ /^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/ , 'com.google.android.gms:play-services:+' ]
] ;
2022-05-18 12:10:15 +08:00
propertiesObj . bomPlatforms . forEach ( function ( p ) {
if ( ! /:.*:/ . exec ( p ) ) {
throw new CordovaError ( 'Malformed BoM platform: ' + p ) ;
}
// Add bom platform
depsList += ' implementation platform("' + p + '")\n' ;
} ) ;
2018-06-29 08:16:57 +08:00
propertiesObj . systemLibs . forEach ( function ( p ) {
2022-04-18 09:39:54 +08:00
let mavenRef ;
2018-06-29 08:16:57 +08:00
// It's already in gradle form if it has two ':'s
if ( /:.*:/ . exec ( p ) ) {
mavenRef = p ;
2022-05-18 12:10:15 +08:00
} else if ( /:.*/ . exec ( p ) ) {
// Support BoM imports
mavenRef = p ;
events . emit ( 'warn' , 'Library expects a BoM package: ' + p ) ;
2018-06-29 08:16:57 +08:00
} else {
2022-04-18 09:39:54 +08:00
for ( let i = 0 ; i < SYSTEM _LIBRARY _MAPPINGS . length ; ++ i ) {
const pair = SYSTEM _LIBRARY _MAPPINGS [ i ] ;
2018-06-29 08:16:57 +08:00
if ( pair [ 0 ] . exec ( p ) ) {
mavenRef = p . replace ( pair [ 0 ] , pair [ 1 ] ) ;
break ;
}
}
if ( ! mavenRef ) {
throw new CordovaError ( 'Unsupported system library (does not work with gradle): ' + p ) ;
}
2018-03-20 04:20:09 +08:00
}
2018-09-07 01:24:12 +08:00
depsList += ' implementation "' + mavenRef + '"\n' ;
2018-03-20 04:20:09 +08:00
} ) ;
2017-03-31 03:41:44 +08:00
2018-06-29 08:16:57 +08:00
buildGradle = buildGradle . replace ( /(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/ , '$1\n' + depsList + ' $2' ) ;
2022-04-18 09:39:54 +08:00
let includeList = '' ;
2017-03-31 03:41:44 +08:00
2018-06-29 08:16:57 +08:00
propertiesObj . gradleIncludes . forEach ( function ( includePath ) {
includeList += 'apply from: "../' + includePath + '"\n' ;
} ) ;
buildGradle = buildGradle . replace ( /(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/ , '$1\n' + includeList + '$2' ) ;
// This needs to be stored in the app gradle, not the root grade
fs . writeFileSync ( path . join ( this . root , 'app' , 'build.gradle' ) , buildGradle ) ;
}
prepEnv ( opts ) {
2022-04-18 09:39:54 +08:00
const self = this ;
2024-05-13 21:28:57 +08:00
const config = this . _getCordovaConfig ( ) ;
2018-06-29 08:16:57 +08:00
return check _reqs . check _gradle ( )
2024-05-13 21:28:57 +08:00
. then ( function ( ) {
events . emit ( 'verbose' , ` Using Gradle: ${ config . GRADLE _VERSION } ` ) ;
return self . installGradleWrapper ( config . GRADLE _VERSION ) ;
2018-06-29 08:16:57 +08:00
} ) . then ( function ( ) {
return self . prepBuildFiles ( ) ;
2020-01-29 09:12:55 +08:00
} ) . then ( ( ) => {
const signingPropertiesPath = path . join ( self . root , ` ${ opts . buildType } ${ SIGNING _PROPERTIES } ` ) ;
2025-01-29 09:39:11 +08:00
if ( fs . existsSync ( signingPropertiesPath ) ) fs . rmSync ( signingPropertiesPath ) ;
2018-06-29 08:16:57 +08:00
if ( opts . packageInfo ) {
2020-01-29 09:12:55 +08:00
fs . ensureFileSync ( signingPropertiesPath ) ;
const signingProperties = createEditor ( signingPropertiesPath ) ;
signingProperties . addHeadComment ( TEMPLATE ) ;
opts . packageInfo . appendToProperties ( signingProperties ) ;
2018-03-20 04:20:09 +08:00
}
} ) ;
2018-06-29 08:16:57 +08:00
}
2017-03-31 03:41:44 +08:00
2021-07-06 14:38:28 +08:00
/ * *
* @ private
* @ returns The user defined configs
* /
_getCordovaConfig ( ) {
2025-01-29 09:39:11 +08:00
return JSON . parse ( fs . readFileSync ( path . join ( this . root , 'cdv-gradle-config.json' ) , 'utf-8' ) || '{}' ) ;
2021-07-06 14:38:28 +08:00
}
2018-06-29 08:16:57 +08:00
/ *
* Builds the project with gradle .
* Returns a promise .
* /
2021-07-06 19:33:26 +08:00
async build ( opts ) {
2024-05-13 21:28:57 +08:00
const wrapper = this . getGradleWrapperPath ( ) ;
2022-04-18 09:39:54 +08:00
const args = this . getArgs ( opts . buildType === 'debug' ? 'debug' : 'release' , opts ) ;
2018-06-29 08:16:57 +08:00
2023-04-23 04:00:51 +08:00
events . emit ( 'verbose' , ` Running Gradle Build: ${ wrapper } ${ args . join ( ' ' ) } ` ) ;
2021-07-06 19:33:26 +08:00
try {
return await execa ( wrapper , args , { stdio : 'inherit' , cwd : path . resolve ( this . root ) } ) ;
} catch ( error ) {
if ( error . toString ( ) . includes ( 'failed to find target with hash string' ) ) {
// Add hint from check_android_target to error message
try {
2021-07-13 14:51:20 +08:00
await check _reqs . check _android _target ( this . root ) ;
2021-07-06 19:33:26 +08:00
} catch ( checkAndroidTargetError ) {
error . message += '\n' + checkAndroidTargetError . message ;
2018-06-29 08:16:57 +08:00
}
2021-07-06 19:33:26 +08:00
}
throw error ;
}
2018-06-29 08:16:57 +08:00
}
clean ( opts ) {
2024-05-13 21:28:57 +08:00
const wrapper = this . getGradleWrapperPath ( ) ;
2020-01-29 09:12:55 +08:00
const args = this . getArgs ( 'clean' , opts ) ;
2020-07-05 22:19:56 +08:00
return execa ( wrapper , args , { stdio : 'inherit' , cwd : path . resolve ( this . root ) } )
2020-01-29 09:12:55 +08:00
. then ( ( ) => {
2025-01-29 09:39:11 +08:00
fs . rmSync ( path . join ( this . root , 'out' ) , { recursive : true , force : true } ) ;
2018-06-29 08:16:57 +08:00
2020-01-29 09:12:55 +08:00
[ 'debug' , 'release' ] . map ( config => path . join ( this . root , ` ${ config } ${ SIGNING _PROPERTIES } ` ) )
. forEach ( file => {
const hasFile = fs . existsSync ( file ) ;
const hasMarker = hasFile && fs . readFileSync ( file , 'utf8' )
. includes ( MARKER ) ;
2025-01-29 09:39:11 +08:00
if ( hasFile && hasMarker ) fs . rmSync ( file ) ;
2020-01-29 09:12:55 +08:00
} ) ;
2018-06-29 08:16:57 +08:00
} ) ;
}
findOutputApks ( build _type , arch ) {
2020-11-21 17:44:56 +08:00
return findOutputFiles . call ( this , 'apk' , build _type , { arch } ) ;
2019-08-09 00:53:10 +08:00
}
findOutputBundles ( build _type ) {
2020-11-21 17:44:56 +08:00
return findOutputFiles . call ( this , 'aab' , build _type ) ;
2018-09-06 10:06:18 +08:00
}
fetchBuildResults ( build _type , arch ) {
return {
apkPaths : this . findOutputApks ( build _type , arch ) ,
buildType : build _type
} ;
2018-06-29 08:16:57 +08:00
}
}
2018-06-28 07:53:36 +08:00
module . exports = ProjectBuilder ;