2015-10-21 07:15:57 +08:00
#!/usr/bin/env node
/ *
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 .
* /
2017-06-14 02:42:20 +08:00
var path = require ( 'path' ) ;
var fs = require ( 'fs' ) ;
var nopt = require ( 'nopt' ) ;
2015-10-21 07:15:57 +08:00
var Adb = require ( './Adb' ) ;
var builders = require ( './builders/builders' ) ;
var events = require ( 'cordova-common' ) . events ;
2020-01-07 06:15:22 +08:00
const execa = require ( 'execa' ) ;
2015-10-21 07:15:57 +08:00
var CordovaError = require ( 'cordova-common' ) . CordovaError ;
2019-08-09 00:53:10 +08:00
var PackageType = require ( './PackageType' ) ;
2015-10-21 07:15:57 +08:00
2018-09-06 10:06:18 +08:00
module . exports . parseBuildOptions = parseOpts ;
2017-06-14 02:42:20 +08:00
function parseOpts ( options , resolvedTarget , projectRoot ) {
2015-10-21 07:15:57 +08:00
options = options || { } ;
options . argv = nopt ( {
prepenv : Boolean ,
versionCode : String ,
minSdkVersion : String ,
2019-04-06 12:28:25 +08:00
maxSdkVersion : String ,
targetSdkVersion : String ,
2016-03-25 18:31:41 +08:00
gradleArg : [ String , Array ] ,
2015-10-21 07:15:57 +08:00
keystore : path ,
alias : String ,
storePassword : String ,
password : String ,
2019-08-09 00:53:10 +08:00
keystoreType : String ,
packageType : String
2015-10-21 07:15:57 +08:00
} , { } , options . argv , 0 ) ;
2017-11-23 03:39:40 +08:00
// Android Studio Build method is the default
2015-10-21 07:15:57 +08:00
var ret = {
buildType : options . release ? 'release' : 'debug' ,
prepEnv : options . argv . prepenv ,
arch : resolvedTarget && resolvedTarget . arch ,
extraArgs : [ ]
} ;
2017-06-14 02:42:20 +08:00
if ( options . argv . versionCode ) { ret . extraArgs . push ( '-PcdvVersionCode=' + options . argv . versionCode ) ; }
if ( options . argv . minSdkVersion ) { ret . extraArgs . push ( '-PcdvMinSdkVersion=' + options . argv . minSdkVersion ) ; }
2019-04-06 12:28:25 +08:00
if ( options . argv . maxSdkVersion ) { ret . extraArgs . push ( '-PcdvMaxSdkVersion=' + options . argv . maxSdkVersion ) ; }
if ( options . argv . targetSdkVersion ) { ret . extraArgs . push ( '-PcdvTargetSdkVersion=' + options . argv . targetSdkVersion ) ; }
2016-03-25 18:31:41 +08:00
if ( options . argv . gradleArg ) {
ret . extraArgs = ret . extraArgs . concat ( options . argv . gradleArg ) ;
}
2015-10-21 07:15:57 +08:00
var packageArgs = { } ;
2017-06-14 02:42:20 +08:00
if ( options . argv . keystore ) { packageArgs . keystore = path . relative ( projectRoot , path . resolve ( options . argv . keystore ) ) ; }
2015-10-21 07:15:57 +08:00
2019-08-09 00:53:10 +08:00
[ 'alias' , 'storePassword' , 'password' , 'keystoreType' , 'packageType' ] . forEach ( function ( flagName ) {
2017-06-14 02:42:20 +08:00
if ( options . argv [ flagName ] ) { packageArgs [ flagName ] = options . argv [ flagName ] ; }
2015-10-21 07:15:57 +08:00
} ) ;
var buildConfig = options . buildConfig ;
// If some values are not specified as command line arguments - use build config to supplement them.
2019-08-09 00:53:10 +08:00
// Command line arguments have precedence over build config.
2015-10-21 07:15:57 +08:00
if ( buildConfig ) {
if ( ! fs . existsSync ( buildConfig ) ) {
throw new Error ( 'Specified build config file does not exist: ' + buildConfig ) ;
}
2017-06-14 02:42:20 +08:00
events . emit ( 'log' , 'Reading build config file: ' + path . resolve ( buildConfig ) ) ;
2016-01-12 07:53:00 +08:00
var buildjson = fs . readFileSync ( buildConfig , 'utf8' ) ;
2016-04-06 21:36:43 +08:00
var config = JSON . parse ( buildjson . replace ( /^\ufeff/ , '' ) ) ; // Remove BOM
2015-10-21 07:15:57 +08:00
if ( config . android && config . android [ ret . buildType ] ) {
var androidInfo = config . android [ ret . buildType ] ;
2017-06-14 02:42:20 +08:00
if ( androidInfo . keystore && ! packageArgs . keystore ) {
if ( androidInfo . keystore . substr ( 0 , 1 ) === '~' ) {
2016-01-20 04:01:44 +08:00
androidInfo . keystore = process . env . HOME + androidInfo . keystore . substr ( 1 ) ;
}
2015-10-21 07:15:57 +08:00
packageArgs . keystore = path . resolve ( path . dirname ( buildConfig ) , androidInfo . keystore ) ;
2016-01-12 07:53:00 +08:00
events . emit ( 'log' , 'Reading the keystore from: ' + packageArgs . keystore ) ;
2015-10-21 07:15:57 +08:00
}
2019-08-09 00:53:10 +08:00
[ 'alias' , 'storePassword' , 'password' , 'keystoreType' , 'packageType' ] . forEach ( function ( key ) {
2015-10-21 07:15:57 +08:00
packageArgs [ key ] = packageArgs [ key ] || androidInfo [ key ] ;
} ) ;
}
}
if ( packageArgs . keystore && packageArgs . alias ) {
ret . packageInfo = new PackageInfo ( packageArgs . keystore , packageArgs . alias , packageArgs . storePassword ,
packageArgs . password , packageArgs . keystoreType ) ;
}
2017-06-14 02:42:20 +08:00
if ( ! ret . packageInfo ) {
2019-08-09 00:53:10 +08:00
// The following loop is to decide whether to print a warning about generating a signed archive
// We only want to produce a warning if they are using a config property that is related to signing, but
// missing the required properties for signing. We don't want to produce a warning if they are simply
// using a build property that isn't related to signing, such as --packageType
let shouldWarn = false ;
const signingKeys = [ 'keystore' , 'alias' , 'storePassword' , 'password' , 'keystoreType' ] ;
for ( let key in packageArgs ) {
if ( ! shouldWarn && signingKeys . indexOf ( key ) > - 1 ) {
// If we enter this condition, we have a key used for signing a build,
// but we are missing some required signing properties
shouldWarn = true ;
}
}
if ( shouldWarn ) {
2015-10-21 07:15:57 +08:00
events . emit ( 'warn' , '\'keystore\' and \'alias\' need to be specified to generate a signed archive.' ) ;
}
}
2019-08-09 00:53:10 +08:00
if ( packageArgs . packageType ) {
const VALID _PACKAGE _TYPES = [ PackageType . APK , PackageType . BUNDLE ] ;
if ( VALID _PACKAGE _TYPES . indexOf ( packageArgs . packageType ) === - 1 ) {
events . emit ( 'warn' , '"' + packageArgs . packageType + '" is an invalid packageType. Valid values are: ' + VALID _PACKAGE _TYPES . join ( ', ' ) + '\nDefaulting packageType to ' + PackageType . APK ) ;
ret . packageType = PackageType . APK ;
} else {
ret . packageType = packageArgs . packageType ;
}
} else {
ret . packageType = PackageType . APK ;
}
2015-10-21 07:15:57 +08:00
return ret ;
}
/ *
* Builds the project with the specifed options
* Returns a promise .
* /
2017-06-14 02:42:20 +08:00
module . exports . runClean = function ( options ) {
2016-01-28 15:50:59 +08:00
var opts = parseOpts ( options , null , this . root ) ;
2018-09-06 10:06:18 +08:00
var builder = builders . getBuilder ( ) ;
2017-06-14 02:42:20 +08:00
return builder . prepEnv ( opts ) . then ( function ( ) {
2015-10-21 07:15:57 +08:00
return builder . clean ( opts ) ;
} ) ;
} ;
/ * *
* Builds the project with the specifed options .
*
* @ param { BuildOptions } options A set of options . See PlatformApi . build
* method documentation for reference .
* @ param { Object } optResolvedTarget A deployment target . Used to pass
* target architecture from upstream 'run' call . TODO : remove this option in
* favor of setting buildOptions . archs field .
*
* @ return { Promise < Object > } Promise , resolved with built packages
* information .
* /
2017-06-14 02:42:20 +08:00
module . exports . run = function ( options , optResolvedTarget ) {
2016-01-28 15:50:59 +08:00
var opts = parseOpts ( options , optResolvedTarget , this . root ) ;
2018-09-06 10:06:18 +08:00
var builder = builders . getBuilder ( ) ;
2017-06-14 02:42:20 +08:00
return builder . prepEnv ( opts ) . then ( function ( ) {
2015-10-21 07:15:57 +08:00
if ( opts . prepEnv ) {
2016-03-09 21:56:47 +08:00
events . emit ( 'verbose' , 'Build file successfully prepared.' ) ;
2015-10-21 07:15:57 +08:00
return ;
}
2017-06-14 02:42:20 +08:00
return builder . build ( opts ) . then ( function ( ) {
2019-08-09 00:53:10 +08:00
var paths ;
if ( opts . packageType === PackageType . BUNDLE ) {
paths = builder . findOutputBundles ( opts . buildType ) ;
events . emit ( 'log' , 'Built the following bundle(s): \n\t' + paths . join ( '\n\t' ) ) ;
} else {
paths = builder . findOutputApks ( opts . buildType , opts . arch ) ;
events . emit ( 'log' , 'Built the following apk(s): \n\t' + paths . join ( '\n\t' ) ) ;
}
2015-10-21 07:15:57 +08:00
return {
2019-08-09 00:53:10 +08:00
paths : paths ,
2018-09-06 10:06:18 +08:00
buildType : opts . buildType
2015-10-21 07:15:57 +08:00
} ;
} ) ;
} ) ;
} ;
/ *
* Detects the architecture of a device / emulator
* Returns "arm" or "x86" .
* /
2017-06-14 02:42:20 +08:00
module . exports . detectArchitecture = function ( target ) {
function helper ( ) {
return Adb . shell ( target , 'cat /proc/cpuinfo' ) . then ( function ( output ) {
2015-10-21 07:15:57 +08:00
return /intel/i . exec ( output ) ? 'x86' : 'arm' ;
} ) ;
}
2020-01-07 20:22:59 +08:00
function timeout ( ms , err ) {
return new Promise ( ( resolve , reject ) => {
setTimeout ( ( ) => reject ( err ) , ms ) ;
} ) ;
}
2015-10-21 07:15:57 +08:00
// It sometimes happens (at least on OS X), that this command will hang forever.
// To fix it, either unplug & replug device, or restart adb server.
2020-01-07 20:22:59 +08:00
return Promise . race ( [
helper ( ) ,
timeout ( 1000 , new CordovaError (
'Device communication timed out. Try unplugging & replugging the device.'
) )
] ) . catch ( err => {
2015-10-21 07:15:57 +08:00
if ( /timed out/ . exec ( '' + err ) ) {
// adb kill-server doesn't seem to do the trick.
// Could probably find a x-platform version of killall, but I'm not actually
// sure that this scenario even happens on non-OSX machines.
2016-05-04 07:25:48 +08:00
events . emit ( 'verbose' , 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.' ) ;
2020-01-07 06:15:22 +08:00
return execa ( 'killall' , [ 'adb' ] ) . then ( function ( ) {
2017-06-14 02:42:20 +08:00
return helper ( ) . then ( null , function ( ) {
2015-10-21 07:15:57 +08:00
// The double kill is sadly often necessary, at least on mac.
2016-05-04 07:25:48 +08:00
events . emit ( 'warn' , 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.' ) ;
2020-01-07 06:15:22 +08:00
return execa ( 'killall' , [ 'adb' ] ) . then ( function ( ) {
2017-06-14 02:42:20 +08:00
return helper ( ) . then ( null , function ( ) {
2020-01-07 20:22:59 +08:00
return Promise . reject ( new CordovaError ( 'adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.' ) ) ;
2015-10-21 07:15:57 +08:00
} ) ;
} ) ;
} ) ;
2017-06-14 02:42:20 +08:00
} , function ( ) {
2015-10-21 07:15:57 +08:00
// For non-killall OS's.
2020-01-07 20:22:59 +08:00
return Promise . reject ( err ) ;
2015-10-21 07:15:57 +08:00
} ) ;
}
throw err ;
} ) ;
} ;
2017-06-14 02:42:20 +08:00
module . exports . findBestApkForArchitecture = function ( buildResults , arch ) {
var paths = buildResults . apkPaths . filter ( function ( p ) {
2015-10-21 07:15:57 +08:00
var apkName = path . basename ( p ) ;
2017-06-14 02:42:20 +08:00
if ( buildResults . buildType === 'debug' ) {
2015-10-21 07:15:57 +08:00
return /-debug/ . exec ( apkName ) ;
}
return ! /-debug/ . exec ( apkName ) ;
} ) ;
var archPattern = new RegExp ( '-' + arch ) ;
var hasArchPattern = /-x86|-arm/ ;
for ( var i = 0 ; i < paths . length ; ++ i ) {
var apkName = path . basename ( paths [ i ] ) ;
if ( hasArchPattern . exec ( apkName ) ) {
if ( archPattern . exec ( apkName ) ) {
return paths [ i ] ;
}
} else {
return paths [ i ] ;
}
}
throw new Error ( 'Could not find apk architecture: ' + arch + ' build-type: ' + buildResults . buildType ) ;
} ;
2017-06-14 02:42:20 +08:00
function PackageInfo ( keystore , alias , storePassword , password , keystoreType ) {
2020-01-29 09:12:55 +08:00
const createNameKeyObject = ( name , value ) => ( { name , value : value . replace ( /\\/g , '\\\\' ) } ) ;
this . data = [
createNameKeyObject ( 'key.store' , keystore ) ,
createNameKeyObject ( 'key.alias' , alias )
] ;
if ( storePassword ) this . data . push ( createNameKeyObject ( 'key.store.password' , storePassword ) ) ;
if ( password ) this . data . push ( createNameKeyObject ( 'key.alias.password' , password ) ) ;
if ( keystoreType ) this . data . push ( createNameKeyObject ( 'key.store.type' , keystoreType ) ) ;
2015-10-21 07:15:57 +08:00
}
PackageInfo . prototype = {
2020-01-29 09:12:55 +08:00
appendToProperties : function ( propertiesParser ) {
for ( const { name , value } of this . data ) propertiesParser . set ( name , value ) ;
propertiesParser . save ( ) ;
2015-10-21 07:15:57 +08:00
}
} ;
2017-06-14 02:42:20 +08:00
module . exports . help = function ( ) {
2015-10-21 07:15:57 +08:00
console . log ( 'Usage: ' + path . relative ( process . cwd ( ) , path . join ( '../build' ) ) + ' [flags] [Signed APK flags]' ) ;
console . log ( 'Flags:' ) ;
console . log ( ' \'--debug\': will build project in debug mode (default)' ) ;
console . log ( ' \'--release\': will build project for release' ) ;
console . log ( ' \'--nobuild\': will skip build process (useful when using run command)' ) ;
console . log ( ' \'--prepenv\': don\'t build, but copy in build scripts where necessary' ) ;
2018-09-06 10:06:18 +08:00
console . log ( ' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs.' ) ;
2019-04-06 12:28:25 +08:00
console . log ( ' \'--minSdkVersion=#\': Override minSdkVersion for this build.' ) ;
console . log ( ' \'--maxSdkVersion=#\': Override maxSdkVersion for this build. (Not Recommended)' ) ;
console . log ( ' \'--targetSdkVersion=#\': Override targetSdkVersion for this build.' ) ;
2015-10-21 07:15:57 +08:00
console . log ( ' \'--gradleArg=<gradle command line arg>\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true' ) ;
2019-08-09 00:53:10 +08:00
console . log ( ' \'--packageType=<apk|bundle>\': Builds an APK or a bundle' ) ;
2015-10-21 07:15:57 +08:00
console . log ( '' ) ;
console . log ( 'Signed APK flags (overwrites debug/release-signing.proprties) :' ) ;
console . log ( ' \'--keystore=<path to keystore>\': Key store used to build a signed archive. (Required)' ) ;
console . log ( ' \'--alias=\': Alias for the key store. (Required)' ) ;
console . log ( ' \'--storePassword=\': Password for the key store. (Optional - prompted)' ) ;
console . log ( ' \'--password=\': Password for the key. (Optional - prompted)' ) ;
console . log ( ' \'--keystoreType\': Type of the keystore. (Optional)' ) ;
process . exit ( 0 ) ;
} ;