Language support · java modules

Java Platform Module System (JPMS)

Curie supports the Java Platform Module System (JPMS, Java 9+) with zero configuration. Adding a module-info.java to your project source root activates modular mode automatically.

Activation

A project becomes modular when module-info.java exists at the root of a production Java source directory:

No Curie.toml flag is needed. Deleting module-info.java reverts the project to classpath mode — matching Maven and Gradle behaviour.

Example module-info.java

module com.example.greeter {
    requires com.fasterxml.jackson.databind;
    exports com.example.greeter;
}

How the module/classpath split works

Curie inspects each resolved dependency JAR to determine its module identity:

  1. Explicit module — JAR contains module-info.class at its root. The module name comes from the binary class file.
  2. Automatic module (manifest) — JAR has Automatic-Module-Name in META-INF/MANIFEST.MF. Stable name provided by the library author.
  3. Automatic module (filename) — module name derived from the JAR filename using the JDK algorithm (strip version suffix, replace non-alphanumerics with dots). Less stable; prefer libraries that declare Automatic-Module-Name.

A JAR goes on --module-path when its module name appears in your module-info.java's requires directives (directly or transitively). All other JARs stay on -cp (the unnamed module). This mirrors how maven-compiler-plugin behaves, ensuring Maven parity.

Compilation

When modular mode is active, Curie passes these extra flags to javac:

javac --module-path <required dep jars>
      -cp <remaining dep jars>
      ... (other flags unchanged)
      src/main/java/module-info.java src/.../*.java

Kotlin projects

Kotlin sources are compiled in Phase 1 with kotlinc (on the classpath, as Kotlin does not enforce JPMS at compile time). module-info.java is intentionally excluded from the kotlinc invocation — Phase 2 re-compiles it with javac --patch-module <name>=target/classes so the module descriptor covers the Kotlin-compiled classes.

Because Kotlin bytecode references kotlin.stdlib at runtime, a modular project that mixes Kotlin sources must declare the dependency in module-info.java and open any packages that reflection-based libraries (e.g. Jackson) need to access:

module com.example.greeter {
    requires com.fasterxml.jackson.databind;
    requires kotlin.stdlib;
    opens com.example.greeter to com.fasterxml.jackson.databind;
}

Curie automatically places kotlin-stdlib on --module-path when it appears in the module graph (BFS expansion), so no manual classpath wiring is needed.

Groovy projects

Groovy production sources in a modular project are a hard error. Groovy's runtime metaprogramming is incompatible with strong encapsulation. Spock tests are not affected because tests run on the classpath (see Testing below).

Testing

By default, tests run in classpath mode regardless of whether the project is modular. Production classes (including module-info.class) are placed on the test classpath where they are treated as the unnamed module. This means:

Maven parity: the generated pom.xml sets <useModulePath>false</useModulePath> in maven-surefire-plugin to match this behaviour.

Running

For modular applications without a fat JAR, curie run uses the modular launch form:

java --module-path target/myapp-1.0.jar:<module-path deps>
     [-cp <classpath deps>:src/main/resources]
     --module com.example.greeter/com.example.greeter.Main

Fat-JAR projects continue to use java -jar; module-info.class is stripped from fat JARs because a shaded artifact cannot have a valid module descriptor.

Library authors: Automatic-Module-Name

Non-modular libraries can declare a stable module name so downstream modular projects can depend on them predictably. Add to Curie.toml:

[library]
name = "my-utils"
version = "1.0.0"
automaticModuleName = "com.example.myutils"

This writes Automatic-Module-Name: com.example.myutils into the JAR's MANIFEST.MF. Do not set this when the project also has a module-info.java — the module descriptor is authoritative.

Optional configuration

Most modular projects need no additional configuration. For escape hatches that cannot be inferred:

[modules]
# Extra root modules (mirrors javac/java --add-modules)
add-modules = ["jdk.incubator.vector"]

# Applied to compile, test, and run
add-opens   = ["my.module/com.example.internal=ALL-UNNAMED"]
add-exports = []
add-reads   = []

# Test mode: "classpath" (default) or "module-path" (phase 2)
test-mode = "classpath"

The [modules] section is entirely optional. These settings are workspace-inheritable: declare them in the workspace root Curie.toml to apply them to all members.

Workspaces

In a workspace, each member is compiled independently. A modular member's JAR is treated like any other dependency JAR by consumers: it is an explicit module (if it has module-info.class) or an automatic module (if it declares automaticModuleName). The topological build order guarantees a member's JAR exists before its consumers compile.

Maven parity

The module/classpath split algorithm mirrors plexus-languages (used internally by maven-compiler-plugin 3.13+), which also auto-detects module-info.java and performs the same split. The synced pom.xml therefore builds identically to curie build without any extra configuration.

For the test phase, the generated POM pins Surefire to <useModulePath>false</useModulePath> to match Curie's classpath test mode (phase 1). Phase 2 (module-path testing) will update this automatically.