Oh No - Another Publishing Android Artifacts to Maven Central Guide?
Yes this is another guide on how to publish Android artifacts to Maven Central. Unlike other guides, it does cover extra “things” though:
- Gradle build files using Kotlin DSL.
- Publish AAR file artifacts.
- The publication includes a Source Jar file (hello
withSourcesJar
). - The publication includes a Javadoc Jar file (hello
withJavadocJar
). - The publication works with a CI/CD pipeline.
- Did I mention this is Android only?
For those who don’t like reading, here are two of my repos using the explained publish mechanism:
Sonatype OSSRH
Maven Central is really just a facade to multiple repositories. One such repository is Sonatype OSSRH. In order to publish your artifacts you need an account with them (or another repository provider). There’s a bunch of good articles on how to open an account and configure it properly. I’ll just reference those here and be done with it (it’s a cumbersome process so make sure you have time…):
- https://central.sonatype.org/publish/publish-guide
- https://mrcurious.medium.com/publishing-your-android-kotlin-library-to-maven-central-in-2021-df263a4f2cbc
- https://getstream.io/blog/publishing-libraries-to-mavencentral-2021
- https://dev.to/madhead/no-bullshit-guide-on-publishing-your-gradle-projects-to-maven-central-3ok4
- https://www.waseefakhtar.com/android/publishing-your-first-android-library-to-mavencentral
Plugins
At this point the assumption is:
- You have an account with Sonatype OSSRH.
- You created a repository with Sonatype OSSRH and have verified ownership of your domain.
- You created a GPG key pair for signing artifacts, have published your public key and exported your private key.
- You already have an app with build files in Kotlin DSL (compared to Groovy build files) -> build.gradle.kts.
Now step one is to add two plugins to your build file:
plugins {
id("com.android.library")
id("maven-publish")
id("signing")
}
- maven-publish: provides the ability to publish build artifacts to an Apache Maven repository
- signing: used to sign all artifacts and metadata files that make up a publication
Publish
Step two is to create and configure the publishing extension by adding this to your gradle build file:
afterEvaluate {
publishing {
publications {
// here goes your configuration
}
}
}
The afterEvaluate
makes sure the project has been evaluated and can be accessed in the config block.
The configuration consists of three steps:
- Configure the repositories
- Configure the publication
- Sign the artifacts
afterEvaluate {
publishing {
publications {
// 1. configure repositories
// 2. configure publication
// 3. sign the artifacts
}
}
}
Repositories
This part of the configuration defines the target repository. In our case it’s the Sonatype OSSRH, either the release or the snapshot repository.
The determination of the target repository is based on the version name. A version with a “-SNAPSHOT” suffix will be uploaded to the snapshot repo.
Publication
Configuring the publication is pretty straight forward.
First we define the artifacts to publish:
from(project.components["release"])
artifact(tasks.named<Jar>("withJavadocJar"))
artifact(tasks.named<Jar>("withSourcesJar"))
project.components["release"]
are the artifacts produced by the Android project which in case of an Android library should be an aar file. The other two lines declare the Javadoc and the source jar file as artifacts to publish. Later in this article I’ll explain how these two artifacts are produced.
The rest of the configuration defines the Maven POM file, which is an XML representation of a Maven project (project meta information).
The most important properties to configure are the groupId, the artifactId and the version (e.g. for com.android.tools.build:gradle:7.0.2
, the groupId is com.android.tools.build, the artifactId is gradle and the version is obviously 7.0.2). All the other properties are optional parameters to define the software license, identify the company/developer, the source code etc.
As you can see the properties are all read from project.properties
which are the properties defined in one of the gradle.properties
file (either the project or the library specific properties file). Here’s an example of such a properties file: https://github.com/1gravity/Android-RTEditor/blob/master/RTEditor/gradle.properties.
Note that the username and the password for the repository aren’t defined in a project properties file but should be defined in your ~/.gradle/gradle.properties
file (~ stands for your home directory) so that the credentials aren’t committed to source control. If you don’t have that file, you need to create it and add the credentials:
ossUsername=<your OSS user>
ossPassword=<your OSS password>
Signing
Signing the artifacts is simple:
signing {
sign(publishing.publications.getByName(publicationName))
}
The signing plugin uses the key you created earlier (see “You created a GPG key pair for signing artifacts, have published your public key and exported your private key”). The plugin needs the private key file, the keyId and the password which you define in your ~/.gradle/gradle.properties
file. Add this to the file:
signing.secretKeyRingFile=/<user>/.gnupg/secring.gp
g
signing.keyId=<last 8 characters of your key id>
signing.password=<the password>
Note that ~/.gnupg/secring.gpg
won’t work since Android isn’t able to resolve ~ to your home directory.
That’s it. If you run ./gradlew publish
from your project directory it will build and publish your library to Sonatype OSSRH. You still need to manually close and publish the library and while that can be automated too, you should be familiar with the manual process: https://central.sonatype.org/publish/release/.
Signing with CI/CD
Most build pipelines read secrets as strings but not from files (like the secretKeyRingFile
we use above). While there are ways to feed files into a pipeline (for BitBucket Pipelines a description can be found here), it’s a rather cumbersome process. Better to avoid files at all.
In our case we can use in-memory PGP secret keys and passwords by calling useInMemoryPgpKeys
before creating the signing task:
The three parameters are read from a property file if compiled locally, so put these values in your ~/.gradle/gradle.properties file:
signingKeyId=<last 8 characters of your key id>
signingKeyPassword=<the password>
signingKey=<the key>
The key needs to be “armored” meaning converted to an encrypted representation of the file consisting entirely of text-mode only / ASCII characters. It also needs to be stripped of the typical wrapper gpg puts around the actual key (the “ — — -BEGIN PGP PRIVATE KEY BLOCK — — -” parts).
Use this one-liner to get the key value:
gpg --export-secret-keys --armor <KEY_ID> |grep -v '\-\-' |grep -v '=.' |tr -d '\n'
To run this in a build pipeline, you need to configure the secrets in the according secrets manager. For GitHub this looks like this:
When defining the pipeline you need to pass in the secrets as parameters for gradle/gradlew.
For GitHub Actions the syntax is:
./gradlew -PossUsername=${{ secrets.OSSRH_USERNAME }}
For BitBucket Pipeline the syntax is:
./gradlew -PossUsername=$OSSRH_USERNAME
withSourcesJar
In Java projects there’s an easy way to create Jar files for the source and the Javadoc:
java {
withSourcesJar()
withJavadocJar()
}
Unfortunately this doesn’t work in an Android project. Replicating withSourcesJar is rather simple though:
tasks {
register<Jar>("withSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs)
}
}
This registers a task named withSourcesJar
. It creates a Jar file (hence the Jar
type definition and uses the Android source directories (android.sourceSets.getByName(“main”).java.srcDirs
) as input for the jar file.
withJavadocJar
Creating a jar file for the Javadoc is equally simple:
tasks {
archiveClassifier.set("javadoc")
dependsOn(named("withJavadoc"))
val destination = named<Javadoc>("withJavadoc")
.get()
.destinationDir
from(destination)
}
The only problem is that we need to create the Javadoc first before we can create the Jar file. As you can see above we’re referencing a task named withJavadoc
that creates the actual documentation:
Summary
This is the code all put together:
That code is used here:
and here:
As usual if you have suggestions or questions, don’t hesitate to comment.
Happy coding!
Addendum
If you think publishing on Maven Central is cumbersome, you’re not alone. IMO there’s a better/much easier alternative: The better Maven Central.