vidocq-runtime-maven-plugin is the Maven plugin that turns an application into a deployable standalone distribution. It exposes ten goals: generate, package, dev, checkpom, modularize, jlink, jpackage, docker, check-module-info, complete-module-info. Also see vidocq/JLINK.md for internal details.
Coordinates
Artefact |
|
Java module |
(none) — Maven plugins run on Maven’s class path, so the plugin intentionally has no |
Goals
| Goal | Role |
|---|---|
|
Generates the CDI bean index ( |
|
Standalone distribution: |
|
Dev/watch mode — forks a child JVM with |
|
Verifies every |
|
Gives the non-modular dependency jars a generated |
|
Standalone Java image ( |
|
Native OS bundle ( |
|
Generates |
|
Verifies the application |
|
Opt-in companion of |
|
Scanned dependencies and Java Modules — new in 0.3.0-SNAPSHOT
When |
vidocq:modularize goal NEW
A dependency jar without module-info.class is an automatic module: its name is derived from the file name (it therefore changes with the version), it reads every module, it exports every package — and jlink refuses to link it at all.
vidocq:modularize patches a copy of each such jar with a generated descriptor — requires derived by jdeps, every package exported, META-INF/services promoted to provides, uses scanned off the bytecode, open module by default — into target/vidocq-modularized/, under the original file name. vidocq:dev, vidocq:jlink and vidocq:package resolve every dependency through that directory first, so a patched copy transparently replaces the original jar on the module path, in the staged image and in the distribution’s lib/. Nothing is installed, deployed or redistributed: the copies never leave target/.
The goal has no default binding — declare an execution (its own default phase is prepare-package):
<plugin>
<groupId>io.vidocq.runtime</groupId>
<artifactId>vidocq-runtime-maven-plugin</artifactId>
<executions>
<execution>
<id>modularize</id>
<!-- process-classes: vidocq:dev rebuilds with `mvn process-classes` and would
never run a goal bound to the default prepare-package phase -->
<phase>process-classes</phase>
<goals><goal>modularize</goal></goals>
<configuration>
<mode>all-automatic</mode>
</configuration>
</execution>
</executions>
</plugin>
<mode>derived</mode> (the default) patches only the jars whose automatic name comes from the file name, leaving untouched every jar whose author already committed to an Automatic-Module-Name. <mode>all-automatic</mode> patches all of them — required before vidocq:jlink, which rejects any automatic module.
Module names
A patched jar keeps the exact name it already had as an automatic module, so the requires your module-info.java was compiled against keeps resolving. <moduleNames> overrides that name per artifactId, but such an override is a runtime-only rename: compilation still resolves against the original jar, which announces its automatic name. Only rename a jar your code does not requires by name — one reached transitively or purely through services.
What can stop the build
Split package. Two automatic jars sharing a package cannot both become named modules, and the module system would reject them side by side anyway. The goal checks the whole automatic closure up front and fails, naming the package and both jars. <excludes> does not silence it — leaving one side unpatched does not make the package any less split; remove one side from the dependency graph (Maven <exclusions>) or wait for an upstream fix.
Jars kept automatic. A jar whose own code calls ServiceLoader for a service type defined in a jar that depends on it has no legal explicit form at all: ServiceLoader checks the calling module, so the uses is demanded from the module below, which would need a requires back — and JPMS has no cycles. langchain4j-core is the textbook case: its generic ServiceHelper.loadFactories(Class) is called with service types owned by langchain4j, which already requires it. Such a jar is left automatic rather than patched into a graph that cannot resolve, and report.txt records why:
kept automatic: langchain4j-core-1.17.1.jar — ServiceLoader of dev.langchain4j.http.client.HttpClientBuilderFactory
from langchain4j.core cannot be declared (module cycle); jlink will reject it, dev mode works
Automatic modules work on the module path, so vidocq:dev and a plain --module-path run are unaffected; only jlink refuses them, and its failure message quotes that very reason back. Re-running the goal cannot fix it: the ways out are upstream (the library shipping a hand-written module-info, or each caller doing its own ServiceLoader.load) — or, in a future version of the goal, merging the mutually-dependent jars into a single module. vidocq.modularize.forceExplicit=true patches the jar anyway (the illegal directives stay dropped) when you know the failing lookup is one your application never reaches.
Parameters
| Parameter (user property) | Default | Effect |
|---|---|---|
|
|
|
|
empty |
Restrict patching to / away from these |
|
empty |
|
|
|
Generate |
|
|
JDK release |
|
empty |
Opt-in licence gate: when set, every artifact that ends up patched must declare one of these licences (names are normalised), otherwise the build fails. |
|
|
Patch a jar even when one of its own |
|
|
Skip the goal entirely. |
report.txt
Every run writes target/vidocq-modularized/report.txt: per patched jar, the module name, why it was selected and the full generated module-info source — plus one line per jar left as is. It is the thing to read when a requires looks wrong or a service is not picked up. The output directory is emptied at the start of each run, so the report always describes the jars sitting next to it.
See vidocq/MODULE_INFO.md § vidocq:modularize for the full reference: the uses scanning rules, the warnings raised for the directives that had to be dropped, and the planned module-merge exit for the cycles.
vidocq:jlink goal
<plugin>
<groupId>io.vidocq.runtime</groupId>
<artifactId>vidocq-runtime-maven-plugin</artifactId>
<executions>
<execution>
<id>jlink</id>
<goals><goal>jlink</goal></goals>
<configuration>
<mainModule>com.example.myapp</mainModule>
<mainClass>com.example.myapp.MainApp</mainClass>
<launcher>my-app</launcher>
<distDir>${project.build.directory}/dist</distDir>
<stripDebug>true</stripDebug>
<compress>zip-6</compress>
<includeResources>
<param>vidocq.properties</param>
<param>logging.properties</param>
</includeResources>
</configuration>
</execution>
</executions>
</plugin>
Typical output:
target/
├── dist/
│ ├── bin/my-app # binary launcher
│ ├── conf/ # overridable config (ExternalFileConfigSource)
│ │ ├── vidocq.properties
│ │ └── logging.properties
│ ├── lib/ # app + JDK Java modules
│ ├── legal/
│ └── release
├── installer/ # if jpackage is enabled
│ └── my-app.app/
└── Dockerfile # if docker is enabled
vidocq:jpackage goal
Reuses the jlink image. Default type: app-image (cross-platform). On macOS, .app. On Windows, .exe or .msi. On Linux, .deb or .rpm.
See vidocq/JLINK.md §3 for the exhaustive parameter list.
vidocq:docker goal
Generates a distroless Dockerfile. Why distroless/base-debian12:nonroot?
-
No shell, no package manager — minimal attack surface.
-
Enforced
nonrootuser (UID 65532). -
Compatible with the strictest image policies.
./mvnw -ntp package -Dvidocq.docker.build=true
docker run --rm -p 8080:8080 example/my-app:1.0.0
# With external config
docker run --rm -p 8080:8080 \
-e VIDOCQ_CONFIG_DIR=/etc/myapp \
-v $(pwd)/conf:/etc/myapp:ro \
example/my-app:1.0.0
External override (ExternalFileConfigSource)
vidocq.properties placed in ${java.home}/conf/ (jlink image) or pointed to by $VIDOCQ_CONFIG_DIR is read with ordinal 250. See Reference for the full hierarchy.
Prerequisites and limitations
-
Strict Java Modules — every dependency must be a named module;
jlinkrejects every automatic module. Give the non-modular jars a descriptor withvidocq:modularizeinall-automaticmode. -
Records serialised over REST — Champollion handles them via APT, not reflection. If an external dependency exposes records that are not generated, declare a JSON-B
Converter<T>. -
Custom
java.util.loggingHandler — declare the module inlogging.propertiessojlinkembeds it (handlers=com.example.MyHandler). -
macOS app-image — the bundle requires a valid
--app-version; the plugin defaults to the project Maven version.
See vidocq/JLINK.md §6 and §8 for the full list of known limitations.