Problem configuring resources output folder in custom Java plugin

Hi

I’m writing a Gradle plugin in Java currently and wish to apply a few conventions e.g. source folder locations, amongst others.

Currently I’m stuck trying to ensure that resource files get copied to the same output folder as the compiled java class files.

I have tried to customize this desired behavior as such:

JavaPluginConvention convention =  project.getConvention().getPlugin(JavaPluginConvention.class);

SourceSet mainSourceSet= convention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME);

mainSourceSet.getResources().setSrcDirs(Paths.get("source/java"));
mainSourceSet.getOutput().setResourcesDir(Paths.get("build/classes"));

However, when I build the project, an additional folder is added on to the resources output path called ‘java’. So the effective root path for the copied over resources would be ‘build/classes/java’ and not the root folder I specified above.

Interestingly, when I configure this using groovy in the gradle.build script directly as follows:

sourceSets {
    main {
        resources {
            srcDirs = ['source/java']
        }
        output.resourcesDir = 'build/classes'
    }
}

The resources are correctly copied into the build/classes folder without first creating a ‘java’ sub-directory under ‘build/classes’ to use as a root for the copied resources files.

Why is the behavior in my plugin different to what is essentially an identical configuration in the build.gradle file?

Is there something I’m missing here or doing wrong? I’m still gaining familiarity with the Gradle API. Any help would be much appreciated.

Thanks
Mark

It’s possible you are accessing the values before they are evaluated. See build lifecycle. So your problem is that you are accessing the Java Plugin configuration before the custom config in your build.gradle is fully processed.

One solution is accessing the values after configuration is done by registering a project.afterEvaluate callback.

You could try lazy evaluation using a supplier.

Gradle also has some lazy evaluation stuff, which might work for you, but I’m not sure.

Thanks, all of my plugin’s functionality is already wrapped at the plugin entry point by first calling project.afterEvaluate(new Action() { —> do my needed customization. I needed to do this because I have a custom extension block that wasn’t being initialized when I needed it to be.

Yeah I completely misunderstood your problem, and gave you a completely unhelpful answer :frowning:

However, I do have a solution for you.
It appears that gradle treats lists and single files differently when passing them to srcDirs.

  • In your groovy (build.gradle) case you are passing a list ['source/java']
  • In you Java case you are passing a single Path: Paths.get("source/java")

In order get the exact same behavior, you would need to send a list in your java code as well: Collections.singletonList(Paths.get("source/java"))

So I eventually figured out why that might:

The input type to srcDirs/setSrcDirs is Iterable (take a look in the gradle source code for SourceDirectorySet):

When you pass a List<Path> with one item: it seems to handle it as a single Path.
When you pass a Path: it iterates over the path and will process each part of the path as a new source (which actually leads to weird results). You should probably never use path.

1 Like

For further insight.

try using

project.afterEvaluate {
  println "source = " + sourceSets.main.resources.source
}

in your code to see what’s going on in each case.

  • In groovy you should get: source = [source/java]
  • In the old java code you shoudl get: source = [source, java], and it looks like what then ends up happening is that it thinks they’re two different directories:
    • <project-root>/source which contains a java directory (which is why you see it in the final output)
    • <project-root>/java which doesn’t exist and then appears to be ignored
1 Like

Thank you so much! That solved my problem.

It is weird that the behavior is different depending on what type of Iterable you set…do you think that this would classify as a bug? In my opinion it is a bug. If I pass in a directory that I want to use to this method and it prepends another one that I’m not expecting and then the behavior is different when I supply the same path in a different type of object to the same method then that seems wrong to me.

Anyway, hopefully this helps someone else out there somewhere :smiley:

Thanks again, much appreciated.

I mean maybe a check to see if the iterable is a path would be valid for java8+ (and fail with illegal argument exception). But I think iterable is core to their API here, changing it would not be a possibility. The fact that Path is inherently iterable is the another part of the issue. This would not be a problem if you used, for instance, File (because the API would not allow it). Curious to see what the gradle devs say though.

1 Like

Yeah, I would be curious too.

Actually, the javadoc for setSrcDirs specifically states that you can pass in a java.nio.file.Path object. Well it refers you to the java doc for org.gradle.api.Project.files(Object) which it states evaluates the parameter in the same way which includes a number of different Iterable types that are supported, including Path.

Something interesting to add here.

I found some weird behavior when I overrode the source folder for the ‘test’ source set. When I pass in a Path object, representing my test source, to testSourceSet.getJava().setSrcDirs(pathInstance) it also included the Java source from the ‘main’ source set, so my output directory for junit classes also contained classes for the application being tested. As soon as I passed in a Collection instead, like for what solved the other issue above, the problem went away and my output folder only contained test class files.

This problem does not present itself when you do something similar to the ‘main’ java SourceSet.

The lesson learned here is that if you pass in a Path object to setSrcDirs, then weird unexpected behavior will occur depending on the type of SourceSet you want to configure. So, like you said, best to avoid Path for now and see if the Gradle devs have any comments on this.

Yeah I think the documentation here is misleading/wrong: https://docs.gradle.org/current/javadoc/org/gradle/api/file/SourceDirectorySet.html#setSrcDirs(java.lang.Iterable)

One is accepting Iterable<?> and one is accepting Object...

It might help to file a bug?