Copying APK file in Android Gradle project
Asked Answered
R

5

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.

Recession answered 29/1, 2014 at 14:51 Comment(3)
Have you tried to use variant.packageApplication.outputFile like described here #18534813 ?Strigil
What would happen if you replace the from line with "from assembleDevDebug"Goins
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 :)Atomize
K
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.

Kikelia answered 3/2, 2014 at 17:39 Comment(4)
I get Error:(270, 0) Could not find property 'outputFile' on com.android.build.gradle.internal.api.ApplicationVariantImpl_Decorated@4be85d24.Disinfest
relpace :variant.outputs[0].outputFileSegmental
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?Motif
To get output file working wrap additionnaly applicationVariants.all { variant -> variant.outputs.all { output ->Shambles
X
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
Xyloid answered 6/2, 2014 at 17:48 Comment(4)
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.Swordplay
@Swordplay variant.mappingFile.parent is what you need insteadChunk
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: pastebin.com/Y8berupv. Thanks!Siobhansion
I like the concept here, but many of the fields (packageName, eg) don't seem to be present in latest beta of AS.Scribbler
M
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]
                }
            }
        }
    }
}
Misshape answered 23/9, 2014 at 20:32 Comment(2)
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.Siobhansion
'.finalizedBy' did it for me, '.dependsOn' did not (No explanation ATOW)Face
F
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"
        }

    } 
}
Fairminded answered 19/2, 2014 at 10:24 Comment(0)
S
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
    }
}
Segmental answered 28/10, 2015 at 2:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.