Building & running
The default build target compiles, tests, and jars every module. You saw it run in
Getting started, and Core concepts explained the step
graph underneath. This chapter is about the everyday loop that graph drives. It covers what each of those
phases actually does, where your tests go, how to hand the compiler an extra flag or an annotation
processor, how to run a module's main, and how to keep rebuilding as you edit.
The build pipeline
For each module, the inferred build wires the same short chain of steps: compile → test → jar. Running
build (or just java build/jenesis/Project.java with no selector) walks that chain for every discovered
module in dependency order. Steps that do not depend on each other run at the same time; on a machine where
that is too much - a laptop on battery, a small CI runner - -Djenesis.executor.concurrency=<n> caps how
many run at once (0, the default, is no limit).
- Compile runs
javacover the module's sources, resolving its dependencies onto the class or module path, and writes the.classfiles. Other-language compiles (Kotlin, Scala, Groovy) slot into the same chain - see Other JVM languages. - Test compiles and runs the module's tests. Jenesis auto-detects the test framework from the test dependencies you already declare - JUnit Platform (JUnit 5/6), JUnit 4, or TestNG - and resolves the matching console runner for you, so you never add it as an explicit dependency. In the modular layouts the tests live in their own test module, built after the module under test (next section).
- Jar packages the compiled classes into the module's jar under
target/. When the module declares a main class (below), the jar's manifest gets aMain-Classentry and itsmodule-infoaModuleMainClassattribute, so the artifact is directly launchable.
build recompiles and
re-tests nothing until an input actually changes. A test step in particular re-runs only when the classes it
covers change - not on every build.
Writing tests
Where tests live depends on the layout, and in both cases it is what you would write anyway.
A pom.xml project keeps its tests under src/test/java (or the <testSourceDirectory> the POM
names), with the test framework as a normal test-scoped dependency:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
A modular project puts its tests in a separate module, in a sibling folder, because the Java Module
System does not let two modules share a package. The test module is an open module (so the framework can
reflect over the tests), requires the module under test and the framework, and carries a @jenesis.test
tag naming the module it tests:
/**
* @jenesis.test demo.greeter
*/
open module demo.greeter.test {
requires demo.greeter;
requires org.junit.jupiter;
}
The tag is what makes it a test module: it is compiled and run as part of build but never staged or
published. Its folder name is how you select it, so with the module under test in greeter/ and its tests
in greeter-test/, +greeter builds the library alone and +greeter-test builds it and runs the tests.
Skipping the tests
To compile and package without running the test suite - a fast inner loop, or a machine that only builds
artifacts - set jenesis.test.skip:
java -Djenesis.test.skip=true build/jenesis/Project.java
The bare flag (-Djenesis.test.skip) works too. Tests still compile; they just do not run.
Choosing the Java version
An @jenesis.release <N> tag on the module declaration pins the compile to a specific Java release - Jenesis
turns it into javac --release <N>, so the module compiles against exactly that platform API regardless of
the JDK running the build:
/**
* @jenesis.release 21
*/
module demo.app {
exports sample;
}
A pom.xml project sets the same thing through the maven.compiler.release property in its
<properties> block.
One jar, several Java versions
A jar can also carry different bytecode for different Java versions, and the JVM loads the copy that matches
its own version at launch. You get one from a source convention: anything under
sources/META-INF/versions/<N>/ is compiled in its own pass with --release <N>.
sources/
├── module-info.java @jenesis.release 21
├── sample/Platform.java the Java 21 baseline
└── META-INF/versions/25/sample/Platform.java the Java 25 override
The jar that comes out runs the baseline on a Java 21 runtime and the override on Java 25 - one artifact, two
implementations, selected by the JVM. Nothing else is needed: producing an overlay is what marks the jar
Multi-Release: true, the flag that tells the JVM to look in the versioned directory at all.
Source and API-documentation jars
A normal build produces just the binary jar. Two flags add the companion artifacts a repository like Maven
Central expects:
java -Djenesis.project.sources=true \
-Djenesis.project.documentation=true \
build/jenesis/Project.java
jenesis.project.sources adds a per-module -sources.jar, and jenesis.project.documentation runs the
documentation tool (javadoc for Java) and adds a -javadoc.jar. Both are off by default because they cost
build time you do not want on every inner-loop run. Turn them on for a release, or record them in a profile
(see Configuration).
Passing extra arguments to a tool
Jenesis picks sensible flags for javac and the other tools it forks, but sometimes you need one more. You
add it with a process-<command>.properties file in a configuration folder (build.jenesis/, as covered
in Configuration), with no build script required. The file is named after the tool, and each entry is a
flag with its argument:
# process-javac.properties → compile with -parameters
-parameters=
Each key is a flag and its value the flag's argument. An empty value emits a bare flag (as above); a value
with embedded newlines repeats the flag once per line. The file merges over the arguments Jenesis already
generates - so javac here receives both the build's own --release and your -parameters.
The same mechanism works for every tool the build forks: javac, kotlinc, scalac, jar, jmod, jlink,
jpackage, and native-image. Two names address the forked JVMs specifically: process-java.properties
applies to every forked java process, while process-test.properties targets only the test JVM
(merged over the java file, with test keys winning).
process-javac.properties in a more specific
folder switches an inherited flag back off. This is the profile-aware way to compile a single module with
extra javac flags.
Annotation processing
A Java annotation processor (JSR-269) is turned on with a single @jenesis.plugin tag on the module
declaration, naming the processor by module name (or <repository>/<coordinate>):
/**
* @jenesis.plugin org.immutables.value
*/
module demo.annotations {
requires static org.immutables.value;
}
Jenesis resolves the processor, places it on javac's processor path (--processor-module-path), and the
compiler runs it. The version is pinned the usual way: the pin step writes back the @jenesis.pin line for
you (pinning is covered in Pinning & bills of materials).
requires of your module, and so already on the module path -
never runs unless a @jenesis.plugin tag places it on the processor path. Delete the tag and the
processor silently stops running; the class it generates is never produced and the build fails to compile.
The same tag, with a compiler name in front (@jenesis.plugin kotlinc <coordinate>), declares a compiler
plugin for another language - covered in Other JVM languages.
Running a module's main
To run a module rather than just build it, declare its entry point and launch it with Execute.java, the
companion of Project.java in the same folder. Declaring the main class differs by layout but converges on
the same result:
-
a modular project uses a
@jenesis.maintag onmodule-info.java:/** * @jenesis.main sample.Sample */ module demo.app { exports sample; } -
a
pom.xmlproject sets a<mainClass>property instead:<properties> <mainClass>sample.Sample</mainClass> </properties>
Execute.java builds the project first, then launches the main class in a fresh java process, forwarding
any trailing arguments to your program:
java build/jenesis/Execute.java ada lovelace
Implicit vs. explicit main
If exactly one module declares a main class, Execute selects it implicitly - you pass nothing. If several
do, it stops and lists the candidates; name the one you want explicitly with two properties, which also
narrows the build to that module's subtree:
java -Djenesis.execute.module=tools \
-Djenesis.execute.mainClass=org.example.tools.Cli \
build/jenesis/Execute.java --help
jenesis.execute.module takes the same module path you would write after + in a build selector.
Execute can also run the launched program inside a container, independently of the build - see
Build performance & isolation. Running an already-published module instead of the
current project is the job of jpx.
Attaching a Java agent
Some libraries have to run as a -javaagent rather than be called through an API - a tracer that instruments
classes as they load, a mocking library that redefines them. A @jenesis.attach tag on the module declaration
adds one to the java commands that module owns: its test run, and the Execute run of its @jenesis.main.
/**
* @jenesis.main demo.agents.Application
* @jenesis.attach io.opentelemetry.javaagent/opentelemetry-javaagent
*/
module demo.agents {
exports demo.agents;
}
The token is a module name or a <groupId>/<artifactId>, and everything after it is passed to the agent
verbatim as its option string. There is no version slot: the version comes from a dependency you already
declare, from a pin, or floats to the latest without one. A pom.xml project declares the same lines in a
project-level <!--jenesis.attach ... --> comment block.
One tag covers both shapes an agent takes. The OpenTelemetry agent above is agent-only: required by
nothing, compiled against nothing, never on a compile or runtime path. It is attached, and that is all.
Mockito is the other shape, a dependency that also attaches, named by a requires and by an attach
declaration. Both resolve to the identical artifact, so the jar on the module path and the jar passed as
-javaagent: are the same file.
An attachment belongs to the module that declares it and never propagates to a dependent, so a test module attaches to its own test run:
/**
* @jenesis.test demo.agents
* @jenesis.attach org.mockito
*/
open module demo.agents.test {
requires demo.agents;
requires org.mockito;
}
Premain-Class manifest attribute - that is what makes it an
agent - and the build says so with a clear error before the launch rather than letting the JVM fail. Agents
are ordinary dependencies otherwise: they resolve, pin, and appear in the bill of materials like any other.
Watch mode
While you are editing, keep the build process alive and let it rebuild on every save. Set
jenesis.project.watch:
java -Djenesis.project.watch=true build/jenesis/Project.java
The first build runs as usual. Jenesis then watches the project root and re-runs the requested target whenever
a file changes, reusing the content-hash cache so each rebuild only re-executes the steps whose inputs actually
moved; a no-op change settles in well under a second. The output folders (target/ and the cache) and
dot-directories are excluded, so the build's own writes never trigger a rebuild. Press Ctrl+C to stop.
Module selectors still apply, so you can watch just one module's subgraph:
java -Djenesis.project.watch=true build/jenesis/Project.java +mymodule
Setting jenesis.project.watch=true in a jenesis.properties file makes watch a project's default. Watch mode
already skips a module's tests when none of its inputs changed; it can go finer and re-run only the tests a
change can reach - a development-loop optimisation covered in Code quality & testing.
src/test style, one as a separate test module;
demo-05 (a
pom.xml app with <mainClass>) and
demo-06 (a
modular app with @jenesis.main) declare an entry point, each driven by its own
build/Demo.java;
demo-08 builds a
multi-release jar with a Java 25 override of one class;
demo-09 hands
javac a -parameters flag through process-javac.properties;
demo-10 runs an annotation
processor (Immutables); and
demo-26 attaches Mockito to
its tests and the OpenTelemetry agent to its application run. Each is a runnable project - see
Demos.