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

io.vidocq.runtime:vidocq-runtime-maven-plugin:0.3.0

Java module

(none) — Maven plugins run on Maven’s class path, so the plugin intentionally has no module-info.java.

Goals

Goal Role

vidocq:generate

Generates the CDI bean index (META-INF/vauban-beans.list) and pre-generates proxies/interceptors at compile time. Extension discovery itself needs no index — it is plain ServiceLoader over provides directives.

vidocq:package

Standalone distribution: bin/run.sh + bin/run.cmd launchers and lib/*.jar (application + module-path closure), attached as a -dist.zip artifact.

vidocq:dev

Dev/watch mode — forks a child JVM with -Dvidocq.profile=dev, recompiles and hot-reloads on change.

vidocq:checkpom

Verifies every vidocq-runtime-<name>-extension declared in the pom is correctly wired (annotation processor paths, codegen bundles). Bound to validate via pluginManagement.

vidocq:modularize

Gives the non-modular dependency jars a generated module-info in target/vidocq-modularized/, picked up automatically by dev, jlink and package.

vidocq:jlink

Standalone Java image (target/dist/) with bin/<launcher> binary — no java required on target.

vidocq:jpackage

Native OS bundle (.app macOS, .exe/.msi Windows, .deb/.rpm Linux) or cross-platform app-image.

vidocq:docker

Generates target/Dockerfile distroless based on distroless/base-debian12:nonroot. Optional build via vidocq.docker.build=true.

vidocq:check-module-info

Verifies the application module-info.java declares the JPMS directives its Vidocq extensions need (e.g. opens db.migration for Flyway migrations).

vidocq:complete-module-info

Opt-in companion of check-module-info — rewrites src/main/java/module-info.java to add the missing directives.

Scanned dependencies and Java Modules — new in 0.3.0-SNAPSHOT

When vidocq:generate runs with <scanDependencies>, the classes it produces for a scanned dependency belong to that dependency’s packages. The plugin now keeps the module path split-package-free end to end: generate parks those classes in target/vidocq-patches/<artifactId>/, package and jlink ship enriched copies of the dependency jars carrying their own generated classes, and dev adds the matching --patch-module options to the child JVM. Zero configuration. See Usage — Multi-module applications.

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

<mode> (vidocq.modularize.mode)

derived

derived or all-automatic — see above.

<includes> / <excludes>

empty

Restrict patching to / away from these artifactId s. Both only ever restrict.

<moduleNames>

empty

artifactId → module name overrides. Runtime-only rename, see above.

<openModules> (vidocq.modularize.open)

true

Generate open module descriptors, so a reflective library keeps working. false generates a closed module that exports every package.

<release>

${maven.compiler.release} (else 25)

JDK release jdeps analyses multi-release jars against.

<allowedLicenses>

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.

<forceExplicit> (vidocq.modularize.forceExplicit)

false

Patch a jar even when one of its own ServiceLoader lookups cannot be declared legally.

<skip> (vidocq.modularize.skip)

false

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.

<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 nonroot user (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; jlink rejects every automatic module. Give the non-modular jars a descriptor with vidocq:modularize in all-automatic mode.

  • 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.logging Handler — declare the module in logging.properties so jlink embeds 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.

Next steps

  • Usage — packaging workflows

  • Reference — configuration keys

  • vidocq/JLINK.md — internal packaging documentation