Mimic processResources with non-java-based projects

Hmm. But these are interfaces and all the concrete implementations I find in Gradle source are internal, not public apis and even these require as constructor parameters other objects from the internal api.

Is there a concrete example that shows how all this might be set up?

Thanks.

You should be using TextResourceFactory to create one of these. The DSL reference shows examples.

Thanks again. I made the mistake of jumping right into the Javadoc weeds.

Basically, if I understand you correctly, I’ll use this example:

def sourcedFromString = resources.text.fromString("some text content")
where “some text content” is my “template”

and then call sourcedFromString.asFile()

I’d rather use:

def sourcedFromArchiveEntry =
  resources.text.fromArchiveEntry("path/to/archive.zip", "path/to/archive/entry.txt")

but I’d like to specify as the first parameter, the archive to which this code (which will be in a plugin) will belong (‘my archive’, i.o.w, what you’d get from getResourceAsStream() ) but I don’t see how to do that.

Yeah, there currently isn’t an option to load a file off the classpath. In that case I’d use something like commons-io to make this a bit simpler.

OK getting around to doing this, but I can’t actually get the task to execute. --info level output tells me that this is because there are no source files. This is indeed the case but I thought this is what TextResource was supposed to provide.

My code is as follows:

    static final String TEMPLATE =
'''
version=${vn}
buildId=${id}
buildTimestamp=${ts}
'''

    protected String generateBuildInfo() {
        TEMPLATE.replace('${vn}', project.version)
            .replace('${id}', project.findProperty('buildNumber') ?: 'unknown')
            .replace('${ts}', project.findProperty('buildTimestamp') ?: 'unknown')    
    }

    @Override
    protected void copy() {
        def buildInfoResource = project.resources.text.fromString(generateBuildInfo())
        
        into("$dest/version.properties") {
            from buildInfoResource
        }       
        super.copy()
    }

Might want to try from buildInfoResource.asFile().

Yeah, that occurred to me after I posted this but I still can’t make it work. Same issue even though there is a file. Here is my latest code.

public abstract class GenerateBuildInfo
extends Copy {
    
    File info
    
    GenerateBuildInfo() {
        super()
        info = project.resources.text.fromString(generateBuildInfo()).asFile()
        println "GenerateBuildInfo() - ${info.getPath()}"
    }

    static final String TEMPLATE =
'''
version=${vn}
buildId=${id}
buildTimestamp=${ts}
'''

    protected String generateBuildInfo() {
        TEMPLATE.replace('${vn}', project.version)
            .replace('${id}', project.findProperty('buildNumber') ?: 'unknown')
            .replace('${ts}', project.findProperty('buildTimestamp') ?: 'unknown')    
    }

    @Override
    protected void copy() {
        into("$dest") {
            from(info.parent) {
                include info.name
                rename 'version.properties'
            }
        }
    }

    protected abstract String getDest();
}

Your problem is you should not be extending Copy and specifically, you should not be overriding the copy() method. The issue here is that copy() is the task action. What you are doing is overriding this method with what is configuration logic and therefore the copy operation actually never happens.

What I would instead suggest is that you create a task of type Copy and simply configure it as such. Extending task types should only be done when you want to change their behavior. In this case that isn’t what you want. You want a copy task, you just want it configured a certain way.

One thing I’ve never fully understood is the difference between extending a task class and declaring a task with the type of a task class. Probably this is because most of the gradle user guide documentation is geared toward writers of build scripts as opposed to writers of plugins. There is a distinct lack of examples of doing what you suggest in a groovy class inside a plugin jar. I’ve been bitten over and over by this.

In any case, I have now tried still keeping my classes (which do add some important default behavior) but following your suggestion of not trying to do the configuration in a copy() override method. This succeeds, but I had to explicitly define the task’s destinationDir and then not use an into{} closure (which isn’t necessary once destinationDir is defined) in my configuration.

The pattern is pretty simple, put this inside a plugin.

public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.tasks.create("myCopyTask", Copy) {
            // put custom task configuration here
        }
    }
}

Thanks, that’s simple enough. I was trying to avoid creating a separate plugin to do this task and creating the task in another preexisting plugin, but I’m having another buildshiip-only issue doing it my way, and I will now try to do this as you suggest. I will need two plugins to capture the two different flavors of this functionality, but that’s probably the best way to go.

So let me try to cast this remark of yours into a rule-of-thumb:

If you want to to create a version of an existing task for a plugin, don’t extend it via subclassing. Instead create it in a new plugin and apply that plugin. Inside the apply() method, do any configuration necessary.

That about right?

One minor quibble: this doesn’t work outside of build scripts:
project.tasks.create("myCopyTask", Copy)

Instead, I had to import the Copy class, and then do
project.tasks.create("myCopyTask", Copy.class)

Anyway, thanks for your help.

Yep.[quote=“sc1478, post:16, topic:18892”]
One minor quibble: this doesn't work outside of build scripts:project.tasks.create("myCopyTask", Copy)

Instead, I had to import the Copy class, and then do project.tasks.create("myCopyTask", Copy.class)
[/quote]

The .class suffix is required when writing your plugin in Java. It can be omitted if you are using Groovy.

didn’t find that to be so. My plugin is in groovy and it wouldn’t compile that way.

You do still have the import the class.

Aha! So that’s the secret.

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.