How to include a dynamically-generated file in an extended `Copy` task

Hi, I now develop a Gradle plugin that has an extended Copy task.

public class InstallSomethingTask extends Copy { ...

In this task, I wanted to include a dynamically-generated file in the destination. Its task declaration would be like below, for example,

installSomething {
    property key1, value1
    property key2, dynamicValue2()
    someOtherProperty true  // It adds "foo": "bar" in the property.
    into "path/to"
}

I wanted ./gradlew installSomething to generate some.properties in the destination, which would consist of:

key1=value1
key2=a-dynamic-value-2
foo=bar

Does anyone have good ideas to implement this?

The built-in jar task must be doing a similar thing to generate MANIFEST.MF, so I looked into it.

However, it is highly depending on internal classes and methods, such as FileCollectionFactory.generated(...). Does Gradle have a good way to realize this only with non-internal APIs?

I would not try to stuff this into one task.
A Copy task should copy stuff, not generate things.
I would create a task of type WriteProperties that creates that file and can then be used as input for the Copy task (which in most cases should better be a Sync task).

If the copying is just one of the work items the task does, I would not extend the Copy (or Sync) task, but inject a FileOperations instance and use its copy or sync method.

Thanks! Using Sync or else instead of Copy is totally fine, but they need to be “declared” in one task for users of the plugin.

I’ll take another look into “injecting a FileOperations instance and use its copy or sync method”.

but they need to be “declared” in one task for users of the plugin.

why?

Besides what I already said, you do not need any internal api to mark the file as generated or anything.
Just create the file like you need it during the task execution in the output directory of the task and that should be all that is necessary. But make sure that you also properly define / adjust the task inputs, so that the task is rerun when the file needs to be regenerated.

why?

Because that’s the aim and want of the Gradle plugin. The plugin is not only my own use, but will be published for users of another framework as an installer for the framework. Wanted to complete all the declarations in a single task for simplicity for users.

Just create the file like you need it during the task execution in the output directory of the task and that should be all that is necessary. But make sure that you also properly define / adjust the task inputs, so that the task is rerun when the file needs to be regenerated.

Got it – but another question here is: How can I trigger “create the file” after all property and someOtherProperty calls are emitted?

An internal java.util.Properties needs to be updated per every property or someOtherProperty call, and then at last the updated Properties needs to be written out.

Because that’s the aim and want of the Gradle plugin.

That’s not really a reason, just an implementation detail. Indeed it is often much more convenient for users if individual tasks do small parts, so one is able to hook in easily it only do some parts. For example a user could just generate the file to see what it contains. Or if the generation of the file would be costly, you also would not have to regenerate it just because one of the copied files changed. Also you could just use the ready-made task to write the properties. And if you do not set a group for the task it also will not appear in the tasks output or similar.

The user either way would just call one task.

But well, was mostly curious.

Got it – but another question here is: How can I trigger “create the file” after all property and someOtherProperty calls are emitted?

Why should you need to? The calls are made in configuration phase. The writing of the file will happen at execution phase, so it will always be after the calls.

Why should you need to? The calls are made in configuration phase. The writing of the file will happen at execution phase, so it will always be after the calls.

Yeah, I should have rephrased my question to “How can I inject my own actions at the execution phase?” , but thanks for the advice. By recognizing the execution phase explicitly, I finally reached at how to realize this by overriding the copy() method like below.

    @Override
    @TaskAction
    protected void copy() {
        // Do my own file generating actions.
        super.copy();
    }

Just an off-topic side note… :

That’s not really a reason, just an implementation detail. Indeed it is often much more convenient for users if individual tasks do small parts, so one is able to hook in easily it only do some parts.

I agree with that for a normal Gradle plugin for a normal “building” use-case, but my use-case is not. We wanted to provide just a DSL to install things for our own framework, just by exploiting Gradle, and we do not expect the users of the DSL would be Gradle experts. We also do not expect them to be trained for Gradle, too. We wanted to complete everything in a single task as a DSL.

I know this is not an expected use-case of the Gradle team, though. It’s totally our own reason, not for general Gradle users…

Yeah, I should have rephrased my question to “How can I inject my own actions at the execution phase?”

For any given task even without subclassing, doLast and doFirst, depending on whether you want to add it to the front or back of the current list of actions.

by overriding the copy() method like below

I wouldn’t do that, this method name is more or less an implementation detail you should imho not rely on.
And if you follow my advice to not extend Copy it also will not be there anymore.
Instead you would just have your own @TaskAction method where you then use the injected FileSystemOperations instance to call copy or sync on.

It’s totally our own reason, not for general Gradle users…

Sure, whatever works for you. I’m just giving my personal recommendations. :slight_smile:

It’s not obvious to me why this requires extending the Copy task, what’s wrong with a task like:

plugins {
    id 'base'
}

@CacheableTask
class InstallSomethingTask extends DefaultTask {
    @Input
    Map<String, Object> properties = new HashMap<>()

    @Input
    boolean someOtherProperty

    @OutputFile
    java.nio.file.Path outputFile

    InstallSomethingTask() {
        def home = System.getProperty 'user.home'
        def path = this.project.file(home).toPath().resolve('.config').resolve('something.properties')
        path.parent.toFile().mkdirs()
        outputFile = path
    }

    @TaskAction
    void installIt() {
        new Properties().tap {
            putAll this.properties
            if (this.someOtherProperty)
                put 'foo', 'bar'
            store(new FileOutputStream(this.outputFile.toFile()), null)
        }
    }

    void property(String key, Object value) {
        properties.put(key, value)
    }
}

tasks.register('installSomething', InstallSomethingTask) {
    group 'build'
    description 'installSomething'
    property 'key1', 'value1'
    property 'key2', new Date().toCalendar().toYear().toString()
    someOtherProperty true  // It adds "foo": "bar" in the property.
}

Because his install task does copy files around and not only generates that one file.
Otherwise he would not need any custom task at all but could just use a WriteProperties task.

And noone said it requires extending the Copy task, I even advised multiple times not to do it. :wink:

Thanks for your comments.

I extended Copy because I wanted to provide not only property and someOtherProperty, but also mostly Copy-compatible methods to the plugin users. The major purpose of the plugin is still like copy, but property and else are just addition to copy.

I agree that “not extending the standard Copy” is better in general if that could be trivially realized.