Execute .bat from Android gradle after APK build - android

I`m moving my project to Gradle build system. After APK build I need to sign it with manufacturer certificate.
How to execute .bat file by Gradle after APK was built?
task runSign(type:Exec) {
println "Sign apk..."
commandLine = ['cmd','/c','sign.bat']
}
I know just how to run .bat before build (but I need after):
preBuild.doLast {
runSign.execute()
}

I've found the solution.
Go to Run -> Edit Configurations...
Choose module where you want to run task after APK build. Add new configuration after "Gradle-aware Make".
Click on icon at picture below to choose module where task is implemented and write name of it.
After this steps your custom Gradle task will be executed after APK build.

I needed to perform something similar, but additionally to that I needed to know with which product favour was built, and with what configuration.
I've ended up adding following line into build.gradle:
android {
applicationVariants.all { variant -> variant.assemble.doLast { signAndInstall.execute() } }
...
And with following helper function:
//
// Returns array for CommandLine, path, variant (arm7), configuration (debug / release)
//
def getCommandLine(path)
{
String taskReqStr = getGradle().getStartParameter().getTaskRequests().toString()
Pattern pattern = Pattern.compile("(assemble|generate)(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(taskReqStr)
if (!matcher.find())
return [ path ]
String flavor = matcher.group(2).toLowerCase() + " " + matcher.group(3).toLowerCase()
return [ path, matcher.group(2).toLowerCase(), matcher.group(3).toLowerCase() ]
}
task signAndInstall(type: Exec) {
def batch = projectDir.toString() + '\\postbuild.bat'
commandLine = getCommandLine(batch)
}
With following postbuild.bat:
#echo off
rem echo %0 %*
if %1. == . exit /b 0
if %2. == . exit /b 0
set InPath=%~dp0build\outputs\apk\%1\%2\app-%1-%2.apk
set OutPath=%~dp0build\outputs\apk\app-%1-%2.apk
copy /y %InPath% %OutPath% 1>NUL
You can of course configure this batch to perform anything what you like, %1 receives your product favour (e.g. arm7, arm8, fat...), and %2 receives 'debug' or 'release' as configuration.

Related

Setting process.env variable from Android Studio flavors

I want to be able to access a variable in my metro.config.js.
On iOS this can be achieved by adding a User Defined variable in the Build Settings tab. This is accessible with process.env.MY_VAR. The value of MY_VAR can be changed depending on which target is being built.
How can I achieve the same for Android using product flavours?
Here is my current setup
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
}
paid {
dimension "apps"
}
}
Execution Steps
When running iOS, I can see from the logs that the project gets built before the Packager starts
info Found Xcode workspace "foo.xcworkspace"
info Building (using "xcodebuild -workspace foo.xcworkspace -configuration Debug -scheme foo -destination id=CE408502-CD8C-4467-AE3D-295081CAF132 -derivedDataPath build/foo") <-- Build
▸ Running script '[CP-User] Config codegen'
▸ Compiling ReactNativeConfig.m
▸ Building library libreact-native-config.a
▸ Running script 'Upload Debug Symbols to Sentry'
▸ Running script 'Start Packager <-- Metro starts here
However, the packager starts before my gradle build task executes
info Starting JS server... <-- Metro starts here
info Installing the app...
> Configure project :app
Reading env from: .env
> Task :app:installFreeDebug <-- Build
Add the following codes to your build.gradle file.
buildTypes.each {
it.buildConfigField 'String', 'KEY_STRING', '"Default_Value"'
it.buildConfigField 'int', 'KEY_INT', '0'
}
applicationVariants.all { variant ->
if (variant.productFlavors[0].ext.has("key_1")) {
buildConfigField('String', 'KEY_STRING', variant.productFlavors.get(0).ext.key_1)
}
if (variant.productFlavors[0].ext.has("key_2")) {
buildConfigField('int', 'KEY_INT', variant.productFlavors.get(0).ext.key_2)
}
}
and then add ext.key_1 = '"desired string value"' and ext.key_2 = 'desired integer value' to flavors like this
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
ext.key_1 = '"free_value"'
ext.key_2 = '0'
}
paid {
dimension "apps"
ext.key_1 = '"paid_value"'
ext.ket_2 = '100'
}
}
Now you will access to the values in your app by calling BuildConfig.KEY_STRING and BuildConfig.KEY_INT.
If you don't set value for key_1 in a flavor, then the default value will be "Default_Value".
Update
I'm ashamed to say my above answer is wrong because in this way you can access the BuildConfig in JS scope but as you mentioned in your question you need to access the configuration in Metro Bundler and metor.config.js. I added the console.log(process.env); at the beginning of metro.config.js file and figured out process.env in Metro Bundler is my OS (in my case Windows 10) environment variables. So it means if you add desired variables as OS environment variable, then you will access them in metro.config.js through process.env. I tried to add environment variables through build.gradle but it wasn't a good way because:
1. I couldn't find a way to add the environment variable in Gradle
2. If I could do step 1, then as you mentioned the Metro starts before android Build, so the configuration will be ready late and Metro can't access them.
So, I decided to solve this issue in another way. First, I changed the build.gradle to log the required variables during Build process.
flavorDimensions "apps"
productFlavors {
free {
dimension "apps"
// Add any configuration you need for free flavor
buildConfigField('String', 'KEY_STRING', '"free_value"')
}
paid {
dimension "apps"
// Add any configuration you need for paid flavor
buildConfigField('String', 'KEY_STRING', '"paid_value1"')
buildConfigField('int', 'KEY_INT', '100')
}
}
applicationVariants.all { variant ->
// Read all flavors configuration
variant.productFlavors.each { flavor ->
flavor.buildConfigFields.each { key, value ->
// Set configuration to variant to be accessible through BuildConfig
variant.buildConfigField(value.type, value.name, value.value)
// Log all configurations in output
println "[" + variant.name + "]---" + value.name + "=" + value.value
}
}
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
Then I wrote a bat file to run {variant}PreBundle task (For example freeDebugPreBundle) in Gradle. Then it will read the output and extract required variables and then set them as environment variables and finally call react-native run-android --variant {variant} command.
run-android.bat
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
:: Reading input arguments to extract desired variant
FOR %%a IN (%*) do (
set "arg=%%a"
if "!found!"=="true" (
set "variant=%%a"
GOTO:EndOfLoop
)
if "!arg!"=="--variant" set found=true
)
:EndOfLoop
:: User must provide variant because when you have flavors, then `debug` varinat does not exist anymore
if "!variant!"=="" (
echo You must provide --variant
GOTO:EOF
)
:: Creating a pttern to extract required variables
set search=[!variant!]---
:: Creating gradle task name based on input variant
set firstCharUpper=%variant:~0,1%
CALL :UpCase firstCharUpper
set taskName=build!firstCharUpper!%variant:~1%PreBundle
echo Running !taskName!
:: Creating an empty bat file to store environment variables and call later
echo. > custome_env.bat
:: Running gradle task and extracting required variables from output log
cd android
FOR /F "tokens=* USEBACKQ" %%F IN (`gradlew app:!taskName!`) DO (
SET "line=%%F"
SET "newLine=!line:%search%=!"
if not "!line!"=="!newLine!" (
echo !line!
set "keyValue=!line:%search%=!"
:: Adding environment variabel to custome_env.bat file
echo set !keyValue!>>../custome_env.bat
)
)
ENDLOCAL
:: Running custome_env.bat file to set required variables as environment variable
call custome_env.bat
:: Calling react-native command with all input parameters (%*)
call react-native run-android %*
GOTO:EOF
#echo on
:UpCase
:: Subroutine to convert a variable VALUE to all UPPER CASE.
:: The argument for this subroutine is the variable NAME.
FOR %%i IN ("a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I" "j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R" "s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z") DO CALL SET "%1=%%%1:%%~i%%"
GOTO:EOF
For running the application you should run this command:
run-android.bat --variant freeDebug
You can use other react-native options as well but you must provide --variant because when you have flavors, default variant debug does not exist.
PS: I don't know how is your release proceseduar, so I'm not sure this answer is compatible with your release procedure or not. if I know it maybe I can find a solution for it.

How can I reduce the size of xml assets when building my apk

In my app I use the assets directory to store some XML files (some are large).
In my design time I want the files to use indentation and also put some comments in it.
This is enlarging my xml files and can add up to a large size.
Is it possible to add a task to the gradle build to remove all indentation and comments for the xml files before packaging it in the apk? If so how?
This will not only shrink my apk, but will also assist at run time with the xml processing.
EDIT
The answer by fhomovc was correct, but was missing some part.
I will mark it as correct but if anyone else will need it, here are the details:
In general I need a task that will run the minify utility and it should look like:
task minifyAssets(type:Exec) {
workingDir dirName // the directory of the merged assets under the build directory
commandLine 'minify', '-r', '-o', '.', '.'
doFirst {
println 'minifyAssets...'
}
}
This task should only be executed after the merged assets task is executed and before the package task is executed.
The main problem is that there should be a dedicated task for each variant, so I needed to do it dynamically:
First create the exec task and make it dependent on the merge task
applicationVariants.all { variant ->
// dynamically add minify task for specific variant
def dirName = new File("app\\build\\intermediates\\merged_assets\\" + variant.name + "\\out\\levels").getAbsolutePath()
def minifyTaskName = "minifyAssets" + variant.name
def mergeAssetsTaskName = "merge" + variant.name + "Assets"
def myTask = tasks.register(minifyTaskName, Exec) {
workingDir dirName
// require that minify utility will be in the path. Download from https://github.com/tdewolff/minify/tree/master/cmd/minify
commandLine 'minify', '-r', '-o', '.', '.'
doFirst {
println 'minifyAssets...' + workingDir
}
}
// set the minify task dependant on the merge assets task
myTask.get().dependsOn mergeAssetsTaskName
}
Now we need to make the specific package task depend on the minify task:
// when the package task is added make it dependant on the minify task
tasks.whenTaskAdded { theTask ->
if (theTask.name.startsWith("package") && (theTask.name.endsWith("Debug") || theTask.name.endsWith("Release"))) {
def minifyTaskName = theTask.name.replace("package", "minifyAssets")
theTask.dependsOn minifyTaskName
}
}
You can run a custom script with an xml minifier.
For the minifier: you can install minify following the installation steps provided.
For the script: you can refer to this answer. Essentially your task would look something like this
task executeScript(type:Exec) {
println 'Minifying xmls...'
//on linux
commandLine 'minify -r -o ./ --match=\.xml ./values' // ./values should be the path to your resources directory
}
Check the documentation to understand better how minify works. I haven't tested this solution myself, so it may need a few adjustments but you get the general idea. If you are using a Windows machine then the Script (commandLine) should be different, if I can find any examples online I'll add them.

How to set up a task that does something to APKs built by Gradle?

I need to set up a custom task that I run after an APK is built when using Gradle for Android. Right now I actually have two tasks. One builds an XML file using the path to the APK as an input, and the other runs after the XML task (named packageConfig) and bundles the APK and the XML file into a ZIP.
Right now, I'm having trouble with my packageConfig task. It runs the python script for only the "fullRelease" product flavor, when I actually have multiple:
x86Debug
x86Release
armDebug
armRelease
fullDebug
fullRelease
My script is only executed once, and for the wrong APK. For example, I will invoke gradle like so:
$ gradlew :Applications:zApp:assemblex86debug packageConfig
I set up a print() call in my python script to output the APK path passed in, and it is:
build\outputs\apk\full\release\zApp.apk
The path I'm expecting (based on the product flavor I chose on the command line, as well as the APK I know that is there):
build\outputs\apk\x86\debug\zApp.apk
I'll paste my gradle task below. Can anyone help me understand what I'm doing wrong? What corrections can I make to my script to get it to run my script on the correct APK?
task packageConfig(type: Exec) {
def buildCommit = getBuildCommit()
def buildBranch = getVcsBranchName()
def buildDescription = buildBranch + '-' + buildCommit
def buildDate = getBuildDate()
workingDir buildscript.sourceFile.parent
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
def apk = output.outputFile
def apkDir = apk.getParent();
def inputConfig = '' + project.projectDir + File.separator +
packageConfigXmlFile
def outputConfig = apkDir + File.separator + 'PackageConfig.xml'
commandLine 'python', 'setup-package-config.py', versions.version, inputConfig,
outputConfig, apk, buildDescription
}
}
}

Android studio: automatically open html test result file generated by './gradlew test'

I have a few unit tests that are being ran by the command './gradlew test'. When this completes, it generates 'index.html' in the build/reports folder.
Is there a way to have that automatically open when finished?
Thanks
Edit: I can use the command
./gradlew test && start C:\\<PACKAGE>\\build\\reports\\tests\\testDebugUnitTest\\debug\\index.html
This works. However. Is there a way to just include that entire line in my gradle file so it does it automatically?
In your project build.gradle you can add this task:
task testAndOpen(type: Exec) {
//execute test task first
dependsOn 'test'
//set the base dir
workingDir './build/reports/tests/testDebugUnitTest/debug'
//launch cmd and open the file with the default associated program
commandLine 'cmd', '/c', 'start index.html'
}
then from terminal you can do .\gradlew testAndOpen.
If you are not on Windows you can do a similar thing with a bit different commands. This is the "dirty and fast" way, because the path folder must be changed manually for different flavors but it's a good starting point.
the task will open all test reports from folder MyProject/mymodule/build/reports/tests
just add the code to mymodule/build.gradle
and execute in terminal:
./gradlew app:testDebugUnitTest --tests com.examplepackage.* testOpenTestReport
task testOpenTestReport(group: "verification") {
doLast{
File reportsDir = new File("${project.buildDir.path}/reports/tests")
if (!reportsDir.exists() || !reportsDir.isDirectory()) {
println "reportsDir ${reportsDir.absolutePath} not exist"
return null
}
fileTree(reportsDir)
.filter { it.isFile() }
.files
.findAll { (it.name == "index.html") }
.forEach{file->
println "${file.absolutePath}"
//launch cmd and open the file with the default associated program
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
println "start ${file.absolutePath}".execute().text.trim()
} else {
println "sensible-browser ${file.absolutePath}".execute().text.trim()
}
}
}
}

Android Studio build gradle

I want to replace a string token(version) in a html file from assets folder.
My question is how I can accomplish this task with build gradle?
Thank you.
THe first thing you need is to know how to replace your token with the desired value with command line (with sed or anything else you like).
Then you can add task in your gradle build logic :
task replaceToken {
commandline "your command line"
}
Then you add dependance on this task :
tasks.withType(JavaCompile) {
compileTask -> compileTask.depensOn replaceToken
}
In this scenario, after each build your asset file will have the version name in it. So your command line should be able to insert the token value even if there is already a value (you will have to use regexp to replace <balise>whatever</balise> by <balise>token value</balise>.
I put this in build.gradle and works
applicationVariants.all{ variant ->
variant.mergeResources.doFirst{
def buildNumber = env.BUILD_NUMBER
if (null != buildNumber) {
File valuesFile = file("${buildDir}/intermediates/assets/${variant.dirName}/about.html")
String content = valuesFile.getText('UTF-8')
content = content.replaceAll('#VERSION#', buildNumber)
valuesFile.write(content, 'UTF-8')
}
}
}

Categories

Resources