Saturday, August 18, 2012

How to customize published artifacts from Gradle

I am really enjoying Gradle.  It's probably a post for another day, but I haven't enjoyed a toolset this much since I discovered Google Guice years ago.

We have been slowly migrating our existing projects from Ant to Gradle, and encountered a few bumps on our way to customizing published build artifacts.  In case anyone else is similarly stumped, here are my findings.  We use Ivy for our dependency management, but Gradle abstracts many of the details, and for the purposes of this discussion it doesn't really matter if you're using Ivy or Maven.  Also for the sake of simplicity, I'm assuming we using the basic resolvers and repo layout patterns.

There are three primary identifiers we are concerned with using dependency management.  Maven calls them group, artifact, and version; Ivy calls them organisation, module, and revision, but we're really talking about the same thing.

The Basics

On publish, Gradle defaults the artifact name to the project name, the version name to the string "unspecified," and leaves the group name blank.  So that means the simplest possible upload task:

 apply plugin: 'java' 
      
 uploadArchives {  
      repositories {  
           ivy {  
                url 'c:/temp/local'  
           }  
      }  
 }  

produces an artifact that looks like this:

 Published projectA to C:\temp\local//projectA/unspecified/projectA-unspecified.jar  
 Published ivy to C:\temp\local//projectA/unspecified/ivy-unspecified.xml  

Probably not what we'd typically want.  Fortunately, customizing group and version is easy.  They are both properties on the Project instance:

 apply plugin: 'java'       
   
 group = 'com.example'  
 version = '1.0'  
   
 uploadArchives {  
      repositories {  
           ivy {  
                url 'c:/temp/local'  
           }  
      }  
 }  

This produces:

 Published projectA to C:\temp\local/com.example/projectA/1.0/projectA-1.0.jar  
 Published ivy to C:\temp\local/com.example/projectA/1.0/ivy-1.0.xml  

This should cover the vast majority of publishing use cases.  Generally these primary three values are all you need to publish your artifact.

Specialty fields

There do exist specialty uses cases where additional fields would be useful; for example, publishing the source code with the binary, or indicating that a jar targets a specific JDK.  For these purposes, Maven introduced (and Ivy supports) an additional property known as a classifier.  Gradle adds another, an appendix, which can optionally be appended to the artifact name.

These are not fields on the project object and so cannot be set in the same way as group and version.  Instead, we set these properties on the archive creation task itself.  In the case of a jar, it might look something like this:

 apply plugin: 'java'       
   
 group = 'com.example'  
 version = '1.0'  
   
 jar {  
      appendix = 'myAppendix'  
      classifier = 'myClassifier'  
 }  
   
 uploadArchives {  
      repositories {  
           ivy {  
                url 'c:/temp/local'  
           }  
      }  
 }  

This produces the following output:

 Published projectA-myAppendix to C:\temp\local/com.example/projectA/1.0/projectA-myAppendix-1.0-myClassifier.jar  
 Published ivy to C:\temp\local/com.example/projectA/1.0/ivy-1.0.xml  

As you can see, the Ivy file name is unaffected.  Only the artifact itself takes on these parameters in its name. The Ivy file does, however, define the fields internally:

      <publications>  
           <artifact name="projectA-myAppendix" type="jar" ext="jar" conf="archives,runtime" m:classifier="myClassifier"/>  
      </publications>  
   

Branching

A common use case in software development is to create feature branches in source control for long-lived or high impact changes to a code base, and being able to publish branched artifacts separate from the mainline of development can be very useful.  Using the appendex or classifier field for this purpose is inappropriate because as we saw above, they are intended for multiple related artifacts.  The basic path to the artifact as well as the ivy.xml descriptor file remain the same.  For a branch, you'd need both of these to be variable.

Ivy supports publishing artifacts in branches by providing an attribute on publishing, but Gradle does not.  (I'm not sure about Maven, but some quick Googling suggests it doesn't either).  This creates a challenge: how can we easily publish branches in Gradle?

Vary the module name

One option is to vary the module name.  Instead of publishing an artifact as "projectA," publish it as "project-A-branch."  This is a problem in Gradle because unlike the group and version fields, the name of the project is immutable (a problem for which an issue has been logged).

The suggested work around is to leverage the Gradle Settings object--which exists prior to any project--and modify the project names there.  Any dependent sub-projects would have to be modified as well.  So a sample settings.gradle file might look like this:

 // hack to support branching on module names, see GRADLE-2412  
 try {  
      rootProject.name += "-" + branch  
      rootProject.children.each {  
           it.name += "-" + branch  
      }  
 } catch (MissingPropertyException e) {  
      // there is no way to test for the existence of the branch property  
      //  so we swallow the exception if it doesn't exist, and publish  
      //  any artifacts with the default project name  
 }  

Here I've made my branch configurable by allowing the user to pass a -P property on the command line.  The hasProperty() function is not available to us in the Settings object, so we have to use try-catch in case the property does not actually exist.

When the Project objects are instantiated, they will be created with the newly modified branch name, and Gradle will publish the artifact with that name.  However one other change has to be made for this to work, and that's any direct project dependencies would also need to be modified to use the new branch name.

For example, a dependency code block that reads:

 dependencies {  
      compile (  
           project(':projectB'),  
           project(':projectC'),  
      )  
 }  

Would have to be changed to read something like:

 dependencies {  
      compile (  
           project(':projectB' + '-' + branch),  
           project(':projectC' + '-' + branch),  
      )  
 }  

Vary the version

There is an easier way to have Gradle support a branch, and that's to vary the version instead.  This is a simple matter of customizing the version number as we have seen already.

 version = hasProperty('publishVersion') ? publishVersion : "latest"  

The user could then pass a branch name using the -P option.  If your Ivy resolver is set up to use "latest" as a keyword  for a constantly changing artifact (similar to SNAPSHOT in Maven), then simply appending that to the end of the branch name will retain that behavior.

Conclusion

Gradle has excellent integration with existing dependency management frameworks.  It easily supports the most common use cases, and also is customizable for edge cases.  Hopefully this has shed a little light on how to customize your published artifacts.