Skip to content

Instantly share code, notes, and snippets.

@groboclown
Last active August 5, 2019 13:52
Show Gist options
  • Save groboclown/a5cf5622641b2251d18adbe5561015f1 to your computer and use it in GitHub Desktop.
Save groboclown/a5cf5622641b2251d18adbe5561015f1 to your computer and use it in GitHub Desktop.
An example gradle file for loading a hierarchy of properties from several locations in a well defined order. It also allows for the properties to be written as plain groovy scripts, and defined as closures so they can depend on other, overwritten properties.
/**
* load-properties.gradle
*
* USE: Include in your root project's `build.gradle` file:
*
* ```(groovy)
* apply from: "$rootDir/init/load-properties.gradle"
* ```
*
* Add a `$rootDir/init/categories.gradle` file to define which
* different property hierarchies you want to support. Note that
* all properties must be under the "setup" hierarchy.
*
* ```(groovy)
* setup.database = [:]
* setup.docker = [:]
* setup.executables = [:]
* ```
*
* Mix in a `$rootDir/init/default-properties.gradle` file to define the
* allowable properties. Any override property the user sets that isn't in
* this file will be ignored. Properties can also be closures, for
* basing a property on other properties.
*
* ```(groovy)
* setup.database.hostname = 'localhost'
* setup.database.port = '54321'
* // A closure - if the value isn't overridden, this will run to generate
* // the connection string from the other properties.
* setup.database.connection = {
* // The setup object is in scope of this closure.
* "jdbc://my-db-connection/${setup.database.hostname}:${setup.database.port}"
* }
* // Just a statement; it will run when the default properties are loaded.
* setup.username = System.getenv("USERNAME") ?: System.getenv("USER")
* setup.docker.image_name = {
* "${setup.username}/my_awesome_project"
* }
* // No default for this property, but we define it so it can be overridden
* setup.executables.dockerMachine = null
* ```
*
* Modify the `load-properties.gradle` script to taste.
*
* Then, your build files can reference the configuration properties with the "setup"
* variable.
*
* ```(groovy)
* assert setup.database.hostname == 'localhost'
* assert project.setup.database.hostname == 'localhost'
* assert project.ext.setup.database.hostname == 'localhost'
* ```
*
* **PROBLEM**: With complex build scripts, there are some actions where you want to give the user
* flexibility in how things are setup. For example, you may want to have default settings for
* a database connection, but you want to allow the user to change these values. This is
* especially important when your build script needs to use external tools which the build
* script can't reasonably expect to always be the same for everyone.
*
* **SOLUTION**: We expect *most* people to have a specific environment, so we define
* default values. We may also expect people to have multiple projects, so we should
* allow for shared property definitions between them. We should also allow for
* build machines or different environments to inject their own values. It would
* also be nice if the properties weren't just static, but instead _groovy_, and
* allow for some code execution to construct values from other values.
*
* This file is a general approach to how to load properties into a project,
* allowing for different ways to override values - from local user files, to
* `gradle.properties`, to System properties.
*
* The loading mechanism uses this priority:
*
* 1. System.getProperties('setup.my.name')
* 2. System.getenv('SETUP_MY_NAME')
* 3. `../local.gradle` overrides. This is in the ".." directory so it isn't accidentally checked in.
* This is easy to change in this file.
* 4. `../${env}.gradle` overrides. Set the "env" value either in the project file, or with the "-Penv=xyz"
* gradle argument. The "env" value does not need to be set.
* 5. `../../local.gradle` overrides. This is in the ".." directory so it isn't accidentally checked in.
* This is easy to change in this file.
* 6. `../../${env}.gradle` overrides. Set the "env" value either in the project file, or with the "-Penv=xyz"
* gradle argument. The "env" value does not need to be set.
* 7. `init/default-properties.gradle` default values.
*
* As this is written here, only properties defined in the `default-properties.gradle`
* are allowed to be defined; all others are ignored.
*
* There are other plugins available that emulate what Ant builds did, but those are
* a bit heavyweight, and don't allow for a groovy-like flexibility in defining the
* values.
*/
// Use a new class so the "apply from: , to:" works as we want.
class Setup {
def setup = [:]
}
def userOverrides = new Setup()
apply from: "$rootDir/init/categories.gradle", to: userOverrides
// ==============================================================
// User Overrides
// The ${env} allows for running `gradle -Penv=dev`
// By starting at the lowest depth, and going up, it means that the higher
// levels override the lower levels. Env properties override local properties.
if (file("$rootDir/../../local.gradle").file) apply from: "$rootDir/../../local.gradle", to: userOverrides
if (project.hasProperty('env') && file("$rootDir/../../${env}.gradle").file) apply from: "$rootDir/../../${env}.gradle", to: userOverrides
if (file("$rootDir/../local.gradle").file) apply from: "$rootDir/../local.gradle", to: userOverrides
if (project.hasProperty('env') && file("$rootDir/../${env}.gradle").file) apply from: "$rootDir/../${env}.gradle", to: userOverrides
// ===============================================================
def base = new Setup()
apply from: "$rootDir/init/categories.gradle", to: base
apply from: "$rootDir/init/default-properties.gradle", to: base
/**
* The default values (loaded into cDefaults) are traversed. The default value
* is changed in the above documented order of priority.
* Note that this changes the values in the default set.
*/
def overrideDefaults(cOverrides, cDefaults, name_base) {
for (ee in cDefaults) {
def key = ee.key
def value = ee.value
if (value != null && value instanceof Map) {
// Descend the map to the next level in the hierarchy.
def nextOverrides = null;
if (cOverrides != null && cOverrides.containsKey(key)) {
nextOverrides = cOverrides[key]
}
overrideDefaults(nextOverrides, value, name_base + '.' + key)
} else {
if (System.getProperty(name_base + '.' + key) != null) {
cDefaults[key] = System.getProperty(name_base + '.' + key)
} else if (System.getenv((name_base.replace('.', '_') + '_' + key).toUpperCase()) != null) {
cDefaults[key] = System.getenv((name_base.replace('.', '_') + '_' + key).toUpperCase())
} else if (project.hasProperty(name_base + '.' + key)) {
cDefaults[key] = project[name_base + '.' + key];
} else if (cOverrides.containsKey(key)) {
cDefaults[key] = cOverrides[key]
}
}
}
}
/**
* Descend a map and call each item that's callable. The argument
* passed in is the base map.
*/
def callMapValues(baseMap, cMap) {
// Ensure the upper parts of the hierarchy are evaluated before the
// deepr parts.
def children = []
for (ee in cMap) {
if (ee.value != null && ee.value instanceof Map) {
children += ee.value
} else if (ee.value != null && ee.value.respondsTo('call')) {
if (ee.value instanceof Closure) {
ee.value.resolveStrategy = Closure.DELEGATE_FIRST
ee.value.delegate = baseMap
}
cMap[ee.key] = ee.value.call(baseMap)
}
}
children.each { callMapValues(baseMap, it) }
}
overrideDefaults(userOverrides.setup, base.setup, 'setup')
callMapValues(base, base.setup)
project.ext.setup = base.setup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment