43

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.

Graham Borland
  • 60,055
  • 21
  • 138
  • 179
  • 1
    Have you tried to use variant.packageApplication.outputFile like described here http://stackoverflow.com/questions/18534313/gradle-applicationvariants-all-skips-one-variant ? – sergej shafarenka Jan 31 '14 at 15:26
  • What would happen if you replace the from line with "from assembleDevDebug" – Ethan Feb 01 '14 at 20:32
  • This "custom logic to inject the version number into the APK filename", would you mind sharing that? I want it, but my Gradle-fu is weak as well :) – Thomas Oct 29 '15 at 18:12

5 Answers5

29

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.

Xavier Ducrohet
  • 28,383
  • 5
  • 88
  • 64
  • 3
    I get `Error:(270, 0) Could not find property 'outputFile' on com.android.build.gradle.internal.api.ApplicationVariantImpl_Decorated@4be85d24.` – phreakhead Aug 15 '15 at 04:40
  • 2
    relpace :`variant.outputs[0].outputFile` – ipcjs Oct 26 '15 at 15:31
  • 1
    This no longer works because variant.outputFile is deprecated as of gradle 3.0.0-alpha3 - the output in the gradle console is: "Cause: getMainOutputFile is no longer supported." Would you know of a fix? – ElectronAnt Jun 07 '17 at 15:06
  • To get output file working wrap additionnaly `applicationVariants.all { variant -> variant.outputs.all { output ->` – MainActivity Dec 20 '18 at 13:14
9

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
Ladios Jonquil
  • 1,576
  • 1
  • 9
  • 4
  • 1
    This doesn't seem to work when it comes to copying the mapping file into the zip. variant.processResources.proguardOutputFile.parent returns the intermediates/proguard folder which doesn't contain the mapping.txt file. – Pete Jul 31 '14 at 02:24
  • @Pete `variant.mappingFile.parent` is what you need instead – TWiStErRob Oct 26 '14 at 18:01
  • I've modified the code a little, to output the archive name during build. But the strange thing is that when I click Build -> Generate Signed APK, in the Gradle Console I can see the filename of the archive, but the archive isn't created or something. Whats more is that the apk is not added to the archive. Check my build.gradle here: http://pastebin.com/Y8berupv. Thanks! – tim687 Nov 01 '15 at 14:52
  • I like the concept here, but many of the fields (packageName, eg) don't seem to be present in latest beta of AS. – RabidMutant Aug 27 '16 at 03:26
7

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]
                }
            }
        }
    }
}
Pedro Andrade
  • 4,556
  • 1
  • 25
  • 24
  • This is kind of working for me, when I click the run icon in Android Studio, it does the task and actually copy something. When I click Generate Signed apk, does the task, but nothing is copied. – tim687 Nov 09 '15 at 18:39
  • '.finalizedBy' did it for me, '.dependsOn' did not (No explanation ATOW) – Pascal Sep 28 '16 at 11:58
6

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"
        }

    } 
}
martinpelant
  • 2,961
  • 1
  • 30
  • 39
4
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
    }
}
ipcjs
  • 2,082
  • 2
  • 17
  • 20