Skip to content

Instantly share code, notes, and snippets.

@mattniehoff
Last active October 3, 2019 18:35
Show Gist options
  • Save mattniehoff/d649d95a8d98451d21be51a17dad1034 to your computer and use it in GitHub Desktop.
Save mattniehoff/d649d95a8d98451d21be51a17dad1034 to your computer and use it in GitHub Desktop.

Latest Revision: 2019-10-03 by Matt Niehoff

Maven Central

This file documents a process for getting your repository up on Maven Central. There may be other approaches that work, but this approach successfully uploaded the pdfreader-android project.

This guide is meant to be followed from top to bottom. It is not exhaustive of all issues you might encounter and is no guarantee that the process won't behave differently on your computer. It was done on a project built in Android Studio for the Windows operating system. There are links at the bottom of this document where you can explore each area futher as you have need or interest.

Table of Contents

Permissions to publish from Sonatype

Maven Central is managed by the company Sonatype. They designate who can publish packages under certain domains in their Maven Central Repositories. This is the mavenCentral() you include in your repositories tag in the build.gradle file.

To begin, start by following the steps in the Sonatype OSSRH Guide, scroll down to the Initial Setup section and:

1. Create a JIRA account.

Sonatype uses your JIRA credentials to manage who can publish to a domain. You will pass along your JIRA credentials as part of the publishing process.

You might consider using a department account for the initial setup and creating more accounts for individuals you want to have publishing rights.

2. Create a New Project ticket.

Filling out the Form

On the New Project Ticket page, you'll enter the details of your project.

Summary - Something along the lines of "Organizational Open Source Projects"

Description - Briefly describe what you are uploading and what domain you are using. Example:

Wanting to upload a library used in an open source project to maven central in the edu.umn.minitex namespace. The provided Project and SCM urls point to the repository for this project.

Group Id - The domain you are using. Example: org.nypl or edu.umn.minitex.

Project URL - Project Website if there is one, otherwise just the online source control repository.

SCM URL - Link to the source control management system (GitHub, GitLab, etc.).

Username(s) - Here you should provide the JIRA account name you created with Sonatype. If you have multiple developers you can have them create accounts and add their names here as well.

Providing Validation Information

Depending on what information you provide, Sonatype will in some way require you to verify that you have the rights to publish to the domain you are asking for.

In requesting edu.umn.minitex, it helped that I applied from an email address ending in @minitex.umn.edu. In our case it turned out someone already published as part of edu.umn so they asked us to find that person to comment on the ticket. Unfortunately we were unable to track that down, so Sonatype ended up giving us permission just for the edu.umn.minitex.* groups.

Alternatively, they may ask you to edit a website to prove you have control over the domain or add a record to your DNS.

After putting in your ticket, you will need to wait until their support staff respond and mark a ticket as Resolved. Sonatype is typically quick to respond, but in the meantime you can work on setting up signing and metadata for your project.

Set up Signing

You need to sign any files you push to Maven Central. We can use GnuPG to do this.

To start, visit the download page and choose an operating system appropriate option. I went with the Gpg4win option under the GNUPG Binary Releases section.

For Windows, this installed Kleopatra, an application that let me generate key-pairs and publish them to a server. However you generate and publish them, make note of your Key-ID since you'll need that when configuring your build.

Configure build.gradle and gradle.properties

build.gradle

Below you will find a build.gradle file that will set up each module/subproject in your Android project to be published in Maven Central. This is your Android Studio project's build.gradle.

You can still define project specific dependencies in their own build.gradle files, but this will ensure signing and publishing gets properly setup.

buildscript {
  if ("$gradle.gradleVersion" != "5.2.1") {
    throw new GradleException("Gradle version 5.2.1 is required (received $gradle.gradleVersion)")
  }
  ext.kotlin_version = "1.3.30"
  repositories {
    mavenCentral()
    google()
    jcenter()
  }
  dependencies {
    classpath "com.android.tools.build:gradle:3.4.0"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    // These two dependencies help with generating required metadata, signing, and publishing to Maven Central
    classpath "digital.wup:android-maven-publish:3.6.2"
    classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.20.0"
  }
}
plugins {
  id "io.codearte.nexus-staging" version "0.20.0"
  id "digital.wup.android-maven-publish" version "3.6.2"
}
ext {
  androidBuildToolsVersion = "28.0.3"
  androidCompileSDKVersion = 28
  androidMinimumSDKVersion = 21
  androidTargetSDKVersion = 28
  if (!project.hasProperty("nexusUsername")) {
    logger.warn("No nexusUsername property specified: Using an empty value")
    nexusUsername = ""
  }
  if (!project.hasProperty("nexusPassword")) {
    logger.warn("No nexusPassword property specified: Using an empty value")
    nexusPassword = ""
  }
}

apply plugin: "io.codearte.nexus-staging"

// The packageGroup should be the same name as your staging repository gorup ("edu.umn.minitex", "org.librarysimplified")
nexusStaging {
  packageGroup = "<PUT PACKAGE DOMAIN HERE!>" 
  numberOfRetries = 32
  delayBetweenRetriesInMillis = 2000
}
allprojects {
  group = project.ext["GROUP"]
  version = project.ext["VERSION_NAME"]
}
subprojects { project ->
  switch (POM_PACKAGING) {
    case "apk":
      logger.info("Configuring ${project} (${POM_PACKAGING}) as Android application project")
      apply plugin: "com.android.application"
      apply plugin: "kotlin-android"
      android {
        compileSdkVersion androidCompileSDKVersion
        buildToolsVersion androidBuildToolsVersion
        defaultConfig {
          minSdkVersion androidMinimumSDKVersion
          targetSdkVersion androidTargetSDKVersion
        }
        compileOptions {
          sourceCompatibility JavaVersion.VERSION_1_8
          targetCompatibility JavaVersion.VERSION_1_8
        }
      }
      break
    case "aar":
      logger.info("Configuring ${project} (${POM_PACKAGING}) as Android library project")
      apply plugin: "com.android.library"
      apply plugin: "kotlin-android"
      android {
        compileSdkVersion androidCompileSDKVersion
        buildToolsVersion androidBuildToolsVersion
        defaultConfig {
          minSdkVersion androidMinimumSDKVersion
          targetSdkVersion androidTargetSDKVersion
          testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
        compileOptions {
          sourceCompatibility JavaVersion.VERSION_1_8
          targetCompatibility JavaVersion.VERSION_1_8
        }
        testOptions {
          execution "ANDROID_TEST_ORCHESTRATOR"
          animationsDisabled = true
        }
      }
      break
    default:
      throw new IllegalStateException(
        "Unrecognized packaging type ${POM_PACKAGING} for ${project}")
  }
  task javadocsJar(type: Jar) {
    classifier = "javadoc"
  }
  task sourcesJar(type: Jar) {
    classifier = "sources"
    from "src/main/java", "src/main/resources"
  }
  apply plugin: "digital.wup.android-maven-publish"
  apply plugin: "signing"
  publishing {
    publications {
      basicJar(MavenPublication) {
        switch (POM_PACKAGING) {
          case "jar":
            from components.java
            break
          case "apk":
            break
          case "aar":
            from components.android
            break
        }
        artifact sourcesJar
        artifact javadocsJar
        pom {
          artifactId = POM_ARTIFACT_ID
          name = POM_NAME
          packaging = POM_PACKAGING
          description = POM_DESCRIPTION
          url = POM_URL
          scm {
            connection = POM_SCM_CONNECTION
            developerConnection = POM_SCM_DEV_CONNECTION
            url = POM_SCM_URL
          }
          licenses {
            license {
              name = POM_LICENCE_NAME
              url = POM_LICENCE_URL
            }
          }
          developers {
            // Define these in gradle.properties or hard code            
            developer {
              id = POM_DEVELOPER_ID // (suggest using github id)
              name = POM_DEVELOPER_NAME 
              email = POM_DEVELOPER_EMAIL
              url = POM_DEVELOPER_URL
            }
          }
        }
      }
    }
    repositories {
      maven {
        def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
        def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
        url = version.endsWith("SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl
        credentials(PasswordCredentials) {
          username nexusUsername
          password nexusPassword
        }
      }
    }
  }
  signing {
    useGpgCmd()
    sign publishing.publications.basicJar
  }
  repositories {
    mavenCentral()
    mavenLocal()
    jcenter()
    google()
  }
}

In this approach, the build.gradle files in each project will just include project specific configurations like dependencies.

gradle.properties

There are three different gradle.properties file types we need to edit/create in this approach:

  1. The Project's gradle.properties
  2. Subproject gradle.properties files
  3. Global gradle.properties file

1. Project gradle.properties

You'll likely have noticed several variables with the POM_* prefix. These should be defined in the entire project's gradle.properties file. This will provide data for the entire project and default values for subprojects to use. Here is the gradle.properties file for our pdfreader-android project:

# These are default values, provide more specific details in a project's own gradle.properties
GROUP=edu.umn.minitex.pdf
VERSION_NAME=0.1.0
POM_ARTIFACT_ID=edu.umn.minitex.pdf
POM_DESCRIPTION=Minitex PDF Library
POM_INCEPTION_YEAR=2019
POM_LICENCE_DIST=repo
POM_LICENCE_NAME=Apache 2.0 License
POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
POM_NAME=edu.umn.minitex.pdf
POM_PACKAGING=pom
POM_SCM_CONNECTION=scm:git:git://github.com/Minitex/pdfreader-android
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/Minitex/pdfreader-android
POM_SCM_URL=http://github.com/Minitex/pdfreader-android
POM_URL=http://github.com/Minitex/pdfreader-android

2. Subproject gradle.properties

Each subproject can have it's own gradle.properties file to overwrite the values in the project level file. Android Studio does not create subproject gradle.properties files and the Android Project view does not include them in the file tree. You can create them at the top-level folder of the subproject and use the Project view to get them to appear.

Here is a link to the api module of the pdfreader-android repository's gradle.properties file. (Source Code Here)

POM_ARTIFACT_ID=edu.umn.minitex.pdf.api
POM_DESCRIPTION=Minitex PDF Library (API)
POM_NAME=edu.umn.minitex.pdf.api
POM_PACKAGING=aar
POM_AUTOMATIC_MODULE_NAME=edu.umn.minitex.pdf.api

These values override any defaults in Project's gradle.properties file.

3. Global gradle.properties

The last file we need to add values to is our global gradle.properties file.

This file lives in the .gradle folder of your home directory. Because this file is not commited to repositories, this is where we will provide our information for signing and publishing to Maven Central.

You will need to provide the following values:

signing.gnupg.executable=gpg
signing.gnupg.useLegacyGpg=false
signing.gnupg.keyName=<YOUR KEY HERE>

nexusUsername=<SONATYPE JIRA USERNAME>
nexusPassword=<SONATYPE JIRA PASSWORD>

The first three configure how the signing plugin will work. The keyName is the key you generated when you configured signing earlier in this guide.

The nexus properties are your credentials you used to sign into Sonatype's JIRA instance. This is how they manage whether you have permissions to publish to the group.

Testing the deployment

You can test publishing to your local maven repository before trying to publish to Maven Central. You can do this with the:

./gradlew assemble publishToMavenLocal

command. Then check the .m2/repository folder in your home directory and you should see generated folders for each subproject. If you open them and you see files with the .asc extension you know your files are being signed.

Publishing the repository

Before attempting to push to Maven Central, please verify that your ticket is Resolved and Sonatype has your account set up. If you fail to wait for this it could cause unexpected behaviors and your repo could be pushed to a catch-all repository.

Once Sonatype is ready for you, you can push to Maven Central with the

./gradlew clean assemble publish closeAndReleaseRepository

command.

This may take some time, but if you get a BUILD SUCCESSFUL message you can check the Staging Repositories link at https://oss.sonatype.org/ (you will need to log in with your JIRA credentials to see the Staging Repositories link).

Sonatype may have asked you to comment on your ticket when you are done, but their system might automatically pick it up and complete it anyway.

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment