plugins {
id 'com.github.blindpirate.gogradle' version '0.6.5'
}
golang {
packagePath = 'github.com/bmuschko/link-verifier'
}
In July 2017 Google Go made a big jump on the TIOBE index. It’s now ranked among the top 10 most popular programming languages. With the rise of Moby aka Docker, Kubernetes and InfluxDB the language has become the go-to tool in the DevOps space. The complexity of automating the process of building, assembling and distributing the source code and binaries for any medium- to large-sized project is high. It’s somewhat shocking to see that the predominant tooling of automating in Go is still a mixture of Make files and shell scripts as it can be observed in the Moby and Kubernetes code base.
Make files and shell script can be powerful tools but they do not provide any support for strong modeling of a domain, are hard to maintain and do not provide any means to testing the automation code.
In this post I’d like to identify if Gradle can live up to the game. We’ll look at a simple Go project, some typical challenges and tasks encountered when building this project and how Gradle can help to automate the process.
Gradle currently does not provide a standard way to build Go project with its core distribution. However, users can write plugins to enhance the functionality by plugins to model new domains.
GoGradle is a plugin that helps with compiling, testing and assembling Go projects. At this year’s Gradle Summit, the plugin was awarded the Gradle plugin of the year 2017. For a deep dive on the plugin functionality check out the recording of the Summit talk on GoGradle.
Disclaimer: I found another Gradle plugin for building Go project but did not have a chance to compare the functionality and their implementation approaches.
For the purpose of demonstrating the functionality provided by the GoGradle plugin, we’ll have a look at a Go project called Link Verifier, a program run from the command line. Functionally, Link Verifier recursively iterates over a given directory and identifies plain-text mark-up files like AsciiDoc and Markdown. For each of the found documents, the program extracts URLs and verifies that they can be resolved by executing a HTTP call.
The project depends on external Go packages: xurls for extract URLs out of a text document and testify for conviently making assertions in test code. Both libraries can be resolved with the help of Glide, a package manager for Go. A developer working on the project will have to install Glide to properly resolve the declared dependencies and their transitive dependencies.
The project uses Go’s built-in support for executing tests, the test
command. Additionally, code coverage metrics are produced by configuring the The test
command. The project uses Codecov to capture and visualize code coverage metrics over time.
The program was designed to run on different OSes e.g. Linux, Windows and MacOSX. For that purpose the project publishes prebuilt libraries with every single release. To adhere to the license agreements of external packages used by the project, I also wrote some logic for extracting the third-party license agreements and packaging them with the corresponding library in a TAR or ZIP file.
Most of the automation steps mentioned above can be executed by invoking a shell script. Every change made to the project runs through Travis CI. To reuse automation logic Travis directly calls the shell script checked into version control with the project’s source code.
Let’s see if the Gradle plugin can fulfill all of those requirements while at the same hiding and simplifying complex implementation logic which would otherwise lives in a shell script. You can find the source code describe below in a dedicated branch.
Getting started with the GoGradle plugin is easy. We just have to create a build.gradle
file in the root directory of the existing project. In the build script, apply the plugin and provide some basic configuration that indicates the root path of the package used for the project. For the Link Verifier project, I assigned github.com/bmuschko/link-verifier
to the packagePath
property of the extension. All sections below directly refer to version 0.6.5. Please be aware that the behavior and/or the configuration options may change in future versions of the plugin.
By default the GoGradle plugin downloads the latest version of Go automatically and stores it in a temporary directory. Alternatively, a user can also configure a concrete Go version as needed. Please refer to the plugin documentation for more information.
build.gradle
plugins {
id 'com.github.blindpirate.gogradle' version '0.6.5'
}
golang {
packagePath = 'github.com/bmuschko/link-verifier'
}
Listing 1. Basic project setup
Now that we applied the plugin to our project, we have a bunch of useful tasks to our disposal. Probably the most important one is build
which resolves all dependencies, compiles the code, runs the tests and assembles the binaries.
Executing the task with the build script shown in listing 1 fails compilation. Obviously, the project is missing external dependencies referenced in source file. In the next step, we’ll fix the issue.
Glide is my package management tool of choice for building Go projects. The project uses the libraries xurls and testify with a specific version to ensure reproducibility. Listing 2 shows those dependencies in the YAML format processed by Glide.
glide.yaml
package: .
import:
- package: github.com/mvdan/xurls
version: 1.1.0
testImport:
- package: github.com/stretchr/testify
version: 1.1.4
Listing 2. The Glide dependency definition file
As you might know Go resolves external packages from Git repositories. In file named glide.lock
, Glide maps the concrete version of an external dependency to a Git commit hash of the repository hosting the code. An example of such a lock file can be seen in listing 3.
glide.lock
hash: 806deb3bb1bb02051f152c49856cac37224f623247742a1b8c028b38dff21aef
updated: 2017-06-03T12:38:37.338393246-04:00
imports:
- name: github.com/mvdan/xurls
version: d315b61cf6727664f310fa87b3197e9faf2a8513
testImports:
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
Listing 3. The Glide dependency lock file
The latest version of Gradle, 4.0, does not support resolving dependencies from a Git repository. So how do we make sure that Gradle understands the information? GoGradle enhances the standard way of declaring dependencies in Gradle. By applying the plugin users can declare Git-based dependencies with a help of a DSL. The exposed DSL seamlessly blends into the existing dependency management DSL as shown in listing 4.
build.gradle
dependencies {
golang {
build name:'github.com/mvdan/xurls', version:'d315b61cf6727664f310fa87b3197e9faf2a8513'
test name:'github.com/stretchr/testify', version:'69483b4bd14f5845b5a1e55bca19e954e827f1d0'
}
}
Listing 4. Gradle dependency definitions derived from Glide yaml file
As you can imagine typing down the dependency declaration manually is somewhat painful. Thankfully, the plugin introduces the convenience task init
. The task derives the information declared in the Glide lock file and translates it into Gradle-based configuration. I’d expect that VCS-based repository formats are going to be introduced by Gradle core natively in the future.
The functionality of the
init
task is not limited to Glide. It also understands a variety of other Go package management tools. Please refer to the documentation to identify if your package manager is supported by GoGradle.
With all the dependencies in place, the project is able to run through the compilation and test steps. Let’s also have a closer look at the testing capabilities of the plugin.
GoGradle’s testing support follows the same conventions you might know from Java-based projects. The test
task provided by the plugin finds all Go files that follow the file name convention <package>_test.go
. GoGradle properly detects and runs all tests in the project as shown in the following console output:
$ gradle test
> Task :prepare
Found go 1.8.3 in /usr/local/go/bin/go, use it.
Use project GOPATH: /Users/bmuschko/dev/projects/gradle-playground/link-verifier/.gogradle/project_gopath
> Task :test
Test for github.com/bmuschko/link-verifier/stat finished, 4 completed, 0 failed
Test for github.com/bmuschko/link-verifier/text finished, 6 completed, 0 failed
Test for github.com/bmuschko/link-verifier/http finished, 3 completed, 0 failed
Test for github.com/bmuschko/link-verifier/file finished, 6 completed, 0 failed
BUILD SUCCESSFUL in 3s
5 actionable tasks: 3 executed, 2 up-to-date
By default the task automatically generates test reports as well as coverage profile data. Coverage metrics can be found in the directory .gogradle/reports
. Figure 1 shows a sample report for the project.
Figure 1. HTML test report
At the time of writing, the plugin only creates one coverage profile data file per package. Aggregating individual coverage files requires writing some custom logic. Listing 5 demonstrates a simple solution to the problem. I expect this functionality to be worked into the plugin sooner or later.
buildSrc/src/main/groovy/com/bmuschko/linkverifier/AggregateCoverage.groovy
package com.bmuschko.linkverifier
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
class AggregateCoverage extends DefaultTask {
@InputDirectory
File inputDir = project.file('.gogradle/reports/coverage/profiles')
@OutputFile
File outputFile = project.file("$project.buildDir/reports/coverage-aggregate/coverage.txt")
AggregateCoverage() {
group = 'GoGradle'
description = 'Aggregates coverage profile data.'
}
@TaskAction
void aggregate() {
StringBuilder aggregatedCoverage = new StringBuilder()
inputDir.listFiles().each {
aggregatedCoverage << it.text
}
outputFile.text = aggregatedCoverage.toString()
}
}
Listing 5. Custom task for aggregating coverage profiles
With the custom task implementation in place, the build script can be enhanced by the task aggregateCoverage
. As shown in listing 6, necessary task dependencies have been established to hook into the typical lifecycle of a Go-based Gradle project.
build.gradle
import com.bmuschko.linkverifier.AggregateCoverage
task aggregateCoverage(type: AggregateCoverage) {
mustRunAfter test
}
build.dependsOn aggregateCoverage
Listing 6. Hooking coverage aggregration task into task lifecycle
After exploring the plugin’s testing capabilities, let’s round out the discussion by having a look at the cross-compilation functionality.
Link Verifier creates a matrix-combination of cross-compiled binary files. The plugin takes away the burden of having to implement the logic manually for every single project. With the help of the build
task, a user can declare a list of target platforms as shown in listing 7.
build.gradle
build {
targetPlatform = 'darwin-amd64, netbsd-amd64, netbsd-386, openbsd-amd64, openbsd-386, freebsd-amd64, freebsd-386, linux-amd64, linux-386, linux-arm, windows-amd64, windows-386'
}
Listing 7. Target platforms definitions
Upon execution, the task creates the resulting prebuilt libraries in the directory .gogradle
. Each file name consists of the provided target platform and the name of the project. Currently, the project version is not taken into account.
. └── .gogradle ├── darwin_amd64_link-verifier ├── freebsd_386_link-verifier ├── freebsd_amd64_link-verifier ├── linux_386_link-verifier ├── linux_amd64_link-verifier ├── linux_arm_link-verifier ├── netbsd_386_link-verifier ├── netbsd_amd64_link-verifier ├── openbsd_386_link-verifier ├── openbsd_amd64_link-verifier ├── windows_386_link-verifier └── windows_amd64_link-verifier
In my shell script, I also extracted all third-party licenses and bundled them with the binary in a tar.gz
or .zip
file. The plugin does not provide this functionality. However, it’s very easy to achieve this with the help of Gradle’s built-in capabilities as shown in listing 8.
buildSrc/src/main/groovy/com/bmuschko/linkverifier/Distributions.groovy
package com.bmuschko.linkverifier
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
class Distributions extends DefaultTask {
@InputDirectory
File inputDir = project.file('.gogradle')
@OutputDirectory
File outputDir = project.file("$project.buildDir/distributions")
Distributions() {
group = 'GoGradle'
description = 'Builds packaged distributions for all prebuilt binaries.'
}
@TaskAction
void create() {
inputDir.listFiles(new CrossCompiledFileFilter(project)).each { preBuiltLib ->
if (preBuiltLib.name.contains('windows')) {
ant.zip(destfile: "${outputDir}/${preBuiltLib.name}.zip") {
fileset(dir: inputDir) {
include(name: preBuiltLib.name)
}
}
} else {
def tarFile = "${outputDir}/${preBuiltLib.name}.tar"
ant.tar(destfile: tarFile) {
tarfileset(dir: inputDir) {
include(name: preBuiltLib.name)
}
}
ant.gzip(destfile: "${tarFile}.gz", src: tarFile)
project.delete(tarFile)
}
}
}
private static class CrossCompiledFileFilter implements FilenameFilter {
private final Project project
CrossCompiledFileFilter(Project project) {
this.project = project
}
boolean accept(File f, String filename) {
filename.endsWith("_${project.name}")
}
}
}
Listing 8. Packaging prebuilt binaries into archive files
In the build script, we just need to create a new task of type Distributions
and ensure that it can only run after the prebuilt libraries have been created.
build.gradle
import com.bmuschko.linkverifier.Distributions
task dist(type: Distributions) {
mustRunAfter build
}
Listing 9. Instantiating a task for creating distributions
Go’s popularity is growing. Gradle can help to automate the build process for Go-based projects. In this post, we had a look at the community plugin GoGradle. GoGradle is a powerful and easy-to-use addition to the Gradle plugin ecosystem. It provides tasks for compiling and testing Go sources. It can also help with creating prebuilt libraries for distribution.
We’ve seen that the plugin can reduce a lot of boiler plate code which normally would have to be written as shell scripts. In some areas, the plugin can be improved even further. To drive innovation, I opened a list of issues on the plugin’s GitHub repository.