I am using the Android Gradle plugin to build an APK file. I want to sign the output APK file.
I know about the Android plugin’s signing capabilities, but I want to sign the resulting APK as an arbitrary file with a GPG key.
I tried finding the task that creates said artifact, but it is not a task of type Jar, Archive, or similar, a requirement of the signing plugin.
So far I have been successful with this approach:
register("sign") {
group = "signing"
dependsOn(build)
doLast {
val outputDirectory = layout.buildDirectory.dir("outputs/apk/release").get().asFile
val integrationsApk = outputDirectory.resolve("${rootProject.name}-$version.apk")
org.gradle.security.internal.gnupg.GnupgSignatoryFactory().createSignatory(project).sign(
integrationsApk.inputStream(),
outputDirectory.resolve("${integrationsApk.name}.asc").outputStream(),
)
}
}
Thank you. This works like you said. With your solution, I had to depend on the “assembleRelease” task in task foo. I am trying to understand why two tasks were used here.
For example, the following works, too:
val assembleAndSignApk by registering {
dependsOn("assembleRelease")
val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk")
inputs.file(apk)
outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") })
doLast {
signing {
useGpgCmd()
sign(*inputs.files.files.toTypedArray())
}
}
}
I noticed a difference, though. Without the dependency, I get an error regarding the input file missing, whereas with your solution, I get an error about the dependency missing instead.
I want to understand why two tasks are needed and why my approach causes a different error when the dependency is missing.
With your solution, I had to depend on the “assembleRelease” task in task foo.
That sounds like it makes no sense at all.
The foo task in my example simply is the task generating whatever file you want to sign.
It just shows that you can sign any output file of any task just fine, in the example a text file.
Manual dependsOn where the left-hand side is not a lifecycle task is practically always wrong and a code-smell that signals that you are not properly wiring inputs and outputs together.
I am trying to understand why two tasks were used here.
foo is the task that generates whatever you want to sign, signFoo is the task that signs it.
Actually, you could also do the signing in a doLast action of the task generating the artifact and add the signature file as output file.
But I like to have clear responsibilities. And with the separation if for example the signature file is deleted, only the signing needs to be redone.
For example, the following works, too:
But is full of bad practices.
For example:
never add explicit dependsOn unless the left-hand side is a lifecycle task; in practically all other cases it is a sign of somewhere not wiring task input and outputs together
your sign task depends on assembleRelease to get the APK built, but I’d guess, that this task is not the one that generates the APK, and thus you are depending on way too much
using manual paths is a bad idea, instead use the settings of the other task.
I noticed a difference, though. Without the dependency, I get an error regarding the input file missing,
Because you did not wire the tasks properly.
You depend on assembleRelease which is most probably not the task generating the APK and as I said, a manual dependsOn is a bad idea anyway.
whereas with your solution, I get an error about the dependency missing instead
Only if you modify it in a bad way.
The code I showed is perfectly fine and not missing any dependency.
That good and all, but given that I tried to apply it to my original question, and you claim I shouldn’t do it the way I did, I don’t see how helpful it is.
To repeat my OP:
How would I realize that given your code snippet.
not wiring task input and outputs together
I don’t know any Android Gradle plugin task I can write the output from to sign.
but I’d guess, that this task is not the one that generates the APK, and thus you are depending on way too much
When I tried your snippet without depending on the task I did, it gave the error that I need to somehow directly or indirectly depend on the assemble task, mentioning that specific task. I don’t know how to depend on something that does “less” because I don’t know any other task that produces these artifacts. I tried to iterate through the tasks and checked each of their outputs, too.
I modified:
val output = layout.buildDirectory.file("foo.txt")
to
val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk")
Leave out foo but instead use the actual task that produces the file.
Besides that, I would have guessed that the task that creates the APK file indeed is “a task of type Jar, Archive, or similar”, as an APK file indeed is just a zip file with specific content layout. So if you would use the correct task that creates that APK File, I think you could indeed just do at top-level the
signing {
sign(theTaskThatCreatesTheApk)
}
and get the signing task and so on created automatically.
So if you find the correct task instead of build or assembleRelease which are not the tasks that create the apk, then those three lines should all you need.
I am unaware of any task besides assembleRelease and the other tasks that use it that produces the artifact.
I thought that too but why when I used your example with the path to the APK as an input file it gave an error mentioning I need to depend on assembleRelease, while assembleRelease is not such an Archive task?
I’ll CTRL+F the codebase of the plugin for such a task.
I’m not an Android dev, so I don’t know what assembleRelease is or does.
Maybe it is a Sync or Copy task that just copies things around and not the actual task creating that file?
Then if you need to sign it there in the output directory, we are back to doing it like the example I showed.
But I cannot directly recommend how to do it without an MCVE showing the situation but can only give you general advice on how to do it.
If it is just a sync / copy task, maybe you should instead find the task that actually creates that file, configure the signing there, and then add the signing task to the inputs of the sync / copy task, …
But as I said, hard to give concrete advice without seeing the actual setup.
The code implies there might be more than one packageRelease task. But when would that be the case?
Why does named("packageRelease") not work and instead you have to use named { it == "packageRelease" }?
Using your snippet I get:
* What went wrong:
Could not determine the dependencies of task ':app:assembleRelease'.
> Could not create task ':app:packageRelease'.
> DefaultTaskContainer#create(String, Class, Action) on task set cannot be executed in the current context.
The code implies there might be more than one packageRelease task.
Well, theoretically a TaskCollection can contain tasks from multiple projects, so you could have in one task collection the tasks :foo:packageRelease and the tasks :bar:packageRelease which both have the name packageRelease. Or you could have no task with that name in the given task collection.
Why does named(“packageRelease”) not work and instead you have to use named { it == “packageRelease” }?
Because named("packageRelease") only works if the task is already registered at the point in time you call that method. tasks.named { it == "packageRelease" } matches all tasks, no matter whether already registered or registered in the future, with that name. The .configureEach { ... } makes sure this filtered collection is treated lazily and according to task-configuration avoidance, not triggering tasks to be realized when they shouldn’t.
Using your snippet I get:
That’s bad, that means the time the configureEach action is triggered it is too late to create the task for the signing.
Seems you currently need to take a little detour over a configuration to not break task-configuration avoidance.
This seems to work as expected:
val signThisShit = configurations.dependencyScope("signThisShit")
tasks.named { it == "signSignThisShit" }.configureEach {
dependsOn("packageRelease")
}
tasks.named { it == "packageRelease" }.configureEach {
artifacts.add(signThisShit.get().name, this)
}
signing {
sign(signThisShit.get())
}
Yes, the manual dependsOn is a bad, but at least better than afterEvaluate and it seems currently necessary for such a situation. Hopefully with Gradle 9 things get better when everything is Propertyfied.
Wouldn’t it make sense to call the former function registeredNamed("")? Otherwise, how else would you know that?
named { } and named() both look the same to me apart from one asking for a lambda and the one for a regular argument, neither one conveys that the task must already be registered or not.
Seems like the packageRelease task is not suitable either?
Could not determine the dependencies of task ':app:assemble'.
> Could not create task ':app:assembleRelease'.
> Cannot convert the provided notation to an object of type ConfigurablePublishArtifact: task ':app:assembleRelease'.
The following types/formats are supported:
- Instances of ConfigurablePublishArtifact.
- Instances of PublishArtifact.
- Instances of AbstractArchiveTask, for example jar.
- Instances of Provider<RegularFile>.
- Instances of Provider<Directory>.
- Instances of Provider<File>.
- Instances of RegularFile.
- Instances of Directory.
- Instances of File.
- Maps with 'file' key
I have tried with assemble, build and packageRelease but they all were giving the same error. If I need another task, I would not know how to find out which one. Do you have any suggestions?
Wouldn’t it make sense to call the former function registeredNamed(“”)? Otherwise, how else would you know that?
By using Gradle since pre-1.0, or by reading the documentation.
Maybe another name would make more sense, but when named(...) was added there was not even task-configuration avoidance a thing.
And named { ... } was just added in one of the latest releases.
If you want methods renamed, you need to discuss this with Gradle folks.
Besides that registeredNamed would not really be any clearer than named.
You can also argue that there is absolutely no sense in assuming you get a task that might be added somewhen in the future when you call a method that is called tasks.named("..."). tasks.named { ... } on the other hand is a filter on top of that task collection just like other methods like .withType<...>() or .matching { ... } and are documented to be live and also getting the future values.
neither one conveys that the task must already be registered or not.
And no name would really make it any clearer, you can always just know what you do or look at the documentation, especially when being aware that some methods might be behave one way and some the other.
Seems like the packageRelease task is not suitable either?
Again, as you do not provide proper MCVE I can only half-guess you situations and can only give general advice and hints and not ready-made solution, which is anyway not what you should expect.
Besides that you show an error about assembleRelease, not packageRelease and as you did not even provide a stacktrace or build --scan or what you made out of the example I can hardly guess what might be wrong. In absence of proper information I just tested with a trivial Jar task named packageRelease that was added in an afterEvaluate and like that reproduced the error you wrote before.
And when I run the publishing task no signature is generated. Using what i marked as the current solution in this thread instead works on the other hand.