Mimic processResources with non-java-based projects

I am using processResources to “filter” build information (timestamp, id, etc.) into a properties file that is then packaged into a jar or war archive and thereby made accessible to a Java-based application. This is a familiar use case and the gradle implementation works well for it.

But I would like to extend this mechanism to non-java-based builds as well. It might be useful to include such a properties file in other packages, which could somehow be read by the code in these packages, parsed, etc.

Would I have to manually invoke processResources for such cases? Or is there a more generic functionality I could use in such cases?

Thanks.

It occurs to me that what I want might better be achieved by a custom task that simply read things like project.version, buildTimestamp and buildNumber properties from memory (perhaps specified on command line) and wrote them out to a file in properties format somewhere. For java-based builds this could be build/resources/main as it is with processResources, but for other types it could be in some other location. An advantage of this scheme would be that it would eliminate the need to create a template file in src/main/resources with the specified properties.

It seems to me that the most reasonable method of implementing such a task would be to define a template file inside the plugin collection where this task would reside. This template could be read in as a resource, filtered with the info, and then written out to the destination file location. But I know of nothing in Gradle that allows a copy to be made from a stream-based as opposed to a file-based source.

The processResources task isn’t special, it’s just a Copy task. You can get the same functionality by creating your own Copy task and using things like filter() or expand().

As for copying from sources that aren’t necessarily files, it’s true only files can be an input to the Copy task but you can easily bridge between steam/archives sources using a TextResource.

Beautiful! Thanks! That TextResource bridge is what I was looking for!

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.