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:
- Maven-style layout:
src/main/java/module-info.java - Flat-package layout:
src/module-info.java
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:
- Explicit module — JAR contains
module-info.classat its root. The module name comes from the binary class file. - Automatic module (manifest) — JAR has
Automatic-Module-NameinMETA-INF/MANIFEST.MF. Stable name provided by the library author. - 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:
- All existing test tooling works unchanged (JUnit, Mockito, JaCoCo, Spock).
- Whitebox tests that access package-private members work without extra flags.
- The module graph is not validated at test time.
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.