Publishing a library to Bintray, how hard can it be, right? RIGHT? Turns out it’s not that easy, unless you know how to do it. I’ve spent a good amount of time publishing a library. Mostly because I couldn’t find an entire tutorial, so I had to collect bits and pieces from multiple sources. This post is my attempt to create the tutorial I wanted to see.

In this tutorial we will publish a Kotlin-based library to Bintray. To make things more interesting, that library will have dependencies to other libraries (shocking, I know). We will configure Travis CI to build our library, generate all required files and publish them to Bintray automagically.

The projects starts out with a simple build file (follow along here).

group 'de.techdev'
version '1.0.0'

buildscript {
    ext.kotlin_version = '1.1.0'

    repositories {
        jcenter()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

repositories {
    jcenter()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
    compile "com.squareup.okhttp3:okhttp:3.6.0"
}

A simple Kotlin-based setup, using OkHttp as an external dependency. The library contains just one class App in the package de.techdev that is performing an HTTP call.

package de.techdev

import okhttp3.OkHttpClient
import okhttp3.Request

class App {

    fun start() {
        val client = OkHttpClient()
        val request = Request.Builder().url("https://techdev.io").build()

        client.newCall(request).execute()
    }

}

This is the entire library. Time to publish it. Once the project has been added to GitHub it’s time to add the Travis CI integration.

Travis CI config

To publish artifacts you need a Bintray account. In your account you can then retrieve your API key. Just go to Edit profile | API Key and copy the key to the clipboard.

Bintray profile

Travis CI needs to know your API key and user, so it can publish to Bintray on your behalf. There are multiple ways to achieve this. Either way, you want your credentials to be secured (especially in a public repo). One option is to add the encrypted credentials to your Travis config (I’ll get to that in a minute), which requires the Travis CLI. You can read more on how to do it here.

I prefer setting the environments variables in the repository config. Just set a name and paste the key and username values.

Travis CI Environment Variables

I’m using BINTRAY_API_USER and BINTRAY_API_KEY respectively. Once the variables have been added they are considered secure and can’t be reviewed anymore.

Before we add the Travis CI config to the repository, there is one more change to the build file – adding the Gradle wrapper.

task wrapper(type: Wrapper) {
    gradleVersion = '3.4.1'
}

This task generates a Gradle wrapper script with the given version which is then used by Travis CI. This way we can control which Gradle version is being used. Just run gradle wrapper and commit’n’push the binaries to your repo.

Now it’s time to add the Travis CI config file named .travis.yml to the repository.

language: java
jdk:
  - oraclejdk8
before_install:
  -chmod +x gradlew

The config specifies Java as the target language, using the Oracle JDK. Because of a permission issue described here I’m making the wrapper script explicitly executable. At this point Travis CI should be able to build the project. Once it can find the .travis.yml it will run ./gradlew check followed by ./gradlew assemble.

The first command runs the following tasks

:compileKotlin UP-TO-DATE
:compileJava NO-SOURCE
:copyMainKotlinClasses UP-TO-DATE
:processResources NO-SOURCE
:classes UP-TO-DATE
:compileTestKotlin NO-SOURCE
:compileTestJava NO-SOURCE
:copyTestKotlinClasses UP-TO-DATE
:processTestResources NO-SOURCE
:testClasses UP-TO-DATE
:test NO-SOURCE
:check UP-TO-DATE
</code></pre>
<p>.. and the second</p>
<pre><code lang="lang-bash">:compileKotlin UP-TO-DATE
:compileJava NO-SOURCE
:copyMainKotlinClasses UP-TO-DATE
:processResources NO-SOURCE
:classes UP-TO-DATE
:jar
:assemble

The relevant part is the jar task which packages everything to build/libs/bintray-demo-1.0.0.jar. We could publish this file already. But it would be missing all the transitive dependencies (OkHttp in our case). All clients using our library would then face a NoClassDefFoundError at runtime, unless the client already has OkHttp in the classpath.

What we need is a Maven POM. To generate it, we have to add the maven-publish plugin to the build file. Below you can find all the changes we have to make to generate the POM file. I’ll explain them one by one.

apply plugin: 'maven-publish'

publishing {
    publications {
        maven(MavenPublication) {
            from components.java
            pom.withXml {
                asNode().dependencies.'*'.findAll() {
                    it.scope.text() == 'runtime' && project.configurations.compile.allDependencies.find { dep ->
                        dep.name == it.artifactId.text()
                    }
                }.each() {
                    it.scope*.value = 'compile'
                }
            }
        }
    }
}

model {
    tasks.generatePomFileForMavenPublication {
        destination = file("$buildDir/libs/bintray-demo-1.0.0.pom")
    }
}

We have to create a publication first. It can have any name, but in this example we are using maven. This results in new tasks becoming available.

Gradle Maven Tasks

The relevant task here is generatePomFileForMavenPublication. As the name suggests, this task will create the POM file, without publishing anything. In the publication we have to add the line from components.java which will add all dependencies to the POM. Otherwise, only the library coordinates (group id, artifact id and version) would be included.

What follows is a customization of the POM content itself. There is a weird behavior which will expose all compile-scoped dependencies as runtime-scoped dependencies in the POM (more background here). So we manually change the scope back to compile.

Finally there is one more customization of the aforementioned task. We change the output directory and filename to match the jar (just using .pom as the extension). This way it’s easier to publish everything eventually. Running the task will generate the POM with the expected content.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.techdev</groupId>
  <artifactId>bintray-demo</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jre8</artifactId>
      <version>1.1.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.6.0</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

The tasks works, but as we have seen it is not executed by Travis CI, yet. Fortunately there is a hook to execute commands at certain points.

before_deploy:
  - ./gradlew generatePomFileForMavenPublication

We are using the Gradle wrapper script to execute the task that will create the POM. The hook before_deploy is only executed in case there is an actual deployment. Deployment in our case means publishing the artifact to Bintray. Travis CI offers out-of-the-box support for Bintray, using the following block (see this commit).

deploy:
  provider: bintray
  file: "descriptor.json"
  user: $BINTRAY_API_USER
  key: $BINTRAY_API_KEY
  dry-run: false

All configuration is part of the file descriptor.json (see below). We only specify Bintray as the provider and set the user and key to be used. Here we are referencing the environment variables we have set previously. The option dry-run allows simulating the deployment, which helps testing the configuration before actually deploying something. The final part is the deployment descriptor. Again, I post the entire file below followed by some explanations.

{
  "package": {
    "name": "bintray-demo",
    "repo": "maven",
    "subject": "techdev-solutions",
    "desc": "Bintray Demo Project",
    "website_url": "https://techdev.io",
    "issue_tracker_url": "https://github.com/techdev-solutions/bintray-demo/issues",
    "vcs_url": "https://github.com/techdev-solutions/bintray-demo.git",
    "licenses": ["Apache-2.0"],
    "labels": ["kotlin", "bintray", "gradle"],
    "public_download_numbers": false,
    "public_stats": false
  },

  "version": {
    "name": "1.0.0",
    "released": "2017-03-08",
    "gpgSign": false
  },

  "files":
  [
    {"includePattern": "build/libs/(.*)", "uploadPattern": "de/techdev/bintray-demo/1.0.0/$1"}
  ],
  "publish": true
}

In the first part we are describing the artifact. The repo field refers to an existing repository. I created one in Bintray myself and gave it the name maven. But any name should do. The remaining fields should be self explanatory. Same goes for the version description. The final part pulls off all the work. In the files section we ask all files under build/libs/ to be uploaded to de/techdev/bintray-demo/1.0.0/.

It is important that the upload path matches the group id, artifact id and version (aka. coordinates) of the library. After these changes have been pushed, Travis CI should be able to publish the artifact to Bintray. Make sure there are no errors in the logs and you can see the environment variables being exported correctly. If there are no errors you should see the following log records.

Travis CI Upload

And that’s it. You can see the result here. If you want to use the library, you have to configure the Bintray repository.

repositories {
    maven {
        url  "http://dl.bintray.com/techdev-solutions/maven"
    }
}

Afterwards you can declare a dependency using the coordinates de.techdev:bintray-demo:1.0.0 and it should work. What I described here is just one way of publishing artifacts. I know there is a Gradle plugin, but I didn’t look into it. You might also wonder that there is not much Kotlin-specific stuff in here. This is because there is no friction at all and everything described here applies to Java as well. I still think there should be an easier way to publish artifacts and I’m very much looking forward to suggestions for improvements!

Stay in the Loop

If you would like to receive an email every now and then with new articles, just sign up below. We will never spam you!