Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow defined and automatic modules to co-exist #164

Closed
wants to merge 2 commits into from

Conversation

hgschmie
Copy link
Contributor

@hgschmie hgschmie commented Aug 18, 2023

In a maven project, a maven module creates two artifacts:

  • a main artifact with a module-info.class file that defines a JPMS
    module "foo.bar". The jar is called foo-bar-1.0.jar
  • a test artifact which is not a JPMS module. It is called
    foo-bar-1.0-tests.jar

Another module declares dependencies on both modules:

<dependencies>
  <dependency>
    <groupId>xxx</groupId>
    <artifactId>foo-bar</artifactId>
    <version>1.0</version>
  </dependency>

  <dependency>
    <groupId>xxx</groupId>
    <artifactId>foo-bar</artifactId>
    <version>1.0</version>
    <classifier>tests</classifier>
    <scope>test</scope>
  </dependency>
</dependencies>

This is a common use case in large projects. The LocationManager now
creates two JavaModuleDescriptors for the "foo.bar" JPMS module. One
from the main artifact and its module-info.class (automatic == false)
and one from the test artifact (automatic == true).

The current code considers these duplicates and drops one. As a result,
the test code no longer compiles because a dependency is missing.

The patch separates out modules with a module descriptor and automatic
modules.

Then it adds the modules with module descriptors to the module path.
Any duplicate is dropped for modules with module descriptors.

Finally, it adds the automatic modules; if a module with the same module
id already exists on the module path, it adds it to the class path.

In the case above, this will allow the compile to successfully compile
test code (the tests dependency drops to the class path, while the main
artifact is on the module path).

@olamy
Copy link
Member

olamy commented Aug 18, 2023

how both .jar files have been created. We need something to be able to build them.
Do you have any IT test ready for your use case with m-compiler-p?

@rfscholte
Copy link
Member

rfscholte commented Aug 18, 2023

Assuming one contains the unittest classes of the other jar, this will lead to package name collisions. I'm pretty sure this is not going to work this way. If tests are reusable by other projects, it should be a separate project/module.
org.springframework.security:spring-security-test is an example how this should be done. Having the unittest exposed as a separate jar doesn't make sense nowadays

@mthmulders
Copy link

I can foresee situations where a Maven project has production code, covered by tests. The test sources could also contain some factories or fakers for domain classes from the production code. As long as these are in a separate package, those wouldn't lead to package name collisions - but it would require the test-jar to only contain that package, not the actual unit tests. Exporting them generally doesn't make sense, I agree.

@hgschmie
Copy link
Contributor Author

In our project (https://github.com/jdbi/jdbi), I am planning to generate actual JPMS modules (with a module-info) for the main artifacts. This is what others will consume and we want to support JPMS there. Our test code is packaged in ...-tests.jar artifacts, which will not be JPMS modules but just plain old java jars.

I will attach a number of traces from the compile run of the "jdbi3-caffeine-cache" maven module, which depends on "jdbi3-core". These are taken from our regular build / my experimental JPMS build.

Currently, we generate all jars with Automatic-Module-Name (and we reuse the id for all artifacts from a single maven module so the main artifact, test artifact etc. all use the same module id).

What happens now is that the compiler plugin will see both artifacts (jdbi3-core-3.41.1-SNAPSHOT.jar) and (jdbi3-core-3.41.1-SNAPSHOT-tests.jar) and put them on the class path (because we don't use the module system in our current build). I have attached a trace of the build without JPMS (logfile-nojpms), that shows

[DEBUG]   (f) testPath = [
  /Users/henning/code/jdbi/cache/caffeine-cache/target/test-classes
  /Users/henning/code/jdbi/cache/caffeine-cache/target/classes
  /Users/henning/.m2/repository/com/github/ben-manes/caffeine/caffeine/3.1.8/caffeine-3.1.8.jar
  /Users/henning/.m2/repository/com/google/errorprone/error_prone_annotations/2.21.1/error_prone_annotations-2.21.1.jar
  /Users/henning/.m2/repository/com/h2database/h2/2.2.220/h2-2.2.220.jar
  /Users/henning/.m2/repository/io/leangen/geantyref/geantyref/1.3.14/geantyref-1.3.14.jar
  /Users/henning/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar
  /Users/henning/.m2/repository/net/bytebuddy/byte-buddy/1.12.21/byte-buddy-1.12.21.jar
  /Users/henning/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
  /Users/henning/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar
  /Users/henning/.m2/repository/org/checkerframework/checker-qual/3.37.0/checker-qual-3.37.0.jar
----> HERE   /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT-tests.jar
----> HERE   /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT.jar
  /Users/henning/.m2/repository/org/jdbi/jdbi3-testing/3.41.1-SNAPSHOT/jdbi3-testing-3.41.1-SNAPSHOT.jar
  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar
  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar
  /Users/henning/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar
  /Users/henning/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
  /Users/henning/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
  /Users/henning/.m2/repository/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
]
[...]
[DEBUG] Classpath:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/test-classes
[DEBUG]  /Users/henning/.m2/repository/com/h2database/h2/2.2.220/h2-2.2.220.jar
[DEBUG]  /Users/henning/.m2/repository/net/bytebuddy/byte-buddy/1.12.21/byte-buddy-1.12.21.jar
[DEBUG]  /Users/henning/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-testing/3.41.1-SNAPSHOT/jdbi3-testing-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/classes
[DEBUG]  /Users/henning/.m2/repository/com/github/ben-manes/caffeine/caffeine/3.1.8/caffeine-3.1.8.jar
[DEBUG]  /Users/henning/.m2/repository/com/google/errorprone/error_prone_annotations/2.21.1/error_prone_annotations-2.21.1.jar
[DEBUG]  /Users/henning/.m2/repository/io/leangen/geantyref/geantyref/1.3.14/geantyref-1.3.14.jar
[DEBUG]  /Users/henning/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar
[DEBUG]  /Users/henning/.m2/repository/org/checkerframework/checker-qual/3.37.0/checker-qual-3.37.0.jar
----> HERE [DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
----> HERE [DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT-tests.jar
[DEBUG] Source roots:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/src/test/java
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/generated-test-sources/test-annotations

So the jars show up on the test path, get added to the class path. All good. We've done this forever and it works just fine.

Now, adding a module-info to the main artifact of jdbi3-core and the jdbi-caffeine-cache maven modules results in this:

[DEBUG]   (f) testPath = [
  /Users/henning/code/jdbi/cache/caffeine-cache/target/test-classes
  /Users/henning/code/jdbi/cache/caffeine-cache/target/classes
  /Users/henning/.m2/repository/com/github/ben-manes/caffeine/caffeine/3.1.8/caffeine-3.1.8.jar
  /Users/henning/.m2/repository/com/google/errorprone/error_prone_annotations/2.21.1/error_prone_annotations-2.21.1.jar
  /Users/henning/.m2/repository/com/h2database/h2/2.2.220/h2-2.2.220.jar
  /Users/henning/.m2/repository/io/leangen/geantyref/geantyref/1.3.14/geantyref-1.3.14.jar
  /Users/henning/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar
  /Users/henning/.m2/repository/net/bytebuddy/byte-buddy/1.12.21/byte-buddy-1.12.21.jar
  /Users/henning/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
  /Users/henning/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar
  /Users/henning/.m2/repository/org/checkerframework/checker-qual/3.37.0/checker-qual-3.37.0.jar
----> HERE  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT-tests.jar
----> HERE  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT.jar
  /Users/henning/.m2/repository/org/jdbi/jdbi3-testing/3.41.1-SNAPSHOT/jdbi3-testing-3.41.1-SNAPSHOT.jar
  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar
  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar
  /Users/henning/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar
  /Users/henning/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
  /Users/henning/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
  /Users/henning/.m2/repository/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
]
[...]
[DEBUG] Classpath:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/test-classes
[DEBUG]  /Users/henning/.m2/repository/com/h2database/h2/2.2.220/h2-2.2.220.jar
[DEBUG]  /Users/henning/.m2/repository/net/bytebuddy/byte-buddy/1.12.21/byte-buddy-1.12.21.jar
[DEBUG]  /Users/henning/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-testing/3.41.1-SNAPSHOT/jdbi3-testing-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
[DEBUG] Modulepath:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/classes
[DEBUG]  /Users/henning/.m2/repository/com/github/ben-manes/caffeine/caffeine/3.1.8/caffeine-3.1.8.jar
[DEBUG]  /Users/henning/.m2/repository/com/google/errorprone/error_prone_annotations/2.21.1/error_prone_annotations-2.21.1.jar
[DEBUG]  /Users/henning/.m2/repository/io/leangen/geantyref/geantyref/1.3.14/geantyref-1.3.14.jar
[DEBUG]  /Users/henning/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar
[DEBUG]  /Users/henning/.m2/repository/org/checkerframework/checker-qual/3.37.0/checker-qual-3.37.0.jar
----> HERE [DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
[DEBUG] Source roots:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/src/test/java
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/generated-test-sources/test-annotations

The test path passed into the maven test compile is still exactly the same as in the non-JPMS build. But the compiler plugin now splits the artifacts into class path (anything that has automatic module naming) and module path (anything that actually has a module-info file OR a Automatic-Module-Name entry in its manifest). And while doing so, it detects a conflict between something that needs to go onto the module path (jdbi3-core-3.41.1-SNAPSHOT.jar, which contains a module-info) and another dependency that happens to have the same name (jdbi3-core-3.41.1-SNAPSHOT-tests.jar, whose module id was generated from the file name). And based on that conflict, it silently (!) drops the jdbi3-core-3.41.1-SNAPSHOT-tests.jar from the build (it neither shows up on the module path (correct, it is not a module) or the class path nor does the plugin error out. It simply fails the compile with "cannot find symbol".

There actually is a simple workaround: Changing the module id of the main artifact from jdbi3.core to org.jdbi.v3.core. This now results in the main artifact having this module id and the test artifact will get a generated id of jdbi3.core. This works as expected:

[DEBUG] Classpath:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/test-classes
[DEBUG]  /Users/henning/.m2/repository/com/h2database/h2/2.2.220/h2-2.2.220.jar
[DEBUG]  /Users/henning/.m2/repository/net/bytebuddy/byte-buddy/1.12.21/byte-buddy-1.12.21.jar
[DEBUG]  /Users/henning/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/assertj/assertj-core/3.24.2/assertj-core-3.24.2.jar
[DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-testing/3.41.1-SNAPSHOT/jdbi3-testing-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar
----> HERE [DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT-tests.jar
[DEBUG] Modulepath:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/classes
[DEBUG]  /Users/henning/.m2/repository/com/github/ben-manes/caffeine/caffeine/3.1.8/caffeine-3.1.8.jar
[DEBUG]  /Users/henning/.m2/repository/com/google/errorprone/error_prone_annotations/2.21.1/error_prone_annotations-2.21.1.jar
[DEBUG]  /Users/henning/.m2/repository/io/leangen/geantyref/geantyref/1.3.14/geantyref-1.3.14.jar
[DEBUG]  /Users/henning/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar
[DEBUG]  /Users/henning/.m2/repository/org/checkerframework/checker-qual/3.37.0/checker-qual-3.37.0.jar
----> HERE [DEBUG]  /Users/henning/.m2/repository/org/jdbi/jdbi3-core/3.41.1-SNAPSHOT/jdbi3-core-3.41.1-SNAPSHOT.jar
[DEBUG]  /Users/henning/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar
[DEBUG] Source roots:
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/src/test/java
[DEBUG]  /Users/henning/code/jdbi/cache/caffeine-cache/target/generated-test-sources/test-annotations

Main artifact goes onto the module path. Test artifact goes onto the class path. Compiler is happy. Tests run fine (because they are executed from the class path and any artifact on the class path is allowed to access the module path).

While this works for me, I consider this a source of "gotcha" unexpected behavior. Migration to JPMS invites using the "automatic module id" as the actual id in a "module-info" file and suddenly compilation fails. Or complicated rework of a large build is necessary.

With the proposed patch applied, the compiler plugin behaves exactly as it does with the workaround above (see logfile-jpms-fix) by moving an "automatically named" module onto the class path and keep an explicitly named (module-info or manifest) on the module path.

I don't have a maven compiler plugin integration test yet, happy to whip one up if this proposed change would be considered for inclusion.

logfile-nojpms.txt
logfile-jpms-workaround.txt
logfile-jpms-fix.txt
logfile-jpms.txt

@hgschmie
Copy link
Contributor Author

(not sure why the unit tests fail on windows, I run them locally on macos and they are fine).

@rfscholte
Copy link
Member

I think Windows is failing because jdbi3-core-3.41.1-SNAPSHOT-tests and jdbi3-core-3.41.1-SNAPSHOT are considered different versions of the same module (i.e. jdbi3.core) and that given the way the Java code resolves the automatic module name, it makes sense.
Changing jdbi3.core to org.jdbi.v3.core is probably not just a work around, but even a better name for the module. (reminds me of the days where people didn't understand the difference between groupId and artifactId, so just give it the same value).
I'd like to close this as won't fix, even though you've applied this pattern very often, JPMS comes with a new set of rules and this old habit just doesn't fit in as it used to be.

@hgschmie
Copy link
Contributor Author

hi @rfscholte , thanks for looking at the PR. Why would the windows build behave differently on VM level than the build on MacOS and Linux. Could you explain the "given the way the java code resolves the automatic module name, it makes sense" comment? It seems to imply that Java on Windows resolves automatic modules names differently than on Linux or MacOS.

I am disagreeing with your assessment; there is an implication that the VM can not deal with multiple artifacts with the same module id spread out across module and classpath. This is not true, as the patch applied to the plexus-java component makes the compiler work correctly.

The current behavior is observed because of a choice that the plexus-java component made when distributing artifacts across module and class path. Ironically, if the components had different module ids it would spread them out exactly as it would spread them with this patch and the same module id. It would still fail if there were two "real" JPMS modules (not automatically named).

Thank you for implying that I don't understand the module system and educating me on the origins of package names. Here is what is happening in our project: We have published automatic module names (org.jdbi.v3.<....>) for a very long time. We use this id for all artifact jars distributed. As those are automatically named modules, it is not a problem. We are now considering to use JPMS for our main artifacts. So it is only natural that we keep using this id for the JPMS artifacts (because we have users depending on those and we will not break our users). What is happening now is that I have a working build that contains main artifact (automatic) and testing artifact (automatic) and the build works. Now I switch to main artifact (JPMS) and testing artifact (automatic) and the build breaks. And it does not break because of a limitation in the JPMS system, the compiler or the runtime BUT the policy by which the plexus-java component tries to distribute artifacts across module and class path. Because this patch shows that this scenario (JPMS main module and automatic testing module) can be used.

At the very least, the component should fail and not silently drop one dependency from the class path. The resulting visible behavior to users is confusing because unless you know where to look, it is opaque where the errors come from.

hgschmie added a commit to hgschmie/jdbi that referenced this pull request Aug 19, 2023
add an automatic module id to the test jars, so test jar and main jar
can coexist on the module path.
@rfscholte
Copy link
Member

There should be no differences between OSes, but the names of the failing tests are related to the kind of changes you are making. I haven't checked your patch, because there's still a disagreement on how plexus-java should work,
If you are aware that your jar and test-jar share the same pom-file, whereas they should both have their own set of dependencies, you might see the real issue here. So I'm not interested in where to put jars so it works as before, but more about where to put which code in your Maven project.
JPMS gave us the opportunity to reexamine this and to stricten things (as is already came with more strict rules), so I hope you understand that what we consider bad practice won't be introduced in plexus-java.

@hgschmie
Copy link
Contributor Author

I think "what we consider" is the important phrase here.

As you have freely admitted that you haven't even looked at the change and the condescending language that you use, I don't think there is any point for me to argue with you.

@hgschmie
Copy link
Contributor Author

how both .jar files have been created. We need something to be able to build them. Do you have any IT test ready for your use case with m-compiler-p?

The included jars are simply "jar cf" commands on directories that contain the required files. Happy to add those; there are a number of other jars included with the source code where it is not clear how those were created e.g. jar with spaces in path/plexus-java-1.0.0-SNAPSHOT.jar or jar.empty.2/plexus-java-2.0.0-SNAPSHOT.jar so I was not sure if you actually need those.

Copy link
Member

@gnodet gnodet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, except the IT failures on windows ...

@hgschmie
Copy link
Contributor Author

I'd love to understand the Windows failures. Unfortunately I don't have access to any windows box myself and all the MacOS / Linux things work ok. The fact that it fails consistently worries me as this implies that there is something systemic. I can't re-run the github actions; could you rerun them with debug logging enabled?

@hgschmie
Copy link
Contributor Author

hgschmie commented Aug 22, 2023

There should be no differences between OSes, but the names of the failing tests are related to the kind of changes you are making. I haven't checked your patch, because there's still a disagreement on how plexus-java should work, If you are aware that your jar and test-jar share the same pom-file, whereas they should both have their own set of dependencies, you might see the real issue here. So I'm not interested in where to put jars so it works as before, but more about where to put which code in your Maven project. JPMS gave us the opportunity to reexamine this and to stricten things (as is already came with more strict rules), so I hope you understand that what we consider bad practice won't be introduced in plexus-java.

The problem was that Filenames on Windows and *ix hash differently, so the test, which relies on the exact order of the files passed into the location manager retained not the first but the second file. The PR used a HashMap where it should use a LinkedHashMap. After fixing this, the tests pass on windows (at least locally for me).

@rfscholte
Copy link
Member

Let me try to express my concerns again: just like javac there's no such things as scoped artifacts, i.e. the difference between compile and test: it is just a bunch of jars and that will be divided over the modulepath and classpath. In case of a required jar you cannot assume that it is pointing to the explicit module instead of the automatich module. Hence the way to solve this is to make sure they have different names, not by adding this logic!

@gnodet
Copy link
Member

gnodet commented Aug 22, 2023

Let me try to express my concerns again: just like javac there's no such things as scoped artifacts, i.e. the difference between compile and test: it is just a bunch of jars and that will be divided over the modulepath and classpath. In case of a required jar you cannot assume that it is pointing to the explicit module instead of the automatich module. Hence the way to solve this is to make sure they have different names, not by adding this logic!

Aren't you assuming that both jars have module names ? In this case, only one has a module name, not both.

@hgschmie
Copy link
Contributor Author

The differentiator is "has a module descriptor" and "does not have a module descriptor". jars that have a module descriptor must go on the module path. It becomes much more murky with jars that have either an automatic module name in the manifest or have the vm generate a module name based on the filename. Fundamentally, those need to be treated the same (because the VM treats them the same).

What can happen is that you have a jar that has a module descriptor and a jar that has not that clash. In that case, the former must go on the module path. We can now argue what should happen with the second:

  1. It should be silently dropped which causes follow-on errors
  2. Depending on the order, an automatic named module may displace a module with a module descriptor from the module path which causes follow-on errors
  3. It can be put on the class path which does not affect the code on the module path (which never accesses the class path) but allows other scenarios (such as the test scenario) to succeed (as the code on the class path can access the module path)
  4. It should cause an explicit build error that is documented to the user with a call for action

The current behavior is 1 + 2 (which is what I am observing).

I propose that "3" is a better world to live in (the patch explicitly prefers modules with a module descriptor) and matches a large number of real world scenarios where projects transition from "no JPMS support" over "automatic module names" to "module descriptors".

The fourth option is also a viable alternative IMHO, except that it is user unfriendly where a better alternative exists.

@rfscholte
Copy link
Member

Aren't you assuming that both jars have module names ? In this case, only one has a module name, not both.

Every jar has a module name, either explicit (so it has a module descriptor, hence you also know its requirements) or automatic (either via the manifest of by filename). Suppose my module descriptor contains a requirement on module q, then there's zero guarantee it is pointing to the explicit one. It could very well the filenamebased one, while the explicit one is pulled in via a different module.

@rfscholte
Copy link
Member

So far this code can be explained in a single line: it takes the ordered list of jars, scans the used module descriptors and put all required jars on the modulepath, the rest on the classpath.
Also know that the Java Specification describes how to handle duplicate module names: pick the first.
I could have decided to leave modules with the same name on the module path and leave it up to Java to pick the first (there was even a bug for it #54 ), but I chose to clean up those duplicates so it would hopefully be easier for users to understand what's happening. So I don't see dropping modules as an error, as the result will be exactly the same.

I still don't support the usecase, especially because module name collisions can be avoided by defining a proper and unique module name and as far as I understand it is not a third party library, so you do control the module names.

While writing this code I considered it as a utility, so I wanted to avoid adding a logging framework (even JUL). The ResolvePathsResult should contain all the information and the consumer (e.g. maven-compiler-plugin or maven-surefire-plugin) can decide what to do. So what might be missing is the reason why this module is not on either modulepath or classpath. It is possible to figure out its modulename and discover it is a duplicate, but maybe it makes sense to raise a pathexception for duplicatemodulename for better understanding.

In a maven project, a maven module creates two artifacts:

- a main artifact with a module-info.class file that defines a JPMS
  module "foo.bar". The jar is called foo-bar-1.0.jar
- a test artifact which is not a JPMS module. It is called
  foo-bar-1.0-tests.jar

Another module declares dependencies on both modules:

<dependencies>
  <dependency>
    <groupId>xxx</groupId>
    <artifactId>foo-bar</artifactId>
    <version>1.0</version>
  </dependency>

  <dependency>
    <groupId>xxx</groupId>
    <artifactId>foo-bar</artifactId>
    <version>1.0</version>
    <classifier>tests</classifier>
    <scope>test</scope>
  </dependency>
</dependencies>

This is a common use case in large projects. The LocationManager now
creates two JavaModuleDescriptors for the "foo.bar" JPMS module. One
from the main artifact and its module-info.class (automatic == false)
and one from the test artifact (automatic == true).

The current code considers these duplicates and drops one. As a result,
the test code no longer compiles because a dependency is missing.

The patch separates out modules with a module descriptor and automatic
modules.

Then it adds the modules with module descriptors to the module path.
Any duplicate is dropped for modules with module descriptors.

Finally, it adds the automatic modules; if a module with the same module
id already exists on the module path, it adds it to the class path.

In the case above, this will allow the compile to successfully compile
test code (the tests dependency drops to the class path, while the main
artifact is on the module path).
@hgschmie
Copy link
Contributor Author

hgschmie commented Sep 2, 2023

I keep coming back to this thread and I am still not sure how to proceed. @rfscholte you keep talking about logging frameworks. No one but you has suggested any logging framework.

The way I see it, the current behavior is simply wrong. Dropping a path element quietly leads to problems.

Looking at the options above, we are currently at "1". I can live with "4" which will show the problems to the user and they can fix their build (and don't have to guess what went wrong). I would be more comfortable with implementing 2 and 3 (which is what this PR does) but at the very minimum this should end up as a path exception.

We are discussing this for two weeks now without any clear resolution in sight. Even though it has been approved, it is not applied.

What is the next step?

@rfscholte
Copy link
Member

There's not a "not approve" option, otherwise I would have picked that one, which would make it still undecided.
I can accept the option of adding the duplicate module as a path exception, that is most likely what can be done with this issue.

@hgschmie
Copy link
Contributor Author

Ok, then let's do that. I will close this PR and submit a new one that errors out if it sees multiple modules with the same module name. My interest is around making this predictable and actionable for users. The current behavior is not.

@hgschmie
Copy link
Contributor Author

Also know that the Java Specification describes how to handle duplicate module names: pick the first.

That is good to know, thank you for pointing this out. I admittedly only glanced at the JLS and JEP 261 and the only reference that I could find was in JEP 261:

"[...]When searching a module path for a module of a particular name, the module system takes the first definition of a module of that name. Version strings, if present, are ignored; if an element of a module path contains definitions of multiple modules with the same name then resolution fails and the compiler, linker, or virtual machine will report an error and exit.[...]"

If this is relevant to the module path resolution, it seems that erroring out might be an acceptable outcome.

@rfscholte rfscholte closed this Sep 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants