Gradle Offline Build
The Problem
In a recent project, I have to do Gradle build on a CI server in a very constrained environment. The constraints are:
- NO Internet connection
- NO Gradle installed
- NO local Maven repo
The obvious problem that these constraints cause is that the project can't build because:
- Gradle is not installed
- Can't get dependencies
The Solution
This solution will achieve the following:
- Use Gradle to build
- Use Gradle to manage dependencies
Let's solve the problem step by step.
Gradle Wrapper
Put simply, Gradle Wrapper is the way to let you run Gradle without installing it. It's done by running a shell/batch script to download Gradle binary (More on Gradle Wrapper here).
But the CI server has no Internet connection, how to download Gradle binary? Simply modify distributionUrl
in gradle-wrapper.properties
to a local path, e.g.
# Sample gradle-wrapper.properties
# Other properties omitted
distributionUrl=gradle-2.4-bin.zip
And put gradle-2.4-bin.zip
(You can get all Gradle distributions from here) in ${projectDir}/gradle/wrapper
. You should add this file in source control.
From now on, you can build using Gradle without Gradle installed, e.g. (assuming on Mac or Linux) ./gradlew clean build
.
Local Dependencies
To let Gradle manage dependencies and smartly load them from the right place, we need a way to store the dependencies and load them conditionally.
In build.gradle
,
ext.libs = "$projectDir/libs"
ext.compileLib = "${libs}/compile"
ext.runtimeLib = "${libs}/runtime"
ext.testCompileLib = "${libs}/testCompile"
ext.testRuntimeLib = "${libs}/testRuntime"
dependencies {
if (gradle.startParameter.isOffline()) {
compile fileTree(dir: compileLib)
runtime fileTree(dir: runtimeLib)
testCompile fileTree(dir: testCompileLib)
testRuntime fileTree(dir: testRuntimeLib)
} else {
compile 'com.google.guava:guava:18.0'
testCompile group: 'junit', name: 'junit', version: '4.11'
}
}
By doing this, if Gradle is running in offline
mode, it will load dependencies from local subdirectories in the project. Otherwise, it will try to get them from Gradle home or download from the Internet. I choose the parameter offline
because I think it's the best parameter that fits the purpose. Of course, you can use other parameter.
How to store dependent jars in the local subdirectories? A simple copyToLibs
task would do the job.
task deleteLibs(type: Delete) {
delete 'libs/compile'
delete 'libs/runtime'
delete 'libs/testCompile'
delete 'libs/testRuntime'
}
task copyToLibs(dependsOn: 'deleteLibs') << {
['compile', 'runtime', 'testCompile', 'testRuntime'].each { scope ->
copy {
from configurations.getByName(scope).files
into "${libs}/${scope}"
}
}
}
To make this whole thing work, when developing on your local machine, before pushing to Git server, you should run copyToLibs
to make sure that the required dependencies are copied to the local subdirectories and add those dependencies to Git.
On the CI server, run ./gradlew --offline clean build
. Your tasks may vary but make sure that --offline
is passed in because that's the way to tell Gradle to find dependencies from local dirs, instead of Gradle home or the Internet.
Summary
By using Gradle Wrapper and storing dependencies in local directories of the project, we can still build and manage dependencies using Gradle. I think this is a simple and elegant solution. If you know a better one, leave a comment or drop me an email (me [at] this domain).
A demo app is available here.