Mimic processResources with non-java-based projects

Alas, my earlier thoughts that I’d solved this beast proved premature. Even though I am now building in accordance to the rule of thumb we laid down, i.e. do it in a plugin, I still don’t find any reliable way of getting gradle to know that there is a source file to be processed and therefore not to skip the copy task. Here is my latest code iteration:

abstract class BuildInfoPlugin implements Plugin<Project>{

    File info
    @Override
    public void apply(Project project) {
        info = project.resources.text.fromString(generateBuildInfo(project)).asFile()
        Task gbi = project.tasks.create("generateBuildInfo", Copy ) {
            setDestinationDir(getDest(project))        
            from(info.path) {
                include(info.name)
                rename { 'version.properties' }
            }
        }

    }

    static final String TEMPLATE =
    '''version=${vn}
    buildId=${id}
    buildTimestamp=${ts}
    '''
    
    
        protected String generateBuildInfo(Project project) {
            TEMPLATE.replace('${vn}', project.version)
                .replace('${id}', project.findProperty('buildNumber') ?: 'unknown')
                .replace('${ts}', project.findProperty('buildTimestamp') ?: generateTimestamp())
        }
        //    2016-08-16 01:49:00 GMT-06:00
        private static final String PATTERN = 'yyyy-MM-dd HH:mm:ss OOOO'
        private static final DateTimeFormatter FORMATTER = buildDateFormatter();
        
        private static DateTimeFormatter buildDateFormatter() {
            new DateTimeFormatterBuilder()
                .appendPattern(PATTERN)
                .toFormatter()
                
        }
        protected static String generateTimestamp() {
            return FORMATTER.format(OffsetDateTime.now());
        }
        
        protected abstract File getDest(Project project)
    
}

Initially I had the call to project.resources.text.fromString()...asFile() in the task creation block and that didn’t work, logically enough, so I moved it where it is now, before the task creation.
That initially worked one time but subsequently did not., always because gradle thinks the task has no source files. How can I override this behavior or tell gradle “trust me, source file will be there when you need it”? There ought to be some way to do this.

Ok, the mysterious part of this has been solved. It works when a clean is not done as part of the build but it fails when a clean is done. In retrospect, this is obvious, as project.resources.text.fromString()...asFile() creates this file under ./build.

So how to get around this? If I create the file during the apply phase, before task creation, a clean will wipe it out and the task will see no source files. How can I create it as the first post-clean step in the build?

And then if I get tricky by putting the file creation in a doFirst{} block, I get null pointer exceptions when the task is evaluated.

Ok, so maybe I need to do everything in tasks. Then use the task outputs to avoid specifying variables that are initally null. But this doesn’t work either:

    @Override
    public void apply(Project project) {
        Task gbi = project.tasks.create("generateBuildInfo", DefaultTask) {
            doFirst {
                project.resources.text.fromString(generateBuildInfo(project)).asFile()
            }
        }
        Task cbi = project.tasks.create("copyBuildInfo", Copy ) {
            setDestinationDir(getDest(project))        
            from(gbi.outputs) {
                rename { 'version.properties' }
            }
        }
        cbi.dependsOn(gbi)

    }

The gbi task executes but says “Task has not declared any outputs.” So then cbi won’t execute because it has no source files.

How does a task “declare” outputs?

The central problem with the previous attempt seems to me to be that project.resources.text.fromString(...).asFile() creates a file whose name cannot be known until it is created, thus making it impossible to specify such a file as a task output in advance.

This insight led me to the solution, which is not to base my task on Copy at all. It is Copy that is demanding that there be an input file, in order to execute. Since, in my scenario there is no input file, Copy is an inappropriate choice, instead, this much simpler solution works:

    @Override
    public void apply(Project project) {
        File outputFile = new File (getDest(project), 'version.properties')
        Task gbi = project.tasks.create("generateBuildInfo") {
            outputs.file(outputFile) 
            doFirst {
                project.resources.text.fromString(generateBuildInfo(project)).asFile().renameTo(outputFile)
            }
        }
    }

It is necessary to know the path of the file we are creating in order to declare it as output, using outputs.file() (which was the answer to my previous question.). The DefaultTask does not do this automatically, as I presume Copy does. Now, the task executes.

While on this subject, I note the arguably ugly call to File.renameTo() in my solution. It would be nice-to-have a TextResource.asFile(File) method to handle this for us.