How to use ArtifactHandler with new task API?

I’m playing around with the new task API promoted in Gradle 4.9, and so far, everything is working as expected – except for the integration with the Project’s ArtifactHandler.

Here is a pattern we use for many projects that produce custom artifacts:

plugins {
    id 'base'
}

task producer(type: Producer) {
    destFile = layout.buildDirectory.file('fnord')
}

artifacts {
    'default' producer.destFile
}

class Producer extends DefaultTask {

    @OutputFile
    RegularFileProperty destFile = newOutputFile()

    @TaskAction
    void produce() {
        destFile.get().asFile.text = 'Fnord!'
    }
}

Migrating this to the new task API should look similar to this:

plugins {
    id 'base'
}

tasks.register 'producer', Producer, {
    destFile = layout.buildDirectory.file('fnord')
}

artifacts {
    'default' tasks.named('producer').get().destFile
}

But this causes the producer task to be configured eagerly, instead of lazily, ruining the benefits of deferred task configuration.

If I leave out the .get(), I get an error when the project is configured:

> Could not get unknown property 'destFile' for provider(task producer, class Producer) of type org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreatingProvider_Decorated.

Not surprising, but what’s the alternative?

In a special case, we want to package the artifact in a Zip task, and then everything
works as expected:

plugins {
    id 'base'
}

tasks.register 'producer', Producer, {
    destFile = layout.buildDirectory.file('fnord')
}

tasks.register 'packageProducer', Zip, {
    from tasks.named('producer')
}

artifacts {
    'default' tasks.named('packageProducer')
}

But in some projects, we really don’t want to be building a zip file, but something else (e.g., a multimedia container), which should be registered as the default artifact without being zipped.

Any feedback or solutions would be appreciated!

Thanks @psibre for the great feedback and being an early adopter of the new lazy task APIs. Unfortunately, in Gradle 4.9, we didn’t have the time to properly handle the laziness around this new API and the ArtifactHandler. We are hoping to get something out in 4.10. We have two direction we may take around this issue:

  1. Allow mapping a task provider to “extract” a specific property while keeping the task unresolved. This is a known issue where task dependencies are sometimes not propagated properly so something like the following doesn’t work when it should:
artifacts {
    'default' tasks.named('producer').map { it.destFile }
}
  1. Allow using any kind of Task type to be consumed by the ArtifactHandler and detect the output file to use as the artifact. This is interesting because, from the user’s perspective, it simply means whatever this task is generating, use it as artifacts (like your last example with the Zip task)

Both solutions are not mutually exclusive. Option #1 would most likely be the first use case to fix in the upcoming Gradle release as it affects much more than the specified use case.

1 Like

We should also decorate the provider so that you don’t need it.destFile, but just destFile.

Thanks for the helpful feedback, @Daniel_L and @st_oehme – I look forward to a lazier ArtifactHandler in the near future…

In the meantime, I’ve found a workaround that seems to do the trick. The key is to use a Map for the artifact notation, and the builtBy key wires up the task dependency without eagerly configuring the producer task.
To keep it DRY, we can of course use a property of some sort to configure the path for the produced file.

Here’s an updated MWE:

plugins {
    id 'base'
}

ext {
    fnordFile = file("$distsDir/fnord")
}

tasks.register 'producer', Producer, {
    destFile = fnordFile
}

artifacts {
    'default' file: fnordFile, builtBy: tasks.named('producer')
}

class Producer extends DefaultTask {

    @OutputFile
    final RegularFileProperty destFile = newOutputFile()

    Producer() {
        project.logger.lifecycle "Configuring Producer"
    }

    @TaskAction
    void produce() {
        destFile.get().asFile.text = 'Fnord!'
    }
}

To demonstrate that the producer task doesn’t get configured until it’s on the task execution graph, the corresponding constructor logs a message. The message does not appear if one simply runs ./gradlew clean or some other build-unrelated task.

Good enough for me! =)

1 Like