Copying files into a folder named based on the result of another task

My goal is to define a Copy task where the destination of the copy operation is a folder that includes the build number of the application, but I can’t figure out how to do it cleanly.

The crux of the issue is that the build number is produced by a different gradle task, which involves making an HTTP call to “claim” a build number from a central server. Obviously, this HTTP call should be happening in the execution phase (as opposed to the config phase). Otherwise, running any gradle task would end up spuriously claiming a build number and you’re not really supposed to do potentially slow things like HTTP calls in the config phase regardless.

So, if the HTTP call doesn’t happen until the execution phase it means that we won’t actually know what the build number is until the execution phase. However, a “Copy” task needs to know the destination directory before the execution phase. It seems like it should be possible to define the build number as an output of the claiming task in some fashion, but I can’t figure out how to make it work. (Seems like you can only really define files and folders as outputs?)

Simplified examples:

def buildNumber = "UNSET"

task claimBuildNumber {
    doLast {
        // Pretend that the build number is being fetched via an HTTP call here~
        buildNumber = "23"
    }
}

task copyArtifacts(type:Copy) {
    dependsOn claimBuildNumber

    from file("artifacts")
    destinationDir file("build-${buildNumber}")
}

This one is wrong cause the copy task snapshots the build number during the config phase and ends up copying into a folder named build-UNSET.


task copyArtifacts(type:Copy) {
    dependsOn claimBuildNumber

    from file("artifacts")
    doFirst {
        destinationDir file("build-${buildNumber}")
    }
}

This one blows up with an error:
Type 'org.gradle.api.tasks.Copy' property 'destinationDir' doesn't have a configured value.


task copyArtifacts(type:Copy) {
    dependsOn claimBuildNumber

    from file("artifacts")
    destinationDir file("DUMMY")

    doFirst {
        destinationDir file("build-${buildNumber}")
    }
}

Ok this one technically works, but still ends up spawning the DUMMY dir for some reason (and it seems like this can’t possibly be the “right” way to accomplish this).

So… what’s the best / correct solution here?

You are right, it is not the right way.
You should never change the configuration of a task in the execution phase.
With configuration cache enabled this would even be forbidden.

You should usually use a Sync task, not a Copy task in most cases.

And I strongly recommend to use Kotlin DSL. It by now is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.

But regarding the concrete problem, it is a bit hard to give the actual right recommendation, as it depends on some details, like whether you need the output of the task as input of another for example.

That DUMMY is created in your last example is, because Gradle is nice and creates @OutputDirectorys for you before the task starts executing.

One way to achieve what you want would be to not use a task of type Copy or Sync, but instead use a copy { ... } or better sync { ... } call in the execution phase.
But as you are unable to configure the task outputs properly, the task will always be out-of-date and thus always execute.

You could also instead define some dedicated output directory for the (still “type-less”) task and then create the directory with the buildnumber therein with the sync { ... } closure at execution time if that would also work and if you then properly declared the inputs too, the task could again be up-to-date.

1 Like