Publish a Java library to Maven Central without Maven or Gradle

by: Ethan McCue

Say, like me, you have some code you want to share with the world.

package dev.mccue.datastructures;

/**
 * "Sum Type" representation of a linked list.
 */
public sealed interface LinkedList<T> {
    /**
     * An empty list.
     */
    record Empty<T>() 
        implements LinkedList<T> {}
    /**
     * A not empty list.
     */
    record NotEmpty<T>(T first, LinkedList<T> rest) 
        implements LinkedList<T> {}
}

To do this, you need to put that code in a place others can find it.

For Python programmers this means publishing to PyPI, Javascript programmers to npm, Rust programmers to crates.io, and C++ programmers to somewhere I assume.

For Java there are a few options, but the only one that will work by default in every build tool is Maven Central. Its apparently really good at being a repository, so publishing there is the thing to do.

There are plugins for all the major build tools that do this. However, last I tried, uploading a Java 16+ library to Maven Central using Maven was busted and requires exposing Java internals to work around.

So we are going to do something a little different. I am going to show you how to go through the entire process manually in the hope that it is straightforward enough to write your own scripts to do.

Prerequisites to follow along

Java

javac --version
jar --version
javadoc --version

gpg

gpg --version

curl

curl --version

git

git --version

Github CLI

gh --version

Step 1. Write your code

For this example I am going to put the linked list code from the top of the page in a file src/dev/mccue/datastructures/LinkedList.java and make a small .gitignore.

target/
.idea/
*.iml
.DS_Store

Step 2. Add your code to a git repo

git init
git add src/
git add .gitignore
git commit -m "Initial Commit"

Step 3. Put that git repo on the internet

You will need a public url to refer to later and services like Github are convenient for that.

gh auth login
gh repo create --public --source .
git branch -M main
git push origin main

Step 4. Get unique coordinates

Unlike other package repositories, Maven Central requires that you have a unique "group id" to prefix any packages you make. You cannot publish code under com.google, only Google can.

To meet this requirement you either need to

Step 5. Make an account with Sonatype

Once you got that all settled

  1. Make an account here. Save the username and password.
  2. Make a ticket here. You will need to prove that you own the website or git account that you want to use for your group id.

This is an annoying step, I know, but it is what it is. If you get caught here ask in the comments below and I'll add more clarification.

Step 6. Compile your code

javac -d target/classes -g --release 17 src/**/*.java

The -g includes debug information. Always do that.

Step 7. Generate documentation for your code

javadoc -d target/doc src/**/*.java

If you get warnings about undocumented classes and methods ignoring them is a choice you are technically allowed to make.

Step 8. Decide on a version number

When you publish code there is the implicit assumption that you might upload newer versions of that code at a later point in time. To distinguish between versions, you need to number them. There are a few schemes for doing this including Semver, Calver, and 0ver.

In the commands from this point on, I am going to assume that the initial version being published is 0.0.1, but you can do what you feel is best.

Step 9. Zip your compiled code into a jar

As early minecraft players learned when installing mods, Jar files are just zip files with a few extra bells and whistles.

mkdir target/deploy
jar --create \
  --file target/deploy/datastructures-0.0.1.jar \
  -C target/classes .

Step 10. Zip your source code into a jar

jar --create \
  --file target/deploy/datastructures-0.0.1-sources.jar \
  -C src .

Step 11. Zip your documentation into a jar

jar --create \
  --file target/deploy/datastructures-0.0.1-javadoc.jar \
  -C target/doc .

Step 12. Create a POM File

A POM - "Project Object Model" - file is the standard format for declaring information about your library including any dependencies it may have on other libraries. This format is going to be around forever and all build tools have to handle it.

The following I am going to put into target/deploy/datastructures-0.0.1.pom. This is the "minimal" POM and every field I list needs to be specified.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
                
    <groupId>dev.mccue</groupId>
    <artifactId>datastructures</artifactId>
    <version>0.0.1</version>
    <packaging>jar</packaging>
                
    <name>Datastructures</name>
    <description>Basic Datastructures for Java.</description>
    <url>https://github.com/bowbahdoe/java-datastructures</url>
                
    <licenses>
        <license>
            <name>The Apache Software License, Version 2.0</name>
            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
        </license>
    </licenses>
                
    <developers>
        <developer>
            <name>Ethan McCue</name>
            <email>ethan@mccue.dev</email>
            <organization>McCue Software Solutions</organization>
            <organizationUrl>https://www.mccue.dev</organizationUrl>
        </developer>
    </developers>
                
    <scm>
        <connection>scm:git:git://github.com/bowbahdoe/java-datastructures.git</connection>
        <developerConnection>scm:git:ssh://github.com:bowbahdoe/java-datastructures.git</developerConnection>
        <url>https://github.com/bowbahdoe/java-datastructures/tree/main</url>
    </scm>
</project>

Step 13. Create a GPG Key

Okay so this part might feel wierd.

The idea here was that you generate a public and private key. You sign all the files you upload with those keys and then later on someone can confirm that it was "you" that actually did that signing.

Maven Central just makes sure that everything is signed, not that there is any way to associate the signed files back to you. Because public key infrastructure never really took off, this step is largely ceremonial in practice. You still need to do it though.

The official guide is more comprehensive than I am going to be

gpg --gen-key

Make sure to save your passphrase if you made one.

Step 14. Distribute your GPG Key

Run this command

gpg --list-keys

And you should get output that kinda looks like this.

pub   rsa3072 2021-06-23 [SC] [expires: 2023-06-23]
      CA925CD6C9E8D064FF05B4728190C4130ABA0F98
uid           [ultimate] Central Repo Test <central@example.com>
sub   rsa3072 2021-06-23 [E] [expires: 2023-06-23]

You want to take the part that looks like CA925CD6C9E8D064FF05B4728190C4130ABA0F98 and run the following command.

gpg --keyserver keyserver.ubuntu.com \
  --send-keys CA925CD6C9E8D064FF05B4728190C4130ABA0F98

Step 15. Sign all the files with GPG

gpg --armor --detach-sign target/deploy/datastructures-0.0.1.jar
gpg --armor --detach-sign target/deploy/datastructures-0.0.1-sources.jar
gpg --armor --detach-sign target/deploy/datastructures-0.0.1-javadoc.jar
gpg --armor --detach-sign target/deploy/datastructures-0.0.1.pom

If you are scripting this you should add --pinentry-mode loopback and provide your passphrase via --passphrase.

Step 16. Zip all the jars into one large jar

Yes, we are making a jar jar.

Jar Jar Binks

The most convenient api for uploading code manually is a form submit on the gui that is undocumented. I wanted to use something more official, but I had trouble finding what to do. I think its probably fine.

Said api wants one large jar as its input.

jar --create --file target/bundle.jar -C target/deploy .

Step 17. Log in to sonatype

Use the username and password you got from step 5.

curl --request GET \
  --url https://s01.oss.sonatype.org/service/local/authentication/login \
  --cookie-jar cookies.txt \
  --user USERNAME:PASSWORD

Step 18. Upload the bundle to a staging repository

curl --request POST \
  --url https://s01.oss.sonatype.org/service/local/staging/bundle_upload \
  --cookie cookies.txt \
  --header 'Content-Type: multipart/form-data' \
  --form file=@target/bundle.jar

When you run this command, you will get output back that looks like this

{"repositoryUris":["https://s01.oss.sonatype.org/content/repositories/STAGING_REPOSITORY_ID"]}

At this point, you can pause and point a build tool to the staging repository to make sure that everything is okay with your code before releasing the final version.

Step 19. Release the staging repository

Fill in the STAGING_REPOSITORY_ID from the output of the last command. There is no going back once the staging repostory is released.

curl --request POST \
  --url https://s01.oss.sonatype.org/service/local/staging/bulk/promote \
  --cookie cookies.txt \
  --header 'Content-Type: application/json' \
  --data '{ 
    "data": {
        "autoDropAfterRelease": true,
        "description": "",
        "stagedRepositoryIds": ["STAGING_REPOSITORY_ID"]
    }
}'

You can try out the linked list we just published by including it in your build tool of choice.

<dependency>
    <groupId>dev.mccue</groupId>
    <artifactId>datastructures</artifactId>
    <version>0.0.1</version>
</dependency>

A fully scripted version of this process can be seen here along with an associated Github workflow

Explain what a Maven MOJO is in 140 characters or less in the comments below.


<- Index