I explain how to use a Gradle task to ensure all of your project’s dependencies are configured as an input enable Nexus Lifeccycle (IQ Server) to better integrate with Android.
Down the rabbit hole we go
First of all — why do these tools need dependencies as input instead of just an APK? Isn’t it awkward? Well, can be awkward, but it’s the only way to do it since Proguard cuts out and obfuscates large pieces of code before it’s being baked into the APK. You can find more on the topic here.
Thankfully we don’t have to start completely from scratch because Gradle has a specific task to help us out. It’s called COPY — you just need to specify it with a path from which you copy things and a path where you copy things into.
Here’s an example of a task copying some kind of docs you have in your project to the resulting build folder.
task copyDocs(type: Copy) {
from ‘src/main/doc’
into ‘build/target/doc’
}
And you can execute it like:
./gradlew copyDocs
Alright, now we know that we can copy things with gradle to an arbitrary directory, but how do we know where to copy our dependencies from? There is no libs directory created in your project to keep all of them, right? So where are they?
Turns out that Gradle downloads and caches all your dependencies between builds to an obscure internal directory outside of your project. Regardless of where it is — Gradle HAS to know it to successfully link with the dependencies during the build. Odds are it creates some kind of a lookup dictionary during it’s configuration stage — you know, the one that keeps annoying you with a message along the lines of “Your Gradle file is out of sync, press here to refresh” every time you change the gradle.build file. In fact, the dictionary that Gradle creates indeed is itself called Configuration.
Remember how for each dependency you specify whether it is ‘compile’ or ‘implementation’ or ‘api’? Well, that’s called Configuration Name and you can access dependencies of ‘compile’ configuration when writing a Gradle task like this:
task copyCompileDependencies(type: Copy) {
into ‘build/lib’
from configurations.compile
}
Which means that all of the dependencies of ‘compile’ configuration will be copied to ‘build/lib’ directory.
So are we done? Should we just run this task and then run the aforementioned command line call?
Not really, no. Starting from Android Gradle Plugin 3.0 there will be no dependencies stored in ‘configurations.compile’ and if you run the task from above — there is going to be no output.
Fateful Deprecations
Configuration ‘compile’ was deprecated by Android Gradle plugin for the same exact reason as we are now trying to exploit — it was exposing all dependencies to the build even those that did not need to be synced and rebuilt. Now Android has two configurations to substitute it — ‘implementation’, which obscures transitive dependencies from the build and ‘api’ that is acting in the same exact way as ‘compile’. More on the topic here
Unfortunately, replacing all ‘compile’ and ‘implementation’ with ‘api’ will not help us for two reasons:
Transitive dependencies: There are some transitive dependencies declared in third-party build.gradle files that you won’t be able to change unless you fork the dependency that you are using.
Flexible build configurations: Configurations ‘implementation’ and ‘api’ are forbidden to be used directly in a Gradle task because the dependencies they use will change according to your build type and flavour. So ‘configurations.api’ will not work at all and your project’s build.gradle file will fail to sync.
It’s not a dead-end though, just a setback. After all — your project still compiles and the configuration files were generated based on something, so deep inside the JVM compilation process these dependencies are being pulled in.
Make it work
All of the Gradle’s configuration objects are being built using good old java classpath under the hood and lucky for us we can get to it once we know what is the particular build-type and the flavor that is being used to compile the project. To do that we can use Gradle’s own mechanism for generating tasks dynamically, on the fly during the configuration stage.
Check this out:
android.applicationVariants.all { variant ->
task “copyDependencies${variant.name.capitalize()}”() {
outputs.upToDateWhen { false }
doLast {
println “Executing task
copyDependencies${variant.name.capitalize()}”
}
}
}
This piece of code will generate a task for each build variant that will always execute — no matter whether it’s actually considered to be “upToDate” and output the string with the full task name including the variant!
After syncing your project you could run it like
./gradlew copyDependenciesStagingDebug
The actual paths to dependency files we are looking for are located in variant.getCompileClasspath()
Holdup though — tools like Nexus Lifecycle (IQ Server) use the name of the file as the name for it’s entry in the report it generates. Why does it matter?
Some of the dependencies are not in .jar format and could be an exploded .aar instead, in such case the actual binary cached by Gradle will be named classes.jar, meaning it will be conflict with other .aar dependencies and will end up being overwritten by them in the report by Nexus IQ.
To account for that we will have to go manually through the classpath, iterating over all dependencies and renaming the ones that are called classes.jar into something more legible.
You can find the resulting code to get the dependencies here.
And that’s it — now you have all your dependencies copied to a separate folder and you can use them however you want including as an input in Nexus Lifecycle (IQ Server) as I discussed in part one.
A version of this article originally appeared in ProAndroidDev and is republished here with permission from the author.
Written by Nikita Belokopytov
Nikita Belokopytov is a Senior Software Engineer @ Quandoo. He loves lean startup and design thinking.