I have a task that generates a metadata file based off the compiled classes in an Android Gradle build. I can get it to run by executing it after the compile task:
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
def compileSourcesTaskName = "compile${variantName}Sources"
def compileSourcesTask = project.tasks.findByName(compileSourcesTaskName)
compileSourcesTask.finalizedBy "myTaskThatGeneratesAssets"
}
Unfortunately, Android has already processed the assets at this point. The new file won't get included in the assembled APK.
An answer to a similar question suggests calling aapt add to add the file to the APK before alignment/signing. This seems like it could work, but the post doesn't go into implementation details. The code to call aapt in the Android Gradle plugin looks fairly complicated for a build script, and I'm not sure how to get access to the IAndroidTarget it references.
I'd appreciate suggestions on how to implement this, or any other solutions!
Okay, here's what I ended up with. It makes two assumptions that may break on later versions of the android gradle plugin (I'm using 1.3.0):
The path to the platform tools is ${android.getSdkDirectory().getAbsolutePath()}/build-tools/${android.buildToolsVersion}/
The path to the intermediate (resources) APK is ${buildDir}/intermediates/res/resources-${variant.baseName}.ap_
So long as those are true, this should generate a task to add the new asset file using aapt after the resources APK has already been built:
def overlayDir = ... // path to a resources overlay directory that contains "assets/my.json"
def addMyAssetTaskName = "add${variantName}MyAsset"
task "${addMyAssetTaskName}" (type: Exec) {
dependsOn myTaskThatGeneratesAssets
workingDir overlayDir
def aaptCommand = "${android.getSdkDirectory().getAbsolutePath()}/build-tools/${android.buildToolsVersion}/aapt"
def apkPath = "${buildDir}/intermediates/res/resources-${variant.baseName}.ap_"
commandLine aaptCommand, 'add', apkPath, "assets/my.json"
}
Then I use finalizedBy like in the question above to addMyAssetTaskName.
Related
I have an Android library (AAR) and during build there are 2 native libraries are compiled, saying libfoo.so and libfoo_test.so.
I want to exclude libfoo_test.so from AAR itself and exclude libfoo.so from <projectname>-debug-androidTest.apk package. Android Gradle Plugin has a possibility to exclude files with packagingOptions, but it works for project itself and I didn't find how to use it for androidTest.
For AAR I solved the problem using Zip task which repacks AAR and excludes libfoo_test.so, but this approach doesn't look convenient for <projectname>-debug-androidTest.apk because I want to run this apk normally from Android Studio and I want to speedup compilation and reduce size.
Is there way to do so?
I created custom keystore and ended up with the following:
tasks.configureEach { task ->
if (task.name == 'packageDebugAndroidTest' || task.name == 'packageReleaseAndroidTest') {
String buildVariant = task.name == 'packageDebugAndroidTest' ? 'debug' : 'release'
String apkFilePath = "${project.buildDir}/outputs/apk/androidTest/${buildVariant}/<project>-${buildVariant}-androidTest.apk"
String apkSigner = "${android.getSdkDirectory().getAbsolutePath()}/build-tools/${android.getBuildToolsVersion()}/apksigner"
task.doLast {
// Remove unnecessary libfoo.so from testing apk. This is the fastest approach. Another one is to use combination of
// Zip task and Copy/Rename + Signing. Such approach is more portable and give better compression result, but it also
// executes much longer
def abis = ['x86_64', 'armeabi-v7a', 'arm64-v8a']
abis.each { abi ->
exec {
executable 'zip'
args '-d', apkFilePath, "lib/${abi}/libfoo.so"
// When zip utility is not available or when Android Studio is used or file already
// doesn't exists in archive just ignore possible error
ignoreExitValue true
}
}
exec {
executable apkSigner
args 'sign', '-v',
'--ks', 'test-properties',
'--ks-pass', "pass:" + 'test-ks-pass',
'--key-pass', "pass:" + 'test-key-pass',
'--ks-key-alias', 'test-key-alias',
apkFilePath
}
}
}
}
I want to upload NDK symbols on every build i do,
Under my Android inside gradle i use to have:
applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
println("symbols will be added on varinat ${variantName}")
def task = project.task("ndkBuild${variantName}")
task.finalizedBy project.("uploadCrashlyticsSymbolFile${variantName}")
}
this does not compile anymore since i moved to FireBase :
Could not get unknown property 'uploadCrashlyticsSymbolFile
I don't see this task running.
I basiclly need this task to run on every build:
./gradlew app:assembleBUILD_VARIANT\
app:uploadCrashlyticsSymbolFileBUILD_VARIANT
Add this at the bottom of app's build.gradle outside android { ... } block.
afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
println("symbols will be added on variant ${variantName}")
def task = tasks.findByName("assemble${variantName}")
def uploader = "uploadCrashlyticsSymbolFile${variantName}"
// This triggers after task completion
task?.finalizedBy(uploader)
// This ensures ordering
task?.mustRunAfter(uploader)
}
}
You can try without afterEvaluate block. It should still work.
Likely you'd need to use Firebase App Distribution, which permits automatic upload of release build artifacts - and if you have the artifact with the matching debug symbols, they could actually be used - without the matching assembly, the symbols are somewhat irrelevant.
Number 1 is obviously a wrongful assumption, because the documentation clearly states:
./gradlew app:assembleBUILD_VARIANT app:uploadCrashlyticsSymbolFileBUILD_VARIANT
And this is already answered here.
In order to always upload, one can create a task dependency:
assembleRelease.finalizedBy uploadCrashlyticsSymbolFileRelease
This may require setting unstrippedNativeLibsDir and strippedNativeLibsDir.
I have to add the Analytics tool Sentry to our Android project. In order to make it work, one needs to create mappings for the obfuscated code (from Proguard/R8) and upload it later to Sentry.
On the website https://docs.sentry.io/platforms/android/ it is even described how to do that.
There it is written that one needs to create a gradle task looking like this:
gradle.projectsEvaluated {
android.applicationVariants.each { variant ->
def variantName = variant.name.capitalize();
def proguardTask = project.tasks.findByName(
"transformClassesAndResourcesWithProguardFor${variantName}")
def dexTask = project.tasks.findByName(
"transformClassesWithDexFor${variantName}")
def task = project.tasks.create(
name: "processSentryProguardFor${variantName}",
type: Exec) {
workingDir project.rootDir
commandLine *[
"sentry-cli",
"upload-proguard",
"--write-properties",
"${project.rootDir.toPath()}/app/build/intermediates/assets" +
"/${variant.dirName}/sentry-debug-meta.properties",
variant.getMappingFile(),
"--no-upload"
]
}
dexTask.dependsOn task
task.dependsOn proguardTask
}
}
This shall wait until Proguard is finished, than copy this properties file to the assets. However, when I add this to my Android gradle script I get the error:
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
No signature of method: java.util.ArrayList.multiply() is applicable for argument types: (ArrayList) values: [[sentry-cli, upload-proguard,
--write-properties, {Application-Path}/app/build/intermediates/assets/playStoreStaging/debug/sentry-debug-meta.properties,
...]] Possible solutions: multiply(java.lang.Number),
multiply(java.lang.Number)
I assume there is something wrong with the multiplication symbol * before the commandLine array. But when I remove it I get the error
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
Cannot cast object 'sentry-cli' with class 'java.lang.String' to class 'int'
So I tried to test this with only that line
commandLine "sentry-cli", ...
Which gave me another error
What went wrong: Cannot invoke method dependsOn() on null object
Thus I assume something went really wrong with that gradle script since it seems the dependend task can't be found. Does anyone have any idea how to fix this (or optionally have any other idea how to copy that sentry-debug-meta.properties file to my assets in another way, once Proguard/R8 is finished)?
Thanks!
-------- EDIT --------
I noticed something important.
The gradle tasks are defined in a different name than what was defined in the manual. Looking at my tasks I have them named
transformClassesAndResourcesWithR8For...
and
transformClassesWithDexBuilderFor...
However, I print the variantName then for checking but it seems my tasks are incomplete.
In my tasks list there exist
transformClassesAndResourcesWithR8ForPlayStoreStagingDebug
but not
transformClassesAndResourcesWithR8ForPlayStoreStagingRelease
and thus the task can't be found. I think that is the real problem here. So where are these gradle tasks defined?
------- EDIT 2 --------
Okay I noticed something strange here. Some variants don't have tasks. It makes sense that DEBUG tasks don't have R8 tasks but I found this here:
Variant: PlayStoreStagingRelease DexTask is null
Variant: PlayStorePreviewRelease DexTask is null
Variant: HockeyAppRelease DexTask is null
Variant: LocalServerRelease DexTask is null
Variant: PlayStoreProductionRelease DexTask is null
So how can this be?
I'd recommend using the Sentry Gradle integration (Gradle plugin) which is described here https://docs.sentry.io/platforms/android/#gradle-integration
The official Android Gradle plugin changed its task names over versions, Gradle version also affects those code snippets.
Google also replaced Proguard with R8 and it also affected those code snippets.
Is there a reason why not using the Sentry Gradle integration? if so, We'll be looking into updating them.
Thanks.
java.util.ArrayList.multiply() hints for that * in front of the [ ] list, which looks strange to me. Try removing the *[ ], only keeping List<String> (there's no ArrayList expected, to begin with):
commandLine "sentry-cli", "upload-proguard", "--write-properties", "${project.rootDir.toPath()}/app/build/intermediates/assets/${variant.dirName}/sentry-debug-meta.properties", variant.getMappingFile(), "--no-upload"
You'd have to look up how your tasks are actually being called, but it should be something alike:
def r8Task = project.tasks.findByName("transformClassesAndResourcesWithR8For${variantName}")
def d8Task = project.tasks.findByName("transformClassesWithDexBuilderFor${variantName}")
With a null check, because not every variant might have minifyEnabled true set:
if(r8Task != null) {
d8Task.dependsOn task
task.dependsOn r8Task
}
Maybe even a previous null check is required, because variant.getMappingFile() needs R8.
And that some flavors have no D8 task might be based upon the absence of code (nothing to do).
Here's a summary of the steps that I followed for integrating Sentry with my Android app. These steps are to ensure the sentry gradle plugin works as expected and automatically uploads the proguard mapping files, without you having to worry about uploading using cli. I assume you would have setup the Sentry SDK as described here:
https://docs.sentry.io/platforms/android/#integrating-the-sdk
Ensure you have Android Studio gradle plugin 3.5.0 (Not 3.6.x, that seems to break the sentry plugin. I observed that the sentry proguard or native symbol upload tasks are not configured or executed at all). This value should be in your root project's build.gradle in dependencies block
Provide a sentry.properties file the root folder of your project. The sentry.properties file should have the following values at minimum:
defaults.project=your_sentry_project_name
defaults.org=your_sentry_org_name
auth.token=sentry_project_auth_token
You can get info about generating auth tokens here: https://sentry.io/settings/account/api/auth-tokens/
(Optional: If you have build flavors) In my case, I have different flavors for my app. So, I had to put the sentry.properties inside my flavor specific folder in /app/src/ folder. Then, I wrote a gradle task to copy the flavor specific sentry.properties file into the project's root folder during gradle's configuration phase. Example:
task copySentryPropertiesTask {
if (getBuildFlavor() != null && !getBuildFlavor().isEmpty()) {
println("Copying Sentry properties file: ${getBuildFlavor()}")
copy {
from "src/${getBuildFlavor()}/"
include "sentry.properties"
into "../"
}
}
}
def getBuildFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1)
else {
println "NO MATCH FOUND"
return ""
}
}
Note 1: You can place this task in your app/build.gradle anywhere (I had placed it at the end).
Note 2: If you followed step 3 for build flavors, you can also add the root folder's sentry.properties in .gitignore. Since, it will be copied everytime you create a build.
Sentry should now be able to upload the proguard files for any release builds (or if you set minifyEnabled=true for any buildType).
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.
Prior to Android plugin version 3.0.0-alpha4, I have been using the following for publishing different variants of my APKs to a specific file path:
def publish = project.tasks.create("publishAll")
android.applicationVariants.all { variant ->
def task = project.tasks.create("publish${variant.name}Apk", Copy)
task.from(variant.outputs[0].outputFile)
task.into(buildDir)
task.dependsOn variant.assemble
publish.dependsOn task
}
I originally got it from this answer from Xavier Ducrohet: Copying APK file in Android Gradle project
As of the new updates to Android Studio Preview which uses version 3.0.0-alpha4, variant.outputFile is deprecated. What is the new suggested way to achieve something like this?
EDIT:
Looks like there is no way to currently access the variant output file as pointed out here: https://developer.android.com/studio/preview/features/new-android-plugin-migration.html#variant_api
Looks like we'll have to wait until they introduce those apis
If you don't use abi splits next snippet works
project.afterEvaluate {
android.applicationVariants.all { variant ->
// create file where to copy
def backupFolder = rootProject.file("backup")
def backupFile = new File(backupFolder, String.format("%s_v%s.%d.apk", variant.flavorName, variant.versionName, variant.versionCode))
variant.outputs.all { output ->
Task copyAndRenameAPKTask = project.task("copyAndRename${variant.name.capitalize()}APK", type: Copy) {
from output.outputFile.getParent()
into backupFolder
include output.outputFileName
rename(output.outputFileName, backupFile.getName())
}
// if copyAndRenameAPKTask needs to automatically execute assemble before
copyAndRenameAPKTask.dependsOn(variant.assemble)
copyAndRenameAPKTask.mustRunAfter(variant.assemble)
// if assemble needs to automatically execute copyAndRenameAPKTask after
variant.assemble.finalizedBy(copyAndRenameAPKTask)
}
}
}