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 javac over the module's sources, resolving its dependencies onto the class or module path, and writes the .class files. 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 a Main-Class entry and its module-info a ModuleMainClass attribute, so the artifact is directly launchable.
Every phase is cached the way Core concepts described: a second 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).

Because the file lives in a configuration folder, it is profile-aware and resolved by first match. A profile can add a flag for one build, and an empty 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).

Processors are run only from what you declare. A dependency that happens to bundle a processor - even one that is also a 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.main tag on module-info.java:

    /**
     * @jenesis.main sample.Sample
     */
    module demo.app {
        exports sample;
    }
    
  • a pom.xml project 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;
}
The resolved jar has to carry a 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.

demo-03 and demo-04 each carry a tested module - one in 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.