I'm trying to add a custom task to my Android project's build.gradle to copy the final APK and Proguard's mapping.txt into a different directory. My task depends on the assembleDevDebug task:
task publish(dependsOn: 'assembleDevDebug') << {
description 'Copies the final APK to the release directory.'
...
}
I can see how to do a file copy using the standard Copy task type, as per the docs:
task(copy, type: Copy) {
from(file('srcDir'))
into(buildDir)
}
but that assumes you know the name and location of the file you want to copy.
How can I find the exact name and location of the APK file which was built as part of the assembleDevDebug task? Is this available as a property? It feels as if I should be able to declare the files as inputs to my task, and declare them as outputs from the assemble task, but my Gradle-fu isn't strong enough.
I have some custom logic to inject the version number into the APK filename, so my publish task can't just assume the default name and location.
If you can get the variant object associated with devDebug you could query it with getOutputFile().
So if you wanted to publish all variants you'd something like this:
def publish = project.tasks.create("publishAll")
android.applicationVariants.all { variant ->
def task = project.tasks.create("publish${variant.name}Apk", Copy)
task.from(variant.outputFile)
task.into(buildDir)
task.dependsOn variant.assemble
publish.dependsOn task
}
Now you can call gradle publishAll and it'll publish all you variants.
One issue with the mapping file is that the Proguard task doesn't give you a getter to the file location, so you cannot currently query it. I'm hoping to get this fixed.
The following code is what I'm using to archive apk and proguard mapping into a zip file for each variant with 'release' build type:
def releasePath = file("${rootDir}/archive/${project.name}")
def releaseTask = tasks.create(name: 'release') {
group 'Build'
description "Assembles and archives all Release builds"
}
android.applicationVariants.all { variant ->
if (variant.buildType.name == 'release') {
def build = variant.name.capitalize()
def releaseBuildTask = tasks.create(name: "release${build}", type: Zip) {
group 'Build'
description "Assembles and archives apk and its proguard mapping for the $build build"
destinationDir releasePath
baseName variant.packageName
if (!variant.buildType.packageNameSuffix) {
appendix variant.buildType.name
}
if (variant.versionName) {
version "${variant.versionName}_${variant.versionCode}"
} else {
version "$variant.versionCode"
}
def archiveBaseName = archiveName.replaceFirst(/\.${extension}$/, '')
from(variant.outputFile.path) {
rename '.*', "${archiveBaseName}.apk"
}
if (variant.buildType.runProguard) {
from(variant.processResources.proguardOutputFile.parent) {
include 'mapping.txt'
rename '(.*)', "${archiveBaseName}-proguard_\$1"
}
}
}
releaseBuildTask.dependsOn variant.assemble
variant.productFlavors.each { flavor ->
def flavorName = flavor.name.capitalize()
def releaseFlavorTaskName = "release${flavorName}"
def releaseFlavorTask
if (tasks.findByName(releaseFlavorTaskName)) {
releaseFlavorTask = tasks[releaseFlavorTaskName]
} else {
releaseFlavorTask = tasks.create(name: releaseFlavorTaskName) {
group 'Build'
description "Assembles and archives all Release builds for flavor $flavorName"
}
releaseTask.dependsOn releaseFlavorTask
}
releaseFlavorTask.dependsOn releaseBuildTask
}
}
}
It creates tasks like the following:
release - Assembles and archives all Release builds
releaseFree - Assembles and archives all Release builds for flavor Free
releaseFreeRelease - Assembles and archives apk and its proguard mapping for the FreeRelease build
releasePaid - Assembles and archives all Release builds for flavor Paid
releasePaidRelease - Assembles and archives apk and its proguard mapping for the PaidRelease build
Content of archive/projectName/packageName-buildType-versionName_versionCode.zip would be:
packageName-buildType-versionName_versionCode.apk
packageName-buildType-versionName_versionCode-proguard_mapping.txt
I've got some good pointers here but also had a hard time to get done as I wanted. Here's my final version:
def archiveBuildTypes = ["distribute"];
def archiveFlavors = ["googleplay"]
android.applicationVariants.all { variant ->
if (variant.buildType.name in archiveBuildTypes) {
variant.productFlavors.each { flavor ->
if (flavor.name in archiveFlavors) {
def taskSuffix = variant.name.capitalize()
def version = "${android.defaultConfig.versionCode} (${android.defaultConfig.versionName})" // assumes that versionName was especified here instead of AndroidManifest.xml
def destination = "${rootDir}/${project.name}/archive/${version}"
def assembleTaskName = "assemble${taskSuffix}"
if (tasks.findByName(assembleTaskName)) {
def copyAPKTask = tasks.create(name: "archive${taskSuffix}", type:org.gradle.api.tasks.Copy) {
description "Archive/copy APK and mappings.txt to a versioned folder."
from ("${buildDir}") {
include "**/proguard/${flavor.name}/${variant.buildType.name}/mapping.txt"
include "**/apk/${variant.outputFile.name}"
}
into destination
eachFile { file->
file.path = file.name // so we have a "flat" copy
}
includeEmptyDirs = false
}
tasks[assembleTaskName].finalizedBy = [copyAPKTask]
}
}
}
}
}
This is how I copy mappings.txt whenever proguard runs
tasks.whenTaskAdded { task ->
if (task.name.startsWith("proguard")) {//copy proguard mappings
task << {
copy {
from buildDir.getPath() + "/proguard"
into '../proguard'
include '**/mapping.txt'
}
println "PROGUARD FILES COPIED"
}
}
}
def publish = project.tasks.create("publishAll")// publish all task
applicationVariants.all { variant ->
if (variant.buildType.name.equals("release")) {// Only Release
File outDir = file("//192.168.4.11/Android/Release")
File apkFile = variant.outputs[0].outputFile
File mapFile = variant.mappingFile
def task = project.tasks.create("publish${variant.name.capitalize()}Apk", Copy)
task.from apkFile, mapFile
task.into outDir
task.rename "mapping.txt", "${apkFile.name.substring(0, apkFile.name.length() - 3)}mapping.txt"// Rename mapping.txt
task.doLast{
println ">>>publish ${variant.name} success!" +
"\ndir: ${outDir}" +
"\napk: ${apkFile.name}"
}
task.dependsOn variant.assemble
publish.dependsOn task
}
}
Related
I am trying to configure an Android project combining multiple product flavors with flavor dimensions.
Here is a snippet of build.gradle
android {
...
flavorDimensions "vendor", "type"
productFlavors {
development {
dimension "vendor"
}
production {
dimension "vendor"
}
free {
dimension "type"
}
paid {
dimension "type"
}
}
...
}
I am using a google service where I need to have the google-services.json file and I would like to have a different file for each vendor, one for development and another for production.
So I have one google-services.json in app/src/development and another in app/src/production.
When building the I get this error:
File google-services.json is missing. The Google Services Plugin cannot function without it.
Searched Location:
<path>/app/src/developmentPaid/debug/google-services.json
<path>/app/src/debug/developmentPaid/google-services.json
<path>/app/src/developmentPaid/google-services.json
<path>/app/src/debug/google-services.json
<path>/app/google-services.json
From the error I need to have the same google-services.json in app/src/developmentFree and app/src/developmentPaid and another in app/src/productionFree and app/src/productionPaid.
Do I need to have the same file in both places when using flavor dimensions?
Edit:
I end up by solving this issue with a couple of extra tasks for each merged productFlavor. I added this code after android :
afterEvaluate {
android.productFlavors.all { flavor ->
if (flavor.dimension == "vendor") {
task("copy${flavor.name.capitalize()}GoogleServicesFile", type: Copy) {
description = 'Switches to google-services.json depending on flavor'
from "src/${flavor.name}"
include "google-services.json"
into "."
}
task("delete${flavor.name.capitalize()}GoogleServicesFile", type: Delete) {
description = 'Delete google-services.json from base folder'
delete "./google-services.json"
}
}
}
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name.capitalize()
def typeFlavorName = variant.productFlavors.get(0).name.capitalize()
def vendorFlavorName = variant.productFlavors.get(1).name.capitalize()
def copyFileTaskName = "copy${vendorFlavorName}GoogleServicesFile"
def deleteFileTaskName = "delete${vendorFlavorName}GoogleServicesFile"
def processGoogleServicesTaskName = "process${typeFlavorName}${vendorFlavorName}${buildType}GoogleServices"
tasks."${processGoogleServicesTaskName}".dependsOn "${copyFileTaskName}"
tasks."${processGoogleServicesTaskName}".finalizedBy "${deleteFileTaskName}"
}
}
This way, for each typeFlavorName, vendorFlavorName and buildType variation, the google-services.json file is copied to app/ before process{typeFlavorName}{vendorFlavorName}{buildType}GoogleServices and deleted afterwards.
I end up by solving this issue with a couple of extra tasks for each merged productFlavor. I added this code after android :
afterEvaluate {
android.productFlavors.all { flavor ->
if (flavor.dimension == "vendor") {
task("copy${flavor.name.capitalize()}GoogleServicesFile", type: Copy) {
description = 'Switches to google-services.json depending on flavor' from "src/${flavor.name}"
include "google-services.json" into "."
}
task("delete${flavor.name.capitalize()}GoogleServicesFile", type: Delete) {
description = 'Delete google-services.json from base folder'
delete "./google-services.json"
}
}
}
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name.capitalize()
def typeFlavorName = variant.productFlavors.get(0).name.capitalize()
def vendorFlavorName = variant.productFlavors.get(1).name.capitalize()
def copyFileTaskName = "copy${vendorFlavorName}GoogleServicesFile"
def deleteFileTaskName = "delete${vendorFlavorName}GoogleServicesFile"
def processGoogleServicesTaskName = "process${typeFlavorName}${vendorFlavorName}${buildType}GoogleServices"
tasks."${processGoogleServicesTaskName}".dependsOn "${copyFileTaskName}"
tasks."${processGoogleServicesTaskName}".finalizedBy "${deleteFileTaskName}"
}
}
This way, for each typeFlavorName, vendorFlavorName and buildType variation, the google-services.json file is copied to app/ before process{typeFlavorName}{vendorFlavorName}{buildType}GoogleServices and deleted afterwards.
I am trying to extract and use the architecture of the artifact to compose the output file name:
void defineDefaultVariantsAPK(String appName) {
defineVariantsAPK({ variant, output ->
def versionName = variant.versionName
def versionCode = "-(${variant.versionCode}"
def buildLetter = variant.buildType.name == "release" ? "-R" : "-D"
def flavor = variant.productFlavors.size() > 0 ? "-${variant.productFlavors[0].name}" : ""
def architecture = "-" + output.getFilter(com.android.build.OutputFile.ABI)
"v${versionName}${versionCode}${buildLetter}${flavor}${architecture}--${appName}.apk"
})
}
void defineVariantsAPK(Closure nameBuild) {
android {
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = new File((String) output.outputFile.parent, nameBuild(variant, output))
}
}
}
}
for some reason many posts says that that is the solution, but my gradle fails on:
> Could not get unknown property 'com' for project ':app' of type org.gradle.api.Project.
I've tried to use import:
unable to resolve class com.android.build.OutputFile
configuration:
com.android.tools.build:gradle:2.3.3
Gradle version 3.5
So I really wonder, what am I doing different that it doesn't work?
Beggining with Gradle 3.0.0 you can do it like this:
applicationVariants.all { variant ->
variant.outputs.all { output ->
def appName = "AppName_"
def buildType = variant.variantData.variantConfiguration.buildType.name
def architecture = output.getFilter(com.android.build.OutputFile.ABI)
def newName
if (buildType == 'debug') {
newName = "${appName}debug_${defaultConfig.versionName}_${architecture}.apk"
} else {
newName = "${appName}release_${defaultConfig.versionName}_${architecture}.apk"
}
outputFileName = newName
}
}
*put this in your module(app) build.gradle file.
This code works for me with gradle 3+ for android
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
def architecture = output.filters[0].identifier
outputFileName = "myapp-${architecture}-${variant.versionName}.apk"
}
}
Where did you define this function? Try to move it you your app/build.gradle file as that is where buildscript is and gradle loads classes from it into classpath.
So following #blazsolar suggestion I've moved the method to the app build gradle file, the move itself was not the solution, when I tried to add the import:
import com.android.build.OutputFile
for some reason the IDE (Android studio) has decided to delete the import..
but once I've this import:
import com.android.build.OutputFile.FilterType
it was just fine!
but that is not the end of it... I want the method to be in a common gradle file, that all my project can reuse.
once I've added the last import to the common gradle file... the import figging disappeared again..
I am left with yet another question
update:
So I've update Android plugin and gradle version to 3.0.1 and 4.1 respectively, and now... things have change.. now there is no
com.android.build.OutputFile.FilterType
Now you need to use:
com.android.build.VariantOutput.ABI
Just in case anyone wonder...
I'm trying to deal with google-services.json and different flavors. The documentation says that we need the file in the root folder.
I got a task that can easily copy the file from the flavor folder to the root folder:
task CopyToRoot(type: Copy) {
def appModuleRootFolder = '.'
def srcDir = 'src'
def googleServicesJson = 'google-services.json'
outputs.upToDateWhen { false }
def flavorName = android.productFlavors.flavor1.name
description = "Switches to $flavorName $googleServicesJson"
delete "$appModuleRootFolder/$googleServicesJson"
from "${srcDir}/$flavorName/"
include "$googleServicesJson"
into "$appModuleRootFolder"
}
Then, in the afterEvaluate I force
afterEvaluate {
processFlavor1DebugGoogleServices.dependsOn CopyToRoot
processFlavor1ReleaseGoogleServices.dependsOn CopyToRoot
}
This works only for 1 flavor (defined statically). How to do this for every flavor? I got 4 flavors. How to get the current flavor that is being compiled?
[UPDATE 1] -
Also tried:
task CopyToRoot(type: Copy) {
def appModuleRootFolder = '.'
def srcDir = 'src'
def googleServicesJson = 'google-services.json'
outputs.upToDateWhen { false }
def flavorName = android.productFlavors.flavor1.name
android.applicationVariants.all { variant ->
def flavorString = variant.getVariantData().getVariantConfiguration().getFlavorName()
println('flavorString: ' + flavorString)
description = "Switches to $flavorString $googleServicesJson"
delete "$appModuleRootFolder/$googleServicesJson"
from "${srcDir}/$flavorString/"
include "$googleServicesJson"
into "$appModuleRootFolder"
}
}
But this doesn't copy the correct file. Any help?
A way to go is to create a folder named "google-services" in each flavor, containing both the debug version and the release version of the json file :
In the buildTypes section of your gradle file, add this :
applicationVariants.all { variant ->
def buildTypeName = variant.buildType.name
def flavorName = variant.productFlavors[0].name;
def googleServicesJson = 'google-services.json'
def originalPath = "src/$flavorName/google-services/$buildTypeName/$googleServicesJson"
def destPath = "."
copy {
if (flavorName.equals(getCurrentFlavor()) && buildTypeName.equals(getCurrentBuildType())) {
println originalPath
from originalPath
println destPath
into destPath
}
}
}
It will copy the right json file at the root of your app module automatically when you'll switch build variant.
Add the two methods called to get the current flavor and current build type at the root of your build.gradle
def getCurrentFlavor() {
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() ) {
println matcher.group(1).toLowerCase()
return matcher.group(1).toLowerCase()
}
else
{
println "NO MATCH FOUND"
return "";
}
}
def getCurrentBuildType() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
if (tskReqStr.contains("Release")) {
println "getCurrentBuildType release"
return "release"
}
else if (tskReqStr.contains("generateDebug")) {
println "getCurrentBuildType debug"
return "debug"
}
println "NO MATCH FOUND"
return "";
}
Based on this answer
I found my answer. Google finally supports different google-services.json per flavor. You just need to update the plugin to com.google.gms:google-services:2.0.0.
There's no need to copy the json file to the app folder, just put your google-services.json different files inside your build flavors directories.
I have android project based on gradle and I want to change mapping.txt file name after it's generated for my build. How can it be done?
upd
How it can be done in build.gradle? Since I have access there to my flavors and other stiff, I would like to create mapping file name based on flavor/build variant version.
Simpler solution.
applicationVariants.all { variant ->
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast {
copy {
from variant.mappingFile
into "${rootDir}/proguardTools"
rename { String fileName ->
"mapping-${variant.name}.txt"
}
}
}
}
}
As of today (May 2020) former solution, which uses variant.mappingFile is not working anymore in new Android Gradle plugin (Android Studio) 3.6 and higher.
Instead variant.mappingFile returns null and following is displayed in the logs:
WARNING: API 'variant.getMappingFile()' is obsolete and has been replaced with 'variant.getMappingFileProvider()'.
I am sharing my working solution, which uses new api:
applicationVariants.all { variant ->
variant.assembleProvider.get().doLast {
def mappingFiles = variant.getMappingFileProvider().get().files
for (file in mappingFiles) {
if (file != null && file.exists()) {
def nameMatchingApkFile = "$archivesBaseName-$variant.baseName-$file.name"
def newMappingFile = new File(file.parent, nameMatchingApkFile)
newMappingFile.delete() //clean-up if exists already
file.renameTo(newMappingFile)
}
}
}
}
Note, that variant.getBuildType().isMinifyEnabled() is not used since we are using DexGuard.
The code above makes mapping file's name match apk's file name.
Just in case, if you need to change apk name - following could be used:
android {
defaultConfig {
//resulting apk will looks like: "archive base name" + -<flavour>-<buildType>.apk
archivesBaseName = "$applicationId-$versionName"
}
}
Use this command in your proguard-rules.pro file:
-printmapping path/to/your/file/file_name.txt
the file will be written in part {root}/path/to/your/file with file_name.txt name.
If you want to have different setting for different flavors you can define many proguard-rules for them
I found one more idea but I am not sure that it is right way.
You can define your path in flavors:
productFlavors {
test1 {
applicationId "com.android.application.test"
project.ext."${name}Path" = 'path/one/mapp.txt'
}
test2 {
project.ext."${name}Path" = 'path/two/mapp.txt'
}
}
And as next you can define new task before $asseble{variant.name.capitalize()} task as is shown below:
android.applicationVariants.all { variant ->
def envFlavor = variant.productFlavors.get(0).name
def modifyProguardPath = tasks.create(name: "modifyProguardFor${variant.name.capitalize()}", type: Exec) {
def pathToMap = project."${envFlavor}Test1"
doFirst {
println "==== Edit start: $pathToMap ===="
}
doLast {
println "==== Edit end: $pathToMap ===="
}
executable "${rootDir}/utils/test.bash"
args pathToMap
}
project.tasks["assemble${variant.name.capitalize()}"].dependsOn(modifyProguardPath);
}
and in script ${root}/utils/test.bash - you can modify proguard-rules.pro.
But I think that exist better solution.
Many thanx to Sergii Pechenizkyi who helped me to found this good solution.
To implement copying of proguard mapping files for each flavor we can create "root" task copyProguardMappingTask and number of dynamic tasks for each flavor
def copyProguardMappingTask = project.tasks.create("copyProguardMapping")
applicationVariants.all { variant ->
variant.outputs.each { output ->
...
if (variant.getBuildType().isMinifyEnabled()) {
def copyProguardMappingVariantTask = project.tasks.create("copyProguardMapping${variant.name.capitalize()}", Copy)
def fromPath = variant.mappingFile;
def intoPath = output.outputFile.parent;
copyProguardMappingVariantTask.from(fromPath)
copyProguardMappingVariantTask.into(intoPath)
copyProguardMappingVariantTask.rename('mapping.txt', "mapping-${variant.name}.txt")
copyProguardMappingVariantTask.mustRunAfter variant.assemble
copyProguardMappingTask.dependsOn copyProguardMappingVariantTask
}
}
}
afterwards we should run this task after assembling our project. I use jenkins and my tasks option looks like
gradle clean assembleProjectName copyProguardMapping
It works like a charm.
Since the last update variant.mappingFile is not longer available.
(I use ProGuard version 4.7, AndroidStudio 2.0)
This is (part of) my build.gradle file:
import java.util.regex.Matcher
import java.util.regex.Pattern
def getCurrentFlavor() {
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).toLowerCase()
else
{
println "NO MATCH FOUND"
return "";
}
}
buildTypes {
release {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
minifyEnabled true
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = new File(output.outputFile.parent, "${variant.name}_v${variant.versionName}.apk")
}
def mappingFile = "${rootDir}\\app\\build\\outputs\\mapping\\${getCurrentFlavor()}\\release\\mapping.txt"
println("mappingFile: ${mappingFile}")
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast {
copy {
from "${mappingFile}"
into "${rootDir}"
rename { String fileName ->
"mapping-${variant.name}.txt"
}
}
}
}
}
}
debug {
minifyEnabled false
useProguard false
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = new File(output.outputFile.parent, "${variant.name}_v${variant.versionName}.apk")
}
}
}
}
variant.assemble is now deprecated, suggested solution incorporating previous modifications:
archivesBaseName = "MyCompany-MyAppName-$versionName"
...
applicationVariants.all { variant ->
variant.assembleProvider.get().doLast {
if (variant.mappingFile != null && variant.mappingFile.exists()) {
def mappingFilename = "$archivesBaseName-$variant.baseName-mapping.txt"
(new File(variant.mappingFile.parent, mappingFilename)).delete()
variant.mappingFile.renameTo(variant.mappingFile.parent +
"/" + mappingFilename)
}
}
}
If using app bundle (aab) instead of apk, add this to after the android section:
afterEvaluate {
bundleRelease.doLast {
android.applicationVariants.all { variant ->
if (variant.buildType.name == 'release') {
tasks.create(name: "renameMappingFile") {
if (variant.mappingFile != null && variant.mappingFile.exists()) {
variant.mappingFile.renameTo(variant.mappingFile.parent + "/$variant.baseName-$versionName-${new Date().format('yyyy-MM-dd_HHmm')}-mapping.txt")
}
}
}
}
}
}
Swap bundleRelease for assembleRelease for apks in the last example too.
Update: However that last doesn't work if you try and build a normal debug directly to your phone then. Error:
Could not get unknown property 'bundleRelease' for project ':app' of type org.gradle.api.Project.
This is a variation of igorpst's answer but renames mapping.txt to match the apk's name exactly including the app version name. I've combined this with code to name the APK with a version number as described in this answer. I had to snoop through the gradle source code to find $variant.baseName
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "com.company.app"
minSdkVersion 13
targetSdkVersion 21
versionCode 14 // increment with every release
versionName '1.4.8' // change with every release
archivesBaseName = "MyCompany-MyAppName-$versionName"
}
applicationVariants.all { variant ->
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast {
(new File(variant.mappingFile.parent, "$archivesBaseName-$variant.baseName-mapping.txt")).delete();
variant.mappingFile.renameTo(variant.mappingFile.parent +
"/$archivesBaseName-$variant.baseName-mapping.txt")
}
}
}
}
All these answers used copy to rename the file.
I didn't want to move the file however, I just wanted to change it's name, keeping in mind the build type and flavor.
I based myself on the code from the other posters here and changed it up a bit.
Since Minify can be false, while still using proguard, I just check if the file is present.
Following code accomplishes just that.
android {
applicationVariants.all { variant ->
variant.assemble.doLast {
def mappingFolderUrl = "${project.buildDir.path}/outputs/mapping/"
if (variant.buildType.name) {
mappingFolderUrl += variant.buildType.name + "/"
}
if (variant.flavorName) {
mappingFolderUrl += variant.flavorName + "/"
}
def mappingFileUrl = mappingFolderUrl + "mapping.txt"
logger.lifecycle("mappingFile path: ${mappingFileUrl}")
File mappingFile = file(mappingFileUrl)
if (mappingFile.exists()) {
def newFileName = mappingFolderUrl + "mapping-${variant.name}.txt"
mappingFile.renameTo(newFileName)
}
}
}
}
NOTE
You could probably use this code to move the file as well.
the .renameTo() method expects a full path, If you change the path, I would suppose you effectively move the File to another place.
A complete solution that worked for me
applicationVariants.all { variant ->
def variantType = variant.buildType.name
if (variantType == "release") {
variant.assemble.doLast {
def mappingFile = variant.mappingFile
mappingFile.renameTo(mappingFile.parent + "/mapping-${variant.name}.txt")
}
}
}
For Android Studio Gradle Plugin Version 4.1.0 and newer (since about May 2020)
This version fixes the following warning:
WARNING: API 'variant.getMappingFile()' is obsolete and has been replaced with 'variant.getMappingFileProvider()'.
applicationVariants.all { variant ->
variant.assembleProvider.get().doLast {
def mappingFileProvider = variant.getMappingFileProvider().get()
if (mappingFileProvider != null) {
try {
def mappingFiles = mappingFileProvider.getFiles()
for (mappingFile in mappingFiles) {
if (mappingFile != null && mappingFile.exists()) {
def newMappingFileName = "$archivesBaseName-$variant.baseName-$mappingFile.name"
project.logger.lifecycle("Renaming '${mappingFile.name}' to '${newMappingFileName}'")
def newMappingFile = new File(mappingFile.parent, newMappingFileName)
newMappingFile.delete()
mappingFile.renameTo(newMappingFile)
}
}
} catch (Exception ignored) {
project.logger.lifecycle("No mapping files found to rename")
}
}
}
}
For Android Studio Gradle Plugin Version 3.3.0 (January 2019) through about May 2020
This overcomes previous issues where Android 3.0/Android Gradle Plugin 3.0 deprecated BuildType.isMinifyEnabled() and the gradle plugin deprecated variant.getAssemble().
applicationVariants.all { variant ->
variant.assembleProvider.get().doLast {
if (variant.mappingFile != null && variant.mappingFile.exists()) {
def mappingFilename = "$archivesBaseName-$variant.baseName-mapping.txt"
(new File(variant.mappingFile.parent, mappingFilename)).delete()
variant.mappingFile.renameTo(variant.mappingFile.parent +
"/" + mappingFilename)
}
}
}
Pinhassi's solution above works great and it is conforms to the latest Gradle changes. There are a couple of things though that I had to change:
The module name is hardcoded ("app"), which is not ideal since in a lot of cases (including mine) that will not be true. It is better to dynamically detect the module name.
The mapping file also only conforms to the Windows file system by having backward escaped slashes ("\"). If you are on a *NIX system like Linux or Mac, you need to replace those with forward non escaped slashes ("/")
Changed a bit the renaming of the .apk file to include the project name and added a date/time stamp at the end.
Here is the finished code:
import java.util.regex.Matcher
import java.util.regex.Pattern
buildTypes {
release {
debuggable false
minifyEnabled true
proguardFiles 'proguard.cfg'
// Rename the apk file and copy the ProGuard mapping file to the root of the project
applicationVariants.all { variant ->
if (variant.getBuildType().name.equals("release")) {
def formattedDate = new Date().format('yyyyMMddHHmmss')
def projectName = ""
variant.outputs.each { output ->
def fullName = output.outputFile.name
projectName = fullName.substring(0, fullName.indexOf('-'))
// ${variant.name} has the value of "paidRelease"
output.outputFile = new File((String) output.outputFile.parent, (String) output.outputFile.name.replace(".apk", "-v${variant.versionName}-${formattedDate}.apk"))
}
def mappingFile = "${rootDir}/${projectName}/build/outputs/mapping/${getCurrentFlavor()}/release/mapping.txt"
println("mappingFile: ${mappingFile}")
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast {
copy {
from "${mappingFile}"
into "${rootDir}"
rename { String fileName ->
"mapping-${variant.name}.txt"
}
}
}
}
}
}
}
debug {
debuggable true
}
}
def getCurrentFlavor() {
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).toLowerCase()
else {
println "NO MATCH FOUND"
return "";
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
if (variant.getBuildType().isMinifyEnabled()) {
variant.assemble.doLast{
copy {
from variant.mappingFile
into "${rootDir}/mapping"
rename { String fileName ->
"mapping-${variant.name}-${new Date().format('yyyy_MM_dd')}.txt"
}
}
}
}
}
}
Here is solution that helps me:
compileSdkVersion 30
JavaVersion.VERSION_1_8
kotlin_version = '1.5.31'
com.android.tools.build:gradle:7.0.2
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant ->
// Generating apk file for each flavour release build
variant.outputs.all {
outputFileName = "${variant.flavorName}-${variant.versionCode}.apk"
}
// Generating mapping file for each flavour release build
if (variant.getBuildType().isMinifyEnabled()) {
variant.assembleProvider.get().doLast {
def files = variant.getMappingFileProvider().get().getFiles()
for (file in files) {
if (file != null && file.exists()) {
def newName = "mapping-${variant.flavorName}-${variant.versionCode}.txt"
def newFile = new File(file.parent, newName)
newFile.delete()
file.renameTo(newName)
}
}
}
}
}
}
Working on an Android library with Gradle (v 1.7) as the building tool, I've used the maven plugin and configured the task uploadArchives to publish both release and debug version of the lib to the local maven repository.
The code below works ok :
// [...]
apply plugin: 'android-library'
// [...] nothing unusual
/*
* Define name of the apk output file (build/apk/<outputFile>)
*/
android.libraryVariants.all
{
variant ->
def outputName = "MyModule-${android.defaultConfig.versionName}-${variant.baseName}.aar"
variant.outputFile = new File("$buildDir/libs", outputName)
}
/*
* Publish to maven local repo (older style maven plugin)
* Used while android plugin is not fixed regarding maven-publish plugin
*
* type command "gradle uploadArchives" to publish the module into the
* local .m2 repository
*/
apply plugin: 'maven'
android.libraryVariants.all
{
variant ->
// add final apk to the 'archives' configuration
project.artifacts
{
archives variant.outputFile
}
}
def localRepoPath = "file://" + new File(
System.getProperty("user.home"), ".m2/repository").absolutePath
uploadArchives
{
repositories.mavenDeployer
{
repository(url: localRepoPath)
addFilter('debug') { artifact, file ->
artifact.name.contains("debug")
}
addFilter('release') { artifact, file ->
artifact.name.contains("release")
}
pom('debug').groupId = 'com.company'
pom('release').groupId = 'com.company'
pom('debug').artifactId = 'id'
pom('release').artifactId = 'id'
pom('debug').version = android.defaultConfig.versionName + "d"
pom('release').version = android.defaultConfig.versionName
pom.packaging = 'aar'
}
}
uploadArchives.dependsOn(assemble)
However, when trying to refactor the pom configuration :
uploadArchives
{
repositories.mavenDeployer
{
repository(url: localRepoPath)
addFilter('debug') { artifact, file ->
artifact.name.contains("debug")
}
addFilter('release') { artifact, file ->
artifact.name.contains("release")
}
pom.groupId = 'com.company'
pom.artifactId = 'id'
pom('debug').version = android.defaultConfig.versionName + "d"
pom('release').version = android.defaultConfig.versionName
pom.packaging = 'aar'
}
}
artifactId is expanded as the name of the output file, and groupId as the name of the root directory ; thus giving bad paths in the maven repo.
I'd like to know why is that, and maybe if there is a cleaner way to achieve what I need.
As a reference, this is how we upload multiple APKs. It may not be exactly what you needed since we are uploading multiple APKs after APK splits, while you were trying to upload multiple APKs from different build types (debug & release). But in theory, they should be the same.
//declare some Variables for later use
def projectName = "ProjectName"
def versionString = "1.0.0"
def baseVersionCode = 1
// Values based on https://developer.android.com/ndk/guides/abis.html#sa
ext.abiCodes = ['armeabi-v7a': 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4]
// collect artifacts, important for the `uploadArchives` to work
artifacts {
if (new File('app/build/outputs/apk').exists()) {
new File('app/build/outputs/apk').eachFile() { file ->
if (file.toString().contains("productionRelease")) {
archives file: file
}
}
}
}
uploadArchives {
repositories {
mavenDeployer {
repository(url: "http://...")
project.ext.abiCodes.values().each{ abiVersionCode ->
def version = "${versionString}.${baseVersionCode + abiVersionCode}"
addFilter(version) { artifact, file -> artifact.name.contains(version) }
pom(version).artifactId = projectName.toLowerCase()
pom(version).groupId = 'com.yourcompanyname'
pom(version).version = version
}
}
}
}
android {
defaultPublishConfig "productionRelease"
buildTypes {
productionRelease {
minifyEnabled false
zipAlignEnabled true
//...more Config like proguard or signing
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
def abiVersionCode = project.ext.abiCodes.get(abiName)
output.versionCodeOverride = variant.versionCode + abiVersionCode
output.versionNameOverride = "$versionString (${output.versionCodeOverride})"
def apkName = "$projectName-${variant.name}-v${versionString}.${output.versionCodeOverride}.apk"
output.outputFile = new File(output.outputFile.parent, apkName)
}
}
You need(ed) to set the filter name for the groupId and artifactId the same as you have for version
pom('debug').groupId = 'com.company'
pom('release').groupId = 'com.company'
pom('debug').artifactId = 'id'
pom('release').artifactId = 'id'
pom('debug').version = android.defaultConfig.versionName + "d"
pom('release').version = android.defaultConfig.versionName
Im surprised you get away with the version name suffix, as its not semver.