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 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 → jar → test. Running
build (or just java build/ 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. caps how
many run at once (0, the default, is no limit). -Djenesis. counts every kind of step;
when it is the tools themselves that are heavy, -Djenesis. caps how many javac,
jar, javadoc and the like run at once, underneath whatever step limit is in force.
- Compile runs
javacover the module's sources, resolving its dependencies onto the class or module path, and writes the.files. Other-language compiles (Kotlin, Scala, Groovy) slot into the same chain - see Other JVM languages.class - 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. - 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 and later), JUnit 4, or TestNG - and resolves the
matching console runner for you, so you never add it as an explicit dependency. A module can also state its
engine: a
test.in its configuration folder holdingproperties engine=,junit-platform junit4ortestngdecides it, the same in every checkout and every CI run. In the modular layouts the tests live in their own test module, built after the module under test (next section).
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.
Reading a build's outcome
Besides the progress lines, every build writes what happened to target/, one JSON
object per line, and the next build replaces it. The second progress line, [EVENTS], names the file, so a
script or a coding agent reads it instead of parsing the console:
{"status":"started","target":"/.../demo-01-java-pom/target","directory":"/.../demo-01-java-pom","run":"20261005T180707.205894Z"}
{"status":"resolved","module":"build","seconds":0.067}
{"status":"skipped","step":"build/maven/identifier/prepare","folder":"/.../target/build/maven/identifier/prepare","run":"20261005T180659.583809Z"}
{"status":"completed","seconds":0.342,"executed":1,"skipped":18,"failed":0}
Every object leads with its status:
status |
Written for | Beside it |
|---|---|---|
started |
the build, first line | target, the directory the build ran in, and the run |
executed |
a step that ran | step, seconds and folder, which holds the step's output/ |
skipped |
a step none of whose inputs changed | step, folder, and the earlier run that produced the folder |
loaded, stored |
a step's output fetched from or stored in the build cache | step and seconds |
resolved |
a module | module and seconds |
failed |
a step or a module that failed | step or module, the error class and its message; a failed step also its folder |
completed, failed |
the build, last line | seconds and how many steps executed, skipped and failed; a failed build adds its error and message |
Every build is a run, named on the started line by the moment it began, in UTC. A step that executed is this
run's. A skipped step names the earlier run whose output it kept, so a test report in its folder is that run's
result, not this one's. The same name sits in a local. beside the step's output/, with
cached= when the output came from the build cache:
run=20261005T180659.583809Z
cached=false
That file stays on the machine that wrote it: no checksum covers it and no build cache carries it.
A file without that last line belongs to a build that is still running or was killed.
-Djenesis. writes no file and leaves one that an earlier build wrote in place.
A failed step keeps what it wrote. Its folder ends in ~ and holds the step's output/ and, for a tool the
build ran, the command, the tool's output and its reports under supplement/, beside an empty
. marker. The folder stays until the step comes up again in a later build. A failure that says
to execute a command names paths relative to the directory of the started line.
The progress lines, the dependency tree and every other line the build prints are coloured with ANSI escape
sequences. To read them as plain text, from a file or a pipe, pass -Djenesis..
Writing tests
Where tests live depends on the layout, and in both cases it is what you would write anyway.
A pom. project keeps its tests under src/ (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, which reads the module under test as any
other module does. When a test needs more than the exported API, the Java Module System already has the
means: the module under test exports a package to the test module by name, with
exports sample.. That grants access one package at a time, to one
named module. Placing tests in the same package instead would reach package-private members, which is the
kind of access modules were made to prevent.
The test module requires the module under test and the framework, and carries a @jenesis. tag
naming the module it tests. It need not be open: when the tests run, each of its packages is opened to the
framework modules that reflect over them (to the unnamed module when the framework sits on the class path),
so the descriptor declares only what the tests use:
/**
* @jenesis.test demo.greeter
*/
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.
A module that only supplies infrastructure to the tests - fixtures, fakes, shared assertions - carries the
same tag with the value abstract instead of a module name:
/**
* @jenesis.test abstract
*/
module demo.greeter.testing {
requires demo.greeter;
exports greetertesting;
}
Such a module is compiled and put on the module path of the test modules that requires it, but no test run
is wired for it, and a plain stage leaves it out. Under jenesis., which publishes the test
modules beside the modules they test, it is staged as well, as a module of its own, because the published test
modules require it. It can be tested like any other module: a module tagged @jenesis.
is run as the other test modules are, and staged as the test variant of demo.. Since abstract
is a Java keyword it can never be a module name, so the two forms of the tag never collide.
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.:
java -Djenesis.test.skip=true build/jenesis/Make.java
The bare flag (-Djenesis.) works too. Tests still compile; they just do not run.
Choosing the Java version
An @jenesis. 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. project sets the same thing through the maven. property in its
<properties> block.
Without either, the module compiles for the release of the JDK running the build: --release 25 on any JDK
25, and Kotlin and Scala sources target the same release. That keeps the output the same across updates and
vendors of one JDK. Which JDK runs the build can be named as well, as described under
The JDK a build runs on.
Preview features
A module that uses a preview feature of Java - a language feature or an API a JDK ships for trying out -
declares its release with a -preview suffix:
/**
* @jenesis.release 25-preview
*/
module demo.preview {
exports sample;
}
A pom. project sets maven. to true beside maven., the
property the Maven compiler reads as well; without a release, it enables the preview features of the JDK
running the build.
The module compiles with javac --release 25 --enable-preview, and its jar records the release in its
manifest as Jenesis-Preview: 25. Every run of it then enables the preview features without being asked:
its tests, Execute, a bundle, a jpackage image, and a program jpx runs from the jar alone. A runtime
image that jlink links is given the option for good, so its own java runs the module as it stands, and
javadoc documents the sources with the same features enabled. An executable jar is the exception: java -jar
opens the jar only after the JVM started, so it runs as java --enable-preview -jar <jar>.
Preview features belong to one Java version, so a module that uses them compiles only on that JDK. A build on
another fails and names the one to select, -Djenesis. (see
The JDK a build runs on). A module compiled against one that uses preview features
enables them itself, so its test module declares 25-preview too; without it, the build stops before
compiling and names the release to declare. A class that uses a preview feature runs only on the Java version
it was compiled for, so a library built this way binds its users to that JDK until the feature is final.
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/ 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/Make.java
jenesis. adds a per-module -sources., and jenesis. runs the
documentation tool (javadoc for Java) and adds a -javadoc.. 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).
Reproducible archives
Every jar, jmod and zip comes out the same from the same inputs: a fixed entry order, no Unix permissions,
and one date on every entry - 1980-02-01 00:00 UTC, a month past the earliest a zip can record so no time
zone reads it as 1979. jenesis. names another, as an ISO-8601 date-time with an offset:
java -Djenesis.archive.timestamp=$(git log -1 --format=%cI) build/jenesis/Make.java
It must lie between 1980-01-01T00:00:02Z and 2099-12-31T23:59:59Z, the range an entry records
independently of the building machine's time zone. An empty value turns the fixed time off, and then every
build produces different archives and no release can be checked against its sources - it exists for a tool
that reads entry times and cannot be told otherwise.
What the build writes is fixed; what it copies stays yours. A resource goes into the jar byte for byte, and
so does a source file into the sources jar, so a file checked out with Windows line endings makes a
different archive than the same file checked out on Linux. Git on Windows commonly converts text files to
Windows line endings on checkout (core.). A . file at the root of the repository
fixes the line endings of every file Git treats as text, whatever machine checks it out:
* text=auto eol=lf
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>. file in a configuration folder (build., 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, and report up to 500 warnings
-parameters
-Xmaxwarns=500
Each key is a flag and its value the flag's argument, so the second line passes -Xmaxwarns 500. A key with
no value emits a bare flag, as the first line does; 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 two flags.
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.
applies to every forked java process, the program Execute runs included, while
process-test. 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.
Handing a program environment variables
A test run, or any other program the build forks, sees only the platform's own environment variables:
PATH, HOME, LANG, LC_ and TMPDIR, and on Windows SystemRoot, TEMP, USERPROFILE and the like.
Nothing else of your shell reaches it. A variable is no input of the build, so a test result never depends on
one the build cannot see, and no tool reads a secret it was not handed.
To hand a program more, add an environment-<command>. file to a configuration folder, named
like a process-<command>.:
# environment-test.properties → the test run gets GREETING, and TOKEN from your shell
GREETING=Hello
TOKEN
NAME= sets a variable to the value written. A name with no value passes on your shell's own value when
the program runs, and leaves the variable unset when your shell has none. The file is an input of the program,
so editing a value runs it again. A value taken from your shell is not part of that input, which suits a
credential or a proxy but not a value a result depends on.
Four names take such a file:
| File | Reaches |
|---|---|
environment-java. |
every JVM the build forks, the test run included |
environment-test. |
the test run only, merged over the java file |
environment-pitest. |
PIT's mutation run |
environment-native-image. |
native-image |
A JDK tool such as javac or javadoc takes none, as it may run inside the build's own JVM, and a file naming
one fails the build. native-image is handed the variables its C compiler reads without a file: INCLUDE,
LIB and LIBPATH from a Visual Studio developer prompt, CPATH, C_, LIBRARY_ and
SDKROOT. A release run by the build hands JReleaser every JRELEASER_ variable, where it reads its
credentials. The program Execute. or jpx runs is the one you asked for rather than a step of the
build, so it is handed your whole environment, as from a shell.
Values from the command line
In a process-<command>. or an environment-<command>., a value @<key> stands for the
setting jenesis., and @<key>/ falls back to what follows the slash when the setting
is absent. @@ writes a literal @. A missing setting without a default fails the build and names the
setting:
# environment-test.properties
GREETING=@greeting/Hello
java -Djenesis.variable.greeting=Hi build/jenesis/Make.java
The setting comes from the command line, a jenesis. or a profile, like any other. The resolved
value is part of the input of every program it reaches, so another value runs the program again. It is also
written into the build's output under target/, so a credential stays a name without a value.
Annotation processing
A Java annotation processor (JSR-269) is turned on with a single @jenesis. tag on the module
declaration, naming the processor by module name (or <repository>/):
/**
* @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.
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.), 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., the
companion of Make. in the same folder. Declaring the main class differs by layout but converges on
the same result:
-
a modular project uses a
@jenesis.tag onmain module-info.:java /** * @jenesis.main sample.Sample */ module demo.app { exports sample; } -
a
pom.project sets axml <mainClass>property instead:<properties> <mainClass>sample.Sample</mainClass> </properties>
Execute. 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
Options for the program's JVM
A leading -J hands an option to the JVM that runs your program, as the JDK's own tools read it, written as
java takes it:
java build/jenesis/Execute.java -J-Xmx512m -J-Xlog:gc ada lovelace
The program's JVM receives -Xmx512m and -Xlog:gc, and the program ada lovelace. The first argument not
starting with -J begins the program's own arguments, so a later -J… reaches the program unchanged. A -J
option follows what a process-java. gives the same JVM, so it wins where both set an option: the file
is where a project keeps what every run needs, the command line what one run needs. Options on the java command
before build/ configure only the JVM that builds. jpx reads -J the same way, and
the jenesis-exec tool refuses it, as the JDK's own tools do (see
Running a build inside another program).
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.main=org.example.tools.Cli \
build/jenesis/Execute.java --help
jenesis. 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. tag on the module declaration
adds one to the java commands that module owns: its test run, and the Execute run of its @jenesis..
/**
* @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>/, and everything after it is passed to the agent as
its option string. There is no version slot: the version comes from a dependency you declare, from a pin, or
floats to the latest. A pom. project declares the same lines in a project-level
<!--jenesis. block.
One tag covers both shapes. The OpenTelemetry agent above is agent-only - required by nothing, on no
compile or runtime path. Mockito is the other shape, a dependency that also attaches, named by a
requires and an attach declaration; both resolve to 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
*/
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.
Granting native access
Code that calls native functions or reads native memory - through the foreign function and memory API, JNI,
or a library built on either - uses restricted methods, which the JDK allows only for a module the launch
names with --enable-native-access. Without it the JDK prints a warning today, and a future release refuses
the call. A @jenesis. tag declares that grant, and the build adds it to every launch the module owns:
its test run, its Execute run, and what it packages - a bundle, a Docker image, a jpackage application and
an executable jar.
Whether native code runs is decided by what a program uses, not by the library that offers it: a library can offer a native API that most of its users never call, so it declares nothing. The module that uses the API names the module that needs access, and a module that calls restricted methods itself names itself:
/**
* @jenesis.native demo.natives.text
*/
module demo.natives.words {
requires transitive demo.natives.text;
}
A declaration grants access only to the runs of the module that makes it, and is never inherited. Its names
are recorded in the module's jar as the manifest attribute Jenesis-Native-Access: demo., so a
module that runs this one can learn what it has to grant, and grants it the same way:
/**
* @jenesis.main demo.natives.app.Application
* @jenesis.native demo.natives.text
*/
module demo.natives.app {
requires demo.natives.words;
}
A test module is a run of its own and grants what its tests need itself. A token is a module name or a
<groupId>/, and several may share one tag. A grant adds no dependency: what it names has to be
on the module's run-time path, or the build fails saying so. A jar on the
class path has no name, so a grant for one becomes --enable-native-access=. jpx grants what
the jar it runs names.
A library that keeps a module in a layer hides that module
from whoever uses the library, and its need for native access with it. The library passes its own native
access on to the module with a native line beside the layer's declaration. The launcher grants it when it
defines the layer, through the lookup of the library that asks for it, so the line also records that the
library needs native access itself. What a granted module passes on to its own layers is granted with it, so
the application grants the library alone - as it would a library that had shaded the module. A module in a
layer is out of the application's reach, so naming it in @jenesis. fails the build like any name the
run does not resolve:
/**
* @jenesis.layer strings api demo.strings.spi
* @jenesis.layer strings provider demo.strings.text
* @jenesis.layer strings native demo.strings.text
*/
module demo.strings.library {
requires build.jenesis.launcher;
requires demo.strings.spi;
}
/**
* @jenesis.main demo.strings.app.Application
* @jenesis.native demo.strings.library
*/
module demo.strings.app {
requires demo.strings.library;
}
A pom. project declares the same in a project-level comment block, naming itself by its own
<groupId>/:
<!--jenesis.native
org.example/jni
-->
jenesis. decides what the build does with the names a dependency's jar records: ignore,
the default, does nothing; warn reports each one the running module does not grant; strict fails the
build on it:
java -Djenesis.dependency.native=warn build/jenesis/Make.java
Watch mode
While you are editing, keep the build process alive and let it rebuild on every save. Set
jenesis.:
java -Djenesis.project.watch=true build/jenesis/Make.java
The first build runs as usual; Jenesis then watches the project root and re-runs the target whenever a file changes, reusing the content-hash cache so only the steps whose inputs moved run again - a no-op change settles in well under a second. The output folders 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/Make.java +mymodule
Setting jenesis. in a jenesis. 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.
Opening the project in an IDE
The ide selector writes project files for IntelliJ IDEA, VS Code and Eclipse from what the build resolved, so an
editor sees exactly the modules, sources and jars the build uses:
java build/jenesis/Make.java ide
ide/, ide/ or ide/ writes the files of one editor only:
| Editor | Files |
|---|---|
| IntelliJ IDEA | an . beside each module, plus . and . |
| Eclipse | a . and a . in each module |
| VS Code | . |
The files are generated, so keep them out of version control. Each editor compiles into a folder of its own -
target/, target/, and a . folder in each module - never into the build's output. A module
of the project is linked as a module rather than as its jar, so navigation crosses from one module into another.
The Java version comes from @jenesis. (see Choosing the Java version), not
from the JDK that ran the build, so the editor refuses what javac would refuse. Which JDK provides that version is
your editor's choice: the first run names IntelliJ IDEA's project JDK after the bare version, and a later run keeps
whatever name you picked there.
A module's classpath holds what the module declares. The tools the build resolves for itself - Checkstyle, PMD,
SpotBugs, a formatter - stay out of it. A test module is marked as test sources, and an @jenesis.
module as ordinary sources, since other modules compile against it.
Run ide again after changing a dependency or adding a module: the files name the resolved jars by their path, so
a stale file points at a jar the build no longer produces.
The JDK a build runs on
A build runs on the JDK that started it, unless the project names one. jenesis. does, in
jenesis. or with -D:
jenesis.toolchain.version=25-temurin
Make. and Execute. check the JVM they were started on first. When it matches, the build runs as
always. When it does not, they look for a matching JDK among those already installed and start again on it,
with the same selectors and the -Djenesis. properties of the command line, and print a line naming the
JDK they chose. Nothing is downloaded or installed: when no JDK matches, the build fails and lists what it
found. Other JVM options, such as -Xmx, reach the new JVM through the JDK_ environment
variable, which every java launcher reads. A build started from an entry point of your own runs on the JVM
that started it, unless that entry point asks (see Extending the build).
Moving to another JDK compiles again only what depends on it: a module that declares no release now compiles for the new JDK's release, while a module that declares one keeps its classes.
Naming a version
A version is <feature>[.:
- The numbers match as a prefix, a missing number counting as zero:
25matches every JDK 25, and25.that update and its patches. Jenesis runs on Java 25 or newer, so a lower version is refused; to compile for an older Java, declare the release as described under Choosing the Java version.0. 3 - Every word must be one the JDK answers to: a word of the vendor or the vendor version its
releasefile records, or of the pre-release and optional parts of its version. Temurin answers toeclipse,adoptiumandtemurin, Azul Zulu toazulandzulu, GraalVM Community Edition tograalvmandcommunity, and a long-term-support build tolts. There is no list of vendors, so a JDK built in-house answers to its own name. - A pre-release matches only when the version names its word:
26-easelects an early-access build of 26, and26never does.
Among several matching JDKs the newest wins. A JDK is identified by reading its release file, so none is
run before one is chosen, and the error for a version nothing matches lists every JDK found with the words
it answers to.
Where Jenesis looks
jenesis. is a comma-separated list of folders. An entry is absolute or starts with ~,
and * stands for any one folder name. The default, @, stands for the usual locations of the operating
system, and combines with entries of your own, as in @,/:
| Operating system | Searched for @ |
|---|---|
| Linux | /, ~/, ~/, ~/ |
| macOS | /, ~/, /, ~/, ~/ |
| Windows | C:\Program Files\<vendor>\* for Eclipse Adoptium, Java, Microsoft, Zulu, Amazon Corretto and BellSoft, ~\., ~\scoop\apps\*\current |
An empty search path searches nothing, so the build only checks the JDK it was started on - the setting for a CI job that sets up its own JDK:
java -Djenesis.toolchain.searchpath= build/jenesis/Make.java
Installing a missing JDK
Jenesis installs no JDK by itself. It runs an installer you name when no JDK on the search path matches, in
jenesis. or, where that is not set, in the JENESIS_ environment
variable. The Jenesis command-line install ships one, jenesis-jdk.
Where SDKMAN, mise or Scoop installed Jenesis, there is nothing to turn on: the jenesis command names its
own jenesis-jdk in JENESIS_ for the run it starts, unless the variable is set already,
and jenesis-jdk installs the JDK with that tool in turn. Nothing is written to your files to arrange it, and
the variable ends with the run. A build started from the sources names the installer on its own command line:
java -Djenesis.toolchain.version=25-zulu -Djenesis.toolchain.installer=jenesis-jdk build/jenesis/Make.java
The setting wins over the environment variable, and an empty one, -Djenesis., runs no
installer at all. jenesis-jdk turns the version into a request for the tool that installed Jenesis:
| Tool | What it installs |
|---|---|
| SDKMAN | The newest Java identifier matching every number of the version, leaving out JavaFX and CRaC builds. ea selects an early-access build from jdk.java.net. |
| mise | The newest build of the vendor matching the numbers, as mise resolves it. mise names Zulu builds by Zulu's own version, so for Zulu it installs the newest build of the feature release. |
| Scoop, on Windows | The package of the vendor and feature release from Scoop's java bucket, in its newest build. |
jenesis-jdk calls back the tool Jenesis itself was installed with, and picks none of its own:
--tool= or --tool= names one when you run it yourself.
A word of the version names the vendor - temurin, zulu, corretto, liberica, microsoft,
sapmachine, semeru, graalvm, oracle or jetbrains - and Temurin is installed when none does. Scoop
offers Temurin, Zulu, Corretto, Liberica and GraalVM only.
Any other program works as well. Jenesis calls it with its own arguments followed by the version, in your
home folder rather than the project, and treats a non-zero exit as a failed build. The program has to install
into a folder on the search path: Jenesis searches once more afterwards, and checks what it finds like any
other JDK. A name is looked up in the folders of PATH that are absolute, and a path has to be absolute or
start with ~; a value that names an existing file is that program as a whole, spaces included.
What a project cannot set
The search path and the installer decide which programs the build runs, so they are yours to set, not the
project's. Both are accepted on the command line and in your own ~/ and its
profiles, and refused in the project's jenesis. and in the project's profiles. A project names
the version it needs, and so chooses among the JDKs you installed or your installer provides, but it cannot
point the build at a program of its own.
Before a JDK it found runs, Jenesis checks on Linux and macOS that every file in it belongs to you or to root
and that no other user can write to it; a group named after the file's owner, the private group many Linux
systems give each user, may. A JDK that fails the check is refused with the file named, not exchanged for
another match. GitHub's hosted Linux runners install the JDKs of actions/ writable by every user,
so a job that searches for one restricts it first, with chmod -R go-w on its folder. Windows has no such
check, so there the search relies on the protection of C:\Program Files and of your user profile.