Matt
Matt

Reputation: 8476

Copying files based on a pattern that is determined after the configuration phase has completed

I am currently assessing gradle as an alternative to Maven for a homegrown convention based ant+ivy build. The ant+ivy build is designed to provide a standard environment for a wide range of j2se apps & it supports the following conventional layout to app config

conf/
    fooPROD.properties
    fooUAT.properties
    bar.properties
    UK/
        bazPROD.properties
        bazUAT.properties

If I choose to do a build for UAT then I get

conf/
    foo.properties
    bar.properties
    UK/
        baz.properties

i.e. it copies the files that are suffixed with the target environment (UAT in this case) as well as anything that has no such pattern. There are a variety of other things that happen alongside this to make it rather more complicated but this is the core of my current problem.

I've been playing around with various gradle features while transcribing this as opposed to just getting it working. My current approach is to allow the targetenv to be provided on the fly like so

tasks.addRule("Pattern: make<ID>") { String taskName ->
    task(taskName).dependsOn tasks['make']
}

The make task deals with the various copying/filtering/transforming of conf files from src into the build area. To do this, it has to work out what the targetenv is which I am currently doing after the DAG has been created

gradle.taskGraph.whenReady {taskGraph ->
    def makeTasks = taskGraph.getAllTasks().findAll{ 
        it.name.startsWith('make') && it.name != 'make' 
    }
    if (makeTasks.size() == 1) {
        project.targetEnv = makeTasks[0].name - 'make'
    } else {
        // TODO work out how to support building n configs at once
    }
}

(it feels like there must be a quicker/more idiomatic way to do this but I digress)

I can then run it like gradle makeUAT

My problem is that setting targetEnv in this way means the targetEnv is not set at configuration time. Therefore if I have a copy task like

task prepareEnvSpecificDist(type: Copy) {
    from 'src/main/conf'
    into "$buildDir/conf"
    include "**/*$project.targetEnv.*"
    rename "(.*)$project.targetEnv.(.*)", '$1.$2'
}

it doesn't do what I want because $project.targetEnv hasn't been set yet. Naively, I changed this to

task prepareEnvSpecificDist(type: Copy) << {
    from 'src/main/conf'
    into "$buildDir/conf"
    include "**/*$project.targetEnv.*"
    rename "(.*)$project.targetEnv.(.*)", '$1.$2'
}

once I understood what was going on. This then fails like

Skipping task ':prepareEnvSpecificDist' as it has no source files.

because I haven't configured the copy task to tell it what the inputs and outputs are.

The Q is how does one deal with the problem of task configuration based on properties that become concrete after configuration has completed?

NB: I realise I could pass a system property in and do something like gradle -Dtarget.env=UAT make but that's relatively verbose and I want to work out what is going on anyway.

Cheers

Matt

Upvotes: 0

Views: 1522

Answers (1)

Peter Niederwieser
Peter Niederwieser

Reputation: 123940

Building for a particular target environment is a cross-cutting concern and does not really fit the nature of a task. Using a system property (-D) or project property (-P) is a natural way of dealing with this.

If you absolutely want to save a few characters, you can query and manipulate gradle.startParameter.taskNames to implement an environment switch that looks like a task name. However, this is a non-standard solution.

How does one deal with the problem of task configuration based on properties that become concrete after configuration has completed?

This is a special case of the more general problem that a configuration value gets written after it has been read. Typical solutions are:

  1. Avoid it if you can.
  2. Some task/model properties accept a closure that will then get evaluated lazily. This needs to be looked up in the corresponding task/plugin documentation.
  3. Perform the configuration in a global hook like gradle.projectsEvaluated or gradle.taskGraph.whenReady (depending on the exact needs).
  4. Perform the configuration in a task action (at execution time). As you have already experienced, this does not work in all cases, and is generally discouraged (but sometimes tolerable).
  5. Plugins use convention mapping to lazily bind model values to task properties. This is an advanced technique that should not be used in build scripts, but is necessary for writing plugins that extend the build language.

As a side note, keep in mind that Gradle allows you to introduce your own abstractions. For example, you could add a method that lets you write:

environment("uat") {
    // special configuration for UAT environment
}

Upvotes: 2

Related Questions