Last active
August 5, 2019 13:52
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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