Merge SourceSet output generically for JPMS

Hello, so I am trying to find a simple way to develop module projects. Because we all know the JPMS is a wonderful system that has no flaws. Anyways, Java’s ModulePath supports exploded directories presumably to facilitate dev time. However this system has no way of knowing that classes and resources can be in different directories because gradle builds them using two different tasks.

Eclipse is easy to support this because it just shoves the files together by default only splitting based on sourceset name I couldn’t find the code for the IntelliJ plugin, but it uses the gradle sourceSet directories.

I was able to get something functional by using your example in SourceSetOutput. However this breaks down due to task input tracking when working with the groovy plugin. because compileGroovy is ‘using’ the resources directory when it is actually using the classes directory. this can be fixed by compileGroovy.dependsOn(processResources) however I would like a more generic solution that works with other language plugins if possible.

Because of the insistence of having resources separated we’ve had to use some rather annoying hacks over the years. Things like guessing environments based on layout, explicitly iterating all paths and having a whole NIO Filesystem based around merging directories.

Basically what I am trying to do is figure out how to tell gradle that I need all the resources merged into the classes directory. In a way that makes it, intellij, and any other plugins happy. Hoping I missed something really simple.

Yeah, doing such a configuration is a pretty bad idea.
I think that JavaDoc of SourceSetOutput is a very bad idea and probably just not got updated.
Long long ago it might have been a viable way.

Giving a concrete recommendation is not too easy, because if I just comment out that part, the build works, so the MCVE does not actually show why this setup is necessary after all and where exactly.

If it is about compile time in a multi-project build, it could for example maybe be mitigated using the org.gradle.java.compile-classpath-packaging system property. If it is about some runtime maybe some attributes can be used, and so on. Or what you can always do is to use --patch-module to patch the resource directory into the module defined by the classes directory. Also for a broad part Gradle also supports JPMS module development already, so maybe you even do not need something special.

I’ve made a slightly simpler example. To reproduce the issue you can run the gradlew runServer command. It’ll take some time the first time to download/build the necessary dependencies. Its building and running a functional minecraft server. So just a heads up. As is, it should crash with a similiar to:

net.minecraftforge.fml.LoadingFailedException: Loading errors encountered: [
        The Mod File Z:\example\build\resources\main has mods that were not found
]

This is because it is attempting to locate the mods to load via the class/module path. With the files unmerged the two directories are treated as separate modules. So when it finds the mods.toml and tries to search that directory for a .class file with the matching ID it fails.

However if you then uncomment the last few lines of build.gradle where it merges the directories and then run gradlew runServer again. You will instead of met with a Mod Detected Successfully as the last line. As the mod class logs that message and exits.

    public ExampleMod() {
        LOGGER.info("Mod Detected Successfully");
        System.exit(0);
    }

To say that our setup is non-standard would be an understatement. We’re doing Minecraft mod development which involves runtime patching, dep time library generation and a bunch of other weird hacks. I’m working on getting things cleaned up.

Beyond that, using command line arguments such as --patch-module is really not possible because of the hacky environment we’re working with. We’re bootstrapping from a non-modular environment to a modular environment. And part of the whole point is trying to simplify the build process so that knowing exact IDE dependent layouts is not required.

Heck i’d like it even more if we could tell IDEs to jar up the entire thing instead of being flatdir to better mimic runtime environment. But I don’t think that is possible.

So i’m trying to keep it as simple as possible. Just shove everything into a single directory, and have that be the entry on the classpath. It works fine except for gradle not liking that outputs of multiple tasks go to the same place.

using command line arguments such as --patch-module is really not possible because of the hacky environment we’re working with

That’s not really the reason.
The reason is, that you are not doing anything with JPMS here and not having anything on the modulepath.
I don’t see any JPMS modules involved here.
The problem is more with the fmlloader that searches for all META-INF/mods.toml on the classpath and has logic that similar to JPMS does not support having that file and the actual mod class on different entries on the classpath.

Imho, the proper fix would be to fix the fmlloader to properly handle this situation by not requiring that mods.toml and the implementing class are in the same classpath entry, or at least in case it is not in a jar, as there anyway is a recognition present whether it is in a jar or not.

The next best thing would probably be to fix MinecraftRunTask by replacing

runConfig.getAllSources().stream().map(SourceSet::getRuntimeClasspath).forEach(this::classpath);

with

runConfig.getAllSources().stream().flatMap(sourceSet -> Stream.of(
        project.getTasks().named(sourceSet.getJarTaskName()),
        project.getConfigurations().named(sourceSet.getRuntimeClasspathConfigurationName())
)).forEach(this::classpath);

which basically is what also the built-in application plugin is doing if a JPMS module project is to be run.