# build-buddy **Repository Path**: mirrors_raphw/build-buddy ## Basic Information - **Project Name**: build-buddy - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-12-29 - **Last Updated**: 2026-07-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README Jenesis ======= Getting started --------------- Jenesis is a build tool for Java projects, written and configured in Java itself. It builds **modular projects** out of the box (anything whose modules declare themselves via `module-info.java`) and also understands the declarative slices of a `pom.xml` - descriptive metadata, plugin-free dependency lists, parent coordinates - so a Maven-shaped project that does not lean on plugin lifecycles can be built without conversion. Pointed at a project root containing a `module-info.java`, a `pom.xml`, or both, Jenesis discovers the multi-project graph automatically and wires the matching compile, package, and (where sources are present) test pipeline. One design goal is to ship the build *with* the project as plain Java source, and not as a binary. The Jenesis sources sit inside your repository under `build/jenesis/`, the launcher is the JVM's single-file mode (`java build/jenesis/Project.java`), and the build is reproducible from a clone plus a JDK. There is no opaque wrapper, no fetched plugin tree, no fetched daemon - which closes the supply-chain surface that wrappers and plugin resolvers otherwise expose. Shipping the build as plain source also keeps it fully modifiable where needed: humans (and AI agents) can adjust how a project is built by implementing build steps in ordinary Java, without a large API to learn first. A second design goal is that the build is naturally incremental at every step and naturally produces reproducible outputs. Each build step's inputs, outputs, and configuration are content-hashed, so unchanged work is reused unchanged from the previous run, and identical inputs always reproduce identical outputs. That same posture is what makes Jenesis strongly security-focused: dependencies can be pinned not only by version number but also by the checksum of every downloaded artifact, so a build is naturally resistant to supply-chain attacks on its inputs. Pinning at that level of detail is itself a consequence of embedding the build tool inside the project, since the pin set lives in the same committed sources as everything else. The combination of plain-Java sources and content-hashed steps also lays a foundation for optimising complex builds: a non-trivial custom build can itself be compiled ahead of time, or shipped as a native image for environments that run the same build at high frequency (a CI server, for example), and step outputs - being pure functions of their content-hashed inputs - are easy to share between builds as a cache. A Jenesis build requires a JVM of version 25 or newer, but nothing else. For runnable, self-contained examples of everything below - Maven and modular layouts, multi-module projects, mixed Java/Kotlin, Java/Scala, and Java/Groovy sources, and loading build modules - see the [`demo/`](demo/) directory. ### Installing Three equivalent ways to populate `build/jenesis/` inside your project. All three land at the same on-disk state, so the canonical `java build/jenesis/Project.java` invocation works identically afterwards: **curl-piped bootstrap.** Fastest, no prerequisites beyond a JDK and `curl`. Run from your project root: curl -fsSL https://get.jenesis.build | bash java build/jenesis/Project.java Set `JENESIS_VERSION=X.Y.Z` to pin a specific release, or pass a git ref as an argument to install an arbitrary tag, commit, or branch (`curl -fsSL https://get.jenesis.build | bash -s -- main`); for a non-release ref the bootstrap sources are taken straight from the repository tree at that ref. The script is `install.sh` at the repository root. **Git submodule.** Most explicit; the pinned submodule commit is the reproducibility anchor, so a fresh clone plus `git submodule update --init` is the entire setup with no separate install step: git submodule add https://github.com/raphw/jenesis.git .jenesis ln -s ../.jenesis/sources/build/jenesis build/jenesis java build/jenesis/Project.java On platforms without symlink support, replace the `ln -s` with `cp -r .jenesis/sources/build/jenesis build/jenesis` and refresh after each submodule update. **SDKMAN.** Best fit when you would rather manage versions globally instead of vendoring sources per project. Install once, then initialise each consuming project from the SDK: sdk install jenesis jenesis-init # from your project root java build/jenesis/Project.java # or just 'jenesis', equivalent `jenesis-init` populates `build/jenesis/` from the installed SDK. The companion scripts `jenesis-validate`, `jenesis-version`, and `jenesis-switch` are documented under [Using Jenesis as a CLI](#using-jenesis-as-a-cli). You can also skip the embedding entirely and run `jenesis` directly from a project root: the SDK's own copy of `Project.main(...)` is invoked against the current directory, with no `build/jenesis/` written. Customisation is then limited to system properties (`-Djenesis.project.layout=...` and friends); custom builders and hand-wired `.java` files under `build/` are not reachable. Useful for quick trials and for building projects with an untrusted build source, where Jenesis itself stays the trusted, SDK-installed copy. ### Example: Building Jenesis itself A clone of this repository is the easiest working example. Sources live under `sources/` and tests under `tests/`, with a `module-info.java` in each (`build.jenesis` and `build.jenesis.test`) and a single root `pom.xml` that points at both directories. The same canonical invocation builds it: git clone https://github.com/raphw/jenesis.git cd jenesis java build/jenesis/Project.java The auto-detected layout is `MAVEN`, since the root `pom.xml` takes precedence over a nested module-info. The build compiles main and test sources, runs the tests, and writes artifacts under `target/`. Try `java build/jenesis/Project.java stage` to materialise the release tree pushed to Maven Central, or browse `metadata.properties` and the module-info javadoc to see how descriptive metadata flows into the emitted POM. ### Customizing the build Customisation comes in three stages, picked by how far from the auto-wired pipeline you need to go. **1. System properties on the canonical launcher.** When the project shape is fine but a knob needs flipping - skip tests, force a layout, route `target/` elsewhere - pass `-Djenesis.project.*` flags. No Java code, no separate entry point: java -Djenesis.test.skip=true \ -Djenesis.project.layout=MODULAR_TO_MAVEN \ build/jenesis/Project.java `Project.main(...)` constructs the project with `new Project()`, whose default constructor reads `jenesis.project.*` (and the other `jenesis.*` properties) as its starting defaults, so they apply unless an in-code wither overrides them. The full list is in [Configuration](#configuration). `jenesis.project.root` also lets you target a project that lives outside the directory holding `build/jenesis/`: java -Djenesis.project.root=/path/to/other/project build/jenesis/Project.java Any of these `-Djenesis.*` / `-Dbuild.jenesis.*` properties can also live in a `jenesis.properties` file at the project root - the build's **profiles** mechanism. The `main` launcher (both `Project` and `Execute`) loads it (the static `Project.loadJenesisProperties(Path)`) into the system properties *before the project is constructed*, so a project carries its own defaults without a wrapper script, and the file drives everything alike - layout, target, pinning, and every later run-time decision. The file is optional, and an explicit `-D` on the command line always wins over any file entry. `jenesis.project.properties` selects further profiles: a comma-separated list of profile names, each resolved to a `jenesis-.properties` file (a `.properties` suffix on the name is ignored) relative to the declaring file's folder and loaded the same way. Profiles compose by *chaining* - any loaded file may set `jenesis.project.properties` itself to pull in more, transitively, until everything is loaded - so a `release` profile can switch on source jars and an SBOM in one selection. Because a profile now designates both a `jenesis-.properties` file and a `/` configuration folder (see below), its properties file is optional: a missing one is simply skipped, so a profile may contribute only a configuration folder. Configuration layers in four tiers, highest precedence first: explicit `-D` command-line properties, then the selected **profiles**, then the project `jenesis.properties`, then a user-global `jenesis.properties` read from `$HOME/.jenesis/` (the weakest layer, shared across every project, optional and ignored when absent). So selecting a profile overrides the project's base `jenesis.properties`, which in turn overrides the user-global file. The `jenesis.project.global` property relocates the user-global folder (default `$HOME`) or, set to an empty string, disables it. See the [`profiles`](demo/demo-15-profiles/README.md) demo and *Configuration*. A selected profile also designates a **configuration subfolder**. The same profile names are held on `Project` as a `SequencedSet profiles` (defaulting to the selected names, settable in code via the `profiles(Path...)` wither), and for each profile `` a `/` subfolder under each configuration location is searched **ahead of** the location itself, so profile-specific tool config (`checkstyle.xml`, `packaging.properties`, and so on) overrides the general config. The resolved search order is: each module-local profile folder, then each project-wide profile folder, then the module-local folder, then the project-wide folders - profile beats plain, module beats project-wide. A configuration location may also carry a `process-.properties` file to pass extra arguments to any external tool the build runs under that name - mainly `javac`, `kotlinc`, and `scalac`, but equally `jar`, `jmod`, `jlink`, `jpackage`, or `native-image`. Each key is a flag and its value the flag's argument (an empty value emits a bare flag, a `\n`-separated value repeats the flag). The `prepare` step resolves each command's file by **first match** across the configuration locations - like `packaging.properties`, so a profile's file (or an empty one) shadows the general config and can switch inherited flags back off - then merges it into the `process/.properties` the build already feeds to that tool step, where a configuration key overrides a build-generated one of the same name. This is the profile-aware way to, for instance, compile a single module with additional `javac` flags. **2. Custom entry point under `build/`.** When you want code-level control - a tailored assembler, an extra step on top of the default per-module pipeline - drop a `.java` file alongside `Project.java` and use the Builder there. Run it the same way (`java build/MyBuild.java`): ```java package build; import module java.base; import build.jenesis.Project; import build.jenesis.project.InferredMultiProjectAssembler; import build.jenesis.project.MultiProjectAssembler; import build.jenesis.project.ProjectModuleDescriptor; public class MyBuild { static void main(String[] args) throws IOException { MultiProjectAssembler base = new InferredMultiProjectAssembler(); MultiProjectAssembler withSign = (descriptor, repos, resolvers) -> base.apply(descriptor, repos, resolvers).mapBuild(delegate -> (sub, inherited) -> { sub.addModule("assemble", delegate, inherited.sequencedKeySet().stream()); sub.addStep("sign", new Sign(), "assemble"); // Sign is a user-defined BuildStep }); Project.builder() .assembler(withSign) .build(args); } } ``` `apply` returns an `AssemblyDescriptor`: the per-module *build* phase plus any later (cross-module packaging) phases. `mapBuild` decorates only the build phase - here it registers `InferredMultiProjectAssembler`'s output as a nested module named `assemble` and chains a `sign` step onto it - while later phases pass through untouched. Assemblers compose this way freely: each layer wraps the build phase of its delegate and adds its own steps next to it, so you can stack multiple decorators (sign, attach licence headers, emit checksums) on top of a base assembler without subclassing. Jenesis itself relies on the same pattern internally - the `MAVEN` and `MODULAR_TO_MAVEN` layouts transparently wrap the user's assembler with `PomAwareAssembler`, which registers the user-supplied assembler under `assemble/` and emits the per-module POM alongside it. **3. Hand-wired build on the `BuildExecutor` API.** When auto-detection is not the right starting point at all - a non-Java pipeline, a wildly custom graph, or you just want the primitives - bypass `Project` and wire the build yourself: ```java package build; import module java.base; import build.jenesis.BuildExecutor; import build.jenesis.step.Bind; import build.jenesis.step.Jar; import build.jenesis.step.Javac; import build.jenesis.step.ProcessHandler; public class Hand { static void main(String[] args) throws IOException { BuildExecutor root = BuildExecutor.of(Path.of("target")); root.addSource("sources", Bind.asSources(), Path.of("sources")); root.addStep("classes", new Javac(ProcessHandler.Factory.of()), "sources"); root.addStep("artifacts", new Jar(ProcessHandler.Factory.of(), Jar.Sort.CLASSES), "classes"); root.execute(args); } } ``` `BuildExecutor.of(Path.of("target"))` is the root of the graph and writes all outputs under `target/`. `addSource` binds an input directory through a `Bind` step so changes to the path invalidate downstream caches; `addStep` chains a `BuildStep` whose argument list names its predecessors (`"sources"` for `classes`, `"classes"` for `artifacts`); `execute(args)` runs the requested target (or the whole graph by default), reusing cached outputs whose inputs have not changed. The full primitive set is documented under [Architecture](#architecture), [Build steps](#build-steps), and [Build executor modules](#build-executor-modules), and the demos under `demo/` (each a self-contained project with its own `build/` launcher) are progressively richer working starting points. ### Faster launch: precompiling and native images When the build ships as source files and is launched with `java build/jenesis/Project.java`, Java recompiles the build's own engine and `Project.java` on every invocation. While the build code is unchanged you can skip that recompile. Precompile once with `javac` and run from the classes: javac -d .jenesis/launcher \ $(find build/jenesis/ -name '*.java') java -cp .jenesis/launcher build.jenesis.Project [selectors...] Or ahead-of-time compile that launcher with GraalVM `native-image` for near-instant startup. Two things matter for the native build: - **How the image runs the JDK tools.** A bare native image has no in-process JDK tools, so `ProcessHandler.Factory.of()` detects the native-image runtime (via the `org.graalvm.nativeimage.imagecode` system property) and forks `javac`/`jar` from a JDK on `JAVA_HOME`/`PATH`. Adding `--add-modules jdk.compiler,jdk.jartool` to the `native-image` command keeps `javac` and `jar` *inside* the image; `Factory.of()` then sees that `javac` is present and runs the tools in-process, with no per-compile process fork. In-process is markedly faster - the ahead-of-time-compiled `javac` has no JVM startup and no JIT warm-up - so it is the form to prefer; the [Build performance](#build-performance) section measures a cold build at ~6 s in-process against ~13 s forking. (`-Djenesis.process.factory=tool|fork` overrides the choice.) A JDK on `JAVA_HOME` is still required either way - the build resolves its tool home from it, and `javac --release` reads that JDK's symbol files - so this trades the fork's process-spawn cost for speed, not the JDK dependency itself. - The incremental cache serializes every `BuildStep` to key it, so that a changed step configuration (for example the `jenesis.test.filter` filter) invalidates that step. That serialization needs native-image reachability metadata, which is captured from a real build with the native-image agent. Putting it together (after the `javac` precompile above), on a GraalVM JDK: # capture metadata from a representative build, running the tools in-process so javac/jar are recorded java -Djenesis.process.factory=tool \ -agentlib:native-image-agent=config-output-dir=.jenesis/native-config \ -cp .jenesis/launcher build.jenesis.Project build # build the launcher with javac/jar kept in the image, so the build compiles in-process native-image --no-fallback --add-modules jdk.compiler,jdk.jartool \ -H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler,com.sun.tools.javac.resources.javac,com.sun.tools.javac.resources.ct,sun.tools.jar.resources.jar \ -H:ConfigurationFileDirectories=.jenesis/native-config \ -cp .jenesis/launcher build.jenesis.Project jenesis # run it; in-process tools are auto-detected, no factory flag needed (a JDK on JAVA_HOME is still required) JAVA_HOME=/path/to/jdk ./jenesis [selectors...] The `-H:IncludeResourceBundles` flag is essential, not optional: the agent only records the message bundles a captured compile happened to load, so without forcing javac's bundles into the image the in-process compiler dies with a `MissingResourceException` (`JavacMessages.getBundles`) the first time it formats a diagnostic it had not recorded. For the smaller fork-based image instead, drop `--add-modules` and `-H:IncludeResourceBundles` and capture with `-Djenesis.process.factory=fork`; it then forks `javac`/`jar` from a JDK on `PATH` at run time. Capture the metadata from builds that exercise every layout and step you use, custom steps included, since it only covers what the agent run reached. Loading foreign build modules (`InternalModule` / `ExternalModule`, which bridge classes across class loaders) needs a full JVM and is not supported in a native image. Rebuild the compiled or native launcher whenever the build sources change; this only accelerates launching the build, while the project being built is still recompiled by the build graph when its own sources change. ### Building the launcher with Jenesis itself The native build above invokes `native-image` by hand. Jenesis can also drive it as a build step, the same way it drives `javac`, `jlink` or `jpackage`, so the launcher is produced by the build rather than a separate script. Because the root `pom.xml` declares `build.jenesis.Project`, the default assembler treats `build.jenesis` as a launchable module and, when asked, wires a `native-image` step for it. The reflection that `native-image`'s closed-world analysis cannot see is discovered by running the build's own tests under GraalVM's tracing agent, which a `graal.properties` file in the configuration directory switches on. A single build, on a GraalVM JDK (or with `GRAALVM_HOME` pointed at one), captures that metadata and compiles the image in one pass once a `packaging.properties` in the configuration location sets `native=true`: java build/jenesis/Project.java stage The `graal.properties` file's presence attaches `-agentlib:native-image-agent` to the test JVM (with `jenesis.observe.native` a default-`true` suppress override, set it to `false` to switch the agent off), so the agent records every reflective, JNI, resource and serialization access the build's tests exercise (including the `BuildStep` serialization the incremental cache depends on), stages it under `nativeimage/` (a build-internal capture, deliberately not a `reports/` report - those are for external tools) and records, in the test module's `inventory.properties`, both that capture (`nativeimage`) and the artifact it tests (`test`). The `native=true` `packaging.properties` then runs `native-image` over the produced launcher in a terminal *package* phase that runs after every module's build: it reads its sibling modules' inventories and collects the capture of the test module whose `test` names this module's `artifact` (the same cross-module channel staging uses for `package`/`image`/`jmod`), so each image gets exactly its own test's metadata - no `META-INF/native-image/` commit in between. The tool is located through `GRAALVM_HOME`, then `java.home`, then `PATH`, and the `stage` goal collects the executable into its canonical target directory (the native-image analogue of `stage/packages` and `stage/runtime`): target/stage/native/output/build.jenesis Running that binary launches the build with the near-instant startup measured below. This was verified end to end on a GraalVM 25 JDK: the agent run captured the reachability metadata and the resulting native executable launched the build correctly. This single-build flow is what the `MultiProjectAssembler` split makes possible. An assembler returns an `AssemblyDescriptor`: a per-module *build* phase plus optional later phases appended with `then(...)`. The heavy packaging steps (`jlink`, `jpackage`, `bundle`, the launcher and `native-image`) move into a *package* phase that the consumer wires as a second, cross-module level - it runs after every module's build with each sibling module's `inventory.properties` in scope (the only cross-module input it needs, so no sibling jar leaks onto a module path). The build phase stays per-module and is the only thing siblings bind (through `assign`), so packaging never blocks a downstream module's compile. Because `native-image` lives in that terminal phase rather than the per-module pipeline, the main module's image can depend on the test module's capture even though the test module `requires build.jenesis` - the cycle that previously forced a two-build `META-INF/native-image/` hand-off is gone. The same flow drives an application image end to end in [`demo-41-native-image`](demo/demo-41-native-image). Committing the metadata under `sources/META-INF/native-image/` still works - GraalVM scans it inside every jar on the module path - and remains the way to vet exactly what reflection gets baked into a published artifact. ### Build performance Because every build step is content-hashed and the build itself ships as plain Java source, Jenesis's performance profile differs from a plugin-based build, and it shifts again with how the build is launched. The figures below build Jenesis itself (131 main sources, 112 test sources, 1054 tests) and compare it against Maven. *Conditions.* The numbers are produced by the on-demand `benchmark` workflow (`.github/workflows/benchmark.yml`), which runs `benchmark/benchmark.sh` unattended on GitHub-hosted runners - Linux x64, macOS ARM64 and Windows x64 - on Oracle GraalVM 25, with Maven pinned to 3.9.9 and Maven 4.0.0-rc-5 downloaded for the Maven-3-vs-4 table. The headline tables below are the **Linux x64** runner (the x86-64 baseline); the *Across three platforms* section at the end gives the macOS and Windows spread. These are shared, virtualised machines, so treat the absolute numbers as representative rather than precise to the tenth - what holds is the *shape*: the ordering of the launchers and the order-of-magnitude warm-path gap, and it reproduces on every runner. All wall-clock times are the median the harness records (the `%e` elapsed field, never a build tool's own self-report); both tools run with warm dependency caches (`~/.m2` for Maven, `.jenesis/artifacts` for Jenesis). The cold compile figure is the median of five runs and the other compile-only figures the median of three; the full builds are run once or twice, being test-bound and barely variable. Warming the caches isolates CPU cost, but it also hides a charge Maven pays on any cold CI runner whose `~/.m2` was not restored: before any build work it downloads its own distribution (via the wrapper) and its full plugin tree on top of the project's dependencies - the one-time, latency-bound, offline-impossible fetch detailed under *First run on a bare machine*. Jenesis pulls only the project's own dependencies (no tool, no plugins), and its source-launcher overhead is a pure-CPU engine recompile that needs no network - so on a genuinely cold CI build the source launcher's recompile tax is offset by the Maven download these warm-cache tables leave out. The only build that touches the network is the full build with tests, which moves about 25 KB **symmetrically on both tools** (a `maven-metadata.xml` lookup for the `RELEASE`-versioned external tools the test fixtures resolve - PMD, Checkstyle, the Scala and Kotlin compilers); that is sub-second against ~200 s of test execution and identical on both sides, so it does not bias the comparison. Maven is 3.9.9 (Maven 4 is discussed at the end). The Jenesis build is launched three ways, differing only in how the build engine is loaded: - *source* - `java build/jenesis/Project.java`, the canonical form, which recompiles the engine every invocation; - *compiled* - `javac` the engine once into `.jenesis/launcher`, then `java -cp .jenesis/launcher build.jenesis.Project`; an SDKMAN-installed `jenesis` (or the unpacked release zip) runs this same already-compiled engine jar, so it shares the *compiled* figures rather than the *source* ones; - *native* - Graal `native-image` the launcher once into `./jenesis`, a standalone ahead-of-time-compiled binary. This is the *Graal* native image, and it is a different mechanism from *Java AOT* (JDK 25's command-line `-XX:AOTCache`, JEP 514/515), which keeps the *compiled* launcher on the JVM and is measured separately below. Throughout this section *Graal* means the native-image binary and *Java AOT* the JVM cache; the two are never the same row. **Launch overhead** (running `help`, no project work; median of three): | Launcher | Overhead | |-----------------|--------------| | source | 3.6 s | | compiled | 0.14 s | | native (Graal) | under 0.01 s | The source launcher recompiles the roughly 130-file engine on every run; removing that is the entire point of precompiling or building the Graal image. The one-time cost to obtain them is a couple of seconds for the `javac` launcher and a few minutes (yielding a ~60 MB binary) for the Graal image, and both must be rebuilt whenever the build sources change. **First run on a bare machine.** The tables assume the build tool and its caches are already present; standing each tool up from nothing - a shell and a JDK, empty caches - is a one-time cost worth comparing too. Maven is not on the machine, so the project carries the Maven wrapper: `./mvnw` downloads the Maven distribution (a 9.4 MB zip, plus the 63 KB `maven-wrapper.jar` when it is not committed), unzips it and boots Maven in about 1 s here. That only obtains Maven itself - the first real build then populates an empty `~/.m2` with every plugin, its POM and its transitive libraries alongside the project's own dependencies. For this project that is about 33 MB across ~490 files (56 jars, 127 POMs, the rest checksums and metadata) and added roughly 6.7 s to the first build on this connection; being many small POM and metadata requests it is latency-bound rather than bandwidth-bound, so it stretches on a slow or distant link and is impossible offline. Roughly a third of those bytes are Maven's own runtime and plugins (`maven-compiler-plugin`, `maven-surefire-plugin`, `maven-jar-plugin` and their `plexus`/`codehaus` plumbing) - build tooling that has no Jenesis equivalent. Jenesis ships as source and runs on the JDK that is required anyway, so there is no tool to download and no plugin ecosystem to fetch; its first run pulls only the project's own dependencies, the same ones Maven also downloads. What it pays for being source rather than a compiled artifact is the per-run engine recompile from the launch-overhead table - about 3.5 s (`java build/jenesis/Project.java help` at ~3.6 s against the compiled launcher's ~0.14 s). That cost is CPU-only: it transfers nothing, works offline, does not grow on a worse connection, and disappears once the engine is compiled once into `.jenesis/launcher` (0.07 s) or installed through SDKMAN, which ships that same compiled jar. So on a fast network these are all single-digit seconds and roughly comparable; the difference is in kind, in that Maven must reach the network to assemble its tooling before it can build while Jenesis trades that for a fixed, offline, eliminable recompile. There is a supply-chain dimension to the wrapper too. The Maven wrapper commits a 63 KB `maven-wrapper.jar` into the repository, and Gradle's equivalent a 47 KB `gradle-wrapper.jar` (which then fetches a ~131 MB Gradle distribution on first run, around 6 s here and far longer on a slower link); each is an opaque binary, checked into version control and run with the build's full privileges, which is enough of an attack surface that Gradle ships a dedicated wrapper-validation step to checksum its jar. Committing a binary to a source repository is exactly what a build tool should not require: Jenesis checks in only the engine's Java source under `build/jenesis/`, auditable as text and compiled locally by the JDK, so there is no opaque artifact to trust or verify in the first place. **Compile and package, tests compiled but not run** (`-DskipTests` for Maven, `-Djenesis.test.skip` for Jenesis). Both compile the 131 main and 112 test sources and skip only execution, which is the fairest tool-to-tool comparison: CPU-bound and network-free. Cold is the median of five runs; the rest are the median of three: | Scenario | Maven 3 | Jenesis source | Jenesis compiled | Jenesis native (Graal) | |-----------------------------------|---------|----------------|------------------|------------------------| | cold (empty `target/`) | 13.7 s | 18.2 s | 12.5 s | 7.8 s | | warm no-op (nothing changed) | 2.0 s | 8.9 s | 0.54 s | 0.09 s | | one-line edit to a main source | 13.9 s | 11.0 s | 4.5 s | 1.1 s | | spurious `touch` (content same) | 13.8 s | 8.9 s | 0.57 s | 0.09 s | The `native (Graal)` column is the Graal `native-image` build run in-process (`--add-modules jdk.compiler,jdk.jartool`, see below); a bare native image that forks `javac` instead is slower cold and is the variant to reach for only when a smaller image matters more than build speed. This column is the Graal native image, not the Java AOT cache (the JVM `-XX:AOTCache`) tabled further down. Read cold-to-warm, not only left-to-right. Cold, the compiled launcher (12.5 s) edges out Maven (13.7 s) and the two are otherwise close: Jenesis's extra per-build work (resolving the dependency graph, emitting a POM, metadata and an inventory, content-hashing every input and output) is repaid by compiling each module in a single in-process `javac` invocation. The source launcher adds the per-run engine recompile on top (18.2 s), though a cold build partially amortizes it: the in-memory engine compile leaves `javac` JIT-hot for the project compile that follows, so the recompile costs less net here than on the warm no-op, where there is no following compile to absorb it. Warm and incremental are where content-hashing separates them. A no-op rebuild is 0.09 to 0.54 s on a compiled or Graal launcher, because every step's input hash matches its recorded output and nothing re-runs, while Maven still walks its lifecycle in ~2.0 s. The source launcher is the honest exception: even its no-op is ~8.9 s, *slower* than Maven, because it recompiles the engine before the hash check can skip the unchanged build - the per-run tax that `compiled`, `native (Graal)`, and an installed `jenesis` remove. On any change to a main source - a real one-line edit, or even a content-preserving `touch` - Maven's mtime-based staleness recompiles the main sources *and* all 112 test sources (~14 s), because test compilation depends on the main output. Jenesis keys on content instead: a `touch` that does not change the bytes is a near no-op (0.09 s on Graal), and a real edit recompiles only the module that changed (4.5 s compiled), leaving the unaffected module's compiled tests cached. Flight-recorder profiles (`-XX:StartFlightRecording=settings=profile`) confirm where the differences come from. In the source-launcher cold build the hottest methods are all in `com.sun.tools.javac` (`Type.hasTag`, `Resolve.instantiate`, `Check.checkType`): it is compiler-bound, and it compiles the 130-file engine on top of the project's own sources. The compiled-launcher cold build is also compiler-bound but only for the project sources - the engine is already compiled - which is the ~6 s difference between them. In the warm no-op build the compiler disappears from the profile entirely; the hottest methods are `sun.security.provider.DigestBase` and file I/O, because the only work left is content-hashing step inputs and outputs to confirm nothing changed. That digest is the incremental cache's `jenesis.executor.digest`, MD5 by default (`DigestBase` is the shared base class; the samples are MD5's compression), and is deliberately distinct from the SHA-256 used for dependency-pinning checksums, which a warm build does not touch. That is the incremental engine made visible: a warm rebuild hashes, it does not compile. The Graal launcher leads the table, and why is worth unpacking. It is built with `--add-modules jdk.compiler,jdk.jartool` (see [Faster launch](#faster-launch-precompiling-and-native-images)), which keeps `javac` and `jar` *inside* the image so `Factory.of()` runs them in-process. Graal's ahead-of-time-compiled `javac` reaches full speed instantly - no JVM to boot, no JIT warm-up - so it wins every row: a cold build in ~7.8 s (under the compiled launcher's 12.5 s and Maven's 13.7 s), a one-line edit in ~1.1 s (the changed module recompiled by an already-hot Graal-compiled `javac`, against the compiled launcher's 4.5 s), and the warm and `touch` no-ops at ~0.09 s (just the MD5 hash check, with no JVM to start). Its times are also the most *reproducible*: being compiled to native code they barely move run to run, where the JVM launchers drift with JIT and the shared runner's variable clock. Two caveats come with it. A Graal native image is closed-world, so its reachability metadata must be complete - and the agent only records the message bundles a training compile happened to load, so without `-H:IncludeResourceBundles` for javac's and jar's bundles the in-process compiler dies at run time with a `MissingResourceException` (`JavacMessages.getBundles`); the flag forces them in and makes it deterministic. And a *bare* Graal native image, without `--add-modules`, has no in-process JDK tools and forks an external `javac` instead - that variant is the slowest cold (~12.5 s, paying a process fork plus a cold `javac` JVM with no shared JIT state) and earns its keep only when a smaller image and reusing the machine's JDK matter more than build speed. A JDK on `JAVA_HOME` is required either way (`javac --release` reads its symbol files). **Java AOT, without Graal (JDK 25).** Distinct from the Graal `native` launcher above, the *compiled* launcher can also be sped up on a plain HotSpot JVM with Java AOT, JDK 25's command-line AOT cache (JEP 514/515): a *recording run* captures the classes the build loads and links, plus JIT method profiles, into a cache that later runs memory-map in. The launcher stays ordinary JVM bytecode - no `native-image`, no standalone binary - so Java AOT is a separate, lower-effort mechanism from the Graal native image, and a smaller win. Measured across the same scenarios as the launcher table above (each cell is *compiled (jar)* baseline / *+ Java AOT*; from a dedicated `aot` run, so the baseline is re-measured and drifts a little from the headline *compiled* figures): | Scenario | Linux x64 (jar / +AOT) | macOS ARM64 (jar / +AOT) | Windows x64 (jar / +AOT) | |---------------------------|------------------------|--------------------------|--------------------------| | launch overhead (`help`) | 0.15 / 0.15 s | 0.09 / 0.09 s | 0.25 / 0.24 s | | cold (empty `target/`) | 12.9 / 12.6 s | 8.0 / 7.6 s | 16.6 / 16.5 s | | warm no-op | 0.59 / 0.54 s | 0.39 / 0.38 s | 0.90 / 0.83 s | | one-line edit | 4.9 / 3.9 s | 2.7 / 2.3 s | 6.9 / 5.3 s | | spurious `touch` | 0.59 / 0.57 s | 0.35 / 0.32 s | 1.15 / 1.10 s | The baseline column is the compiled launcher repackaged as a jar (Java AOT requires it; see below), so it tracks the *compiled* column of the headline table. The cache is within run-to-run noise on the launch, no-op and `touch` rows (there is barely any JVM work there to front-load) and a few percent on the cold build (dominated by actual compile work, not warm-up). Its clearest win is the **one-line edit** - ~12 to 22 % across the three platforms - because a short incremental build is mostly engine startup and `javac` warm-up, exactly what the cache replays. Even there it cannot approach the Graal launcher (a ~1.1 s edit), because the JVM still interprets-then-JITs `javac` where Graal's `javac` is machine code from the first instruction. Two practicalities decide whether it works: the engine must be a *jar*, not an exploded class directory (CDS/AOT refuses a non-empty directory on the classpath), and the cache comes from a training run: jar --create --file launcher.jar -C .jenesis/launcher . java -XX:AOTCacheOutput=build.aot -cp launcher.jar build.jenesis.Project build # recording run java -XX:AOTCache=build.aot -cp launcher.jar build.jenesis.Project build # subsequent runs The same mechanism would trim the startup and early warm-up off the test JVM, but the full build's test phase is dominated by long-running and forked external tools rather than JVM warm-up, so the effect there is small. `benchmark/benchmark.sh aot` reproduces these figures. **Full build, all 1054 tests** (warm caches; the ~25 KB symmetric metadata fetch noted under *Conditions* applies here and only here): | Scenario | Maven 3 | Jenesis | |----------------------------------|---------|---------| | cold | 199 s | 201 s | | warm no-op (nothing changed) | 188 s | 9.2 s | The launcher variant is omitted here on purpose. This table is test-bound, and which launcher starts the build (source, compiled, or Graal) only moves the few seconds of launch-and-compile against ~200 s of tool-driven test execution; the Graal launcher's win does not apply, and running the 1049-test integration suite *inside* a Graal native image (JUnit plus the forked external tools, all closed-world) is impractical, so the JVM launcher is used. Cold, the two land within about one percent: the build is dominated by running 1054 integration tests that drive external tools (PMD, Checkstyle, the Scala and Kotlin compilers, and so on), which is process- and IO-bound and essentially independent of the build tool. The warm no-op row is where they diverge by more than an order of magnitude. Maven's surefire re-runs the entire suite on every invocation, so a rebuild with nothing changed still costs the full ~188 s; Jenesis content-hashes each test step's inputs - the compiled classes under test and their dependency closure - and skips the step when that hash is unchanged, so a no-op rebuild is about 9.2 s. This is the property that compounds in multi-module builds: a test step re-runs only when the artifacts it depends on actually change, so editing one module re-tests that module and its dependents while every unaffected module keeps its cached test result. The single-project measurement here is the floor of that effect; the more modules a build carries, the larger the share of the suite a typical change leaves untouched. (The ~1 % cold gap is genuine and not a bias: byte counters put both tools at the same ~25 KB of metadata I/O, and the cold builds run with the external-tool cache already warm on both sides.) **Maven 3 versus Maven 4.** The project's `pom.xml` pins `maven-compiler-plugin` to 3.15.0 in ``, which both Maven versions honour; Maven 3.9.9 already selects that version by default, while Maven 4.0.0-rc-5 would otherwise bind the older 3.13.0, whose bundled ASM rejects Java 25 bytecode (`Unsupported class file major version 69`), so the pin is what lets Maven 4 compile Java 25 at all. With it in place the two are close on real builds, Maven 4-rc5 adding a fixed slice of CLI startup that shows up most on near-empty rebuilds (Linux x64): | Scenario | Maven 3 | Maven 4 | |------------|---------|---------| | cold | 13.9 s | 14.9 s | | warm no-op | 2.0 s | 3.0 s | That gap is startup, not compile work, so it weighs most on the warm no-op; it is also platform-sensitive - about 0.5 s on macOS ARM64, but ~3 s on Windows x64 (a 6.3 s no-op against Maven 3's 3.3 s). The headline tables above use Maven 3.9.9. Jenesis sidesteps the plugin question entirely: it compiles through the JDK's own `javac` by way of `ToolProvider`, with no intermediate bytecode-analysis layer that can lag the JDK. **Across three platforms.** The headline tables above are the Linux x64 runner; the workflow runs the same `benchmark/benchmark.sh` on macOS ARM64 and Windows x64 too, and the *shape* is identical on all three. Cold compile-and-package and warm no-op, medians under the same `-DskipTests` / `jenesis.test.skip` flags: | Platform (runner) | Maven 3 cold | source cold | compiled cold | native (Graal) cold | Maven 3 warm | source warm | compiled warm | native (Graal) warm | |-------------------|--------------|-------------|---------------|---------------------|--------------|-------------|---------------|---------------------| | macOS ARM64 | 11.2 s | 17.6 s | 11.4 s | 6.9 s | 2.2 s | 11.9 s | 0.63 s | 0.18 s | | Linux x64 | 13.7 s | 18.2 s | 12.5 s | 7.8 s | 2.0 s | 8.9 s | 0.54 s | 0.09 s | | Windows x64 | 22.3 s | 27.9 s | 16.7 s | 9.3 s | 4.4 s | 15.5 s | 0.87 s | 0.17 s | The ordering is the same on every platform: the Graal launcher is the fastest cold build and the source launcher the slowest (it recompiles the ~130-file engine on every run), with the compiled launcher edging Maven in between; a warm no-op on a compiled or Graal launcher stays an order of magnitude under Maven, while the source launcher's no-op stays above it for the same recompile reason. macOS ARM64 is the quickest of the three - its single-thread speed gives the lowest Graal cold build (~6.9 s) - and Windows x64 the slowest, paying its heavier filesystem. The secondary findings hold across all three: `pin=versions` is indistinguishable from the default (checksum validation costs nothing on the warm path), the Java AOT cache is within run-to-run noise on the no-op paths and a modest win (~12 to 22 %) only on an incremental edit, and the in-process Graal launcher built successfully on every runner, Windows included. ### Selectors `Project.main(args)` and `Project.build(args)` accept selector strings as positional arguments. The canonical example is `stage`, which runs the full release recipe (build → stage) and materialises a Maven-shaped tree under `target/stage/maven/output/`: java build/jenesis/Project.java stage Without arguments, `Project` runs whatever its `defaultTarget` is set to. Out of the box that is `"build"`, which compiles and packages every discovered module but stops short of the downstream `stage` step. `Project.defaultTarget(...)` changes the default (there is no matching system property). The other top-level targets the shipped layouts register are `export` (on `MAVEN`, `MODULAR`, and `MODULAR_TO_MAVEN`; `MAVEN` publishes the staged tree into the local Maven repository, `MODULAR` into the local Jenesis module repository, and `MODULAR_TO_MAVEN` into both), `pin` (rewrite every `pom.xml` / `module-info.java` so the full transitive closure is pinned at source level), `dependencies` (print each module's resolved dependency graph, with licenses, from the persisted `graph.properties`; see *Printing the dependency tree*), and `ide` (generate IntelliJ IDEA, VS Code, and Eclipse project metadata at the project root from the same persisted inventories; see *Generating IDE metadata*). Three further selectors print or refresh without building artifacts: `help` (the human usage screen), `skill` (an agent-oriented briefing of the same material), and `metadata` (refresh the metadata module outputs). **Module selectors.** Selectors that start with `+` are rewritten by the active layout into the per-project module path of that name, so a single module can be built without dragging its siblings in. The shipped layouts encode names as `module-` and place them under their per-project aggregator: - `+sources` resolves to `build/modules/compose/module/module-sources` under `MODULAR` and `MODULAR_TO_MAVEN`, or to `build/maven/compose/module/module-sources` under `MAVEN`. - `+` alone resolves to `module-` (trailing empty segment), the identity Maven's scanner produces for the root POM in a multi-module Maven layout. A pure modular project has no such root, so `+` alone will not resolve there. The rewriter always yields a literal path, which avoids the lenient cascade that a bare module name would trigger across sibling modules. **General syntax and wildcards.** Under the hood every selector is a slash-delimited path of step identities (`module/step`) that the executor matches against the registered graph. Two wildcards are supported: - `:` matches a single path segment, so `build/:/binary` matches the `binary` module of every direct child of `build`. - `::` matches any depth (zero or more segments), so `::/sign` matches every `sign` step anywhere in the tree. Wildcards are lenient: branches that fail to match are silently skipped. A literal path that does not resolve throws. Once a step is matched, its transitive preliminary closure runs unconditionally, so its inputs are real folders rather than lenient-skipped placeholders. The full mechanics, including how sibling modules along a wildcard path still have their `accept(...)` invoked, are documented under [`BuildExecutor`](#buildexecutor) and [Selectors on the command line](#selectors-on-the-command-line). ### Dependency groups Every resolved dependency is keyed by a **group** - the top isolation axis of dependency resolution, with scope nested beneath it (`///`). Each group is a self-contained version tree: coordinates in different groups resolve and pin independently and never collide, even when they name the same library at different versions. Two kinds of group exist, and the defaults are chosen so a normal build never sets either: - The **project group**, `main` by default, holds the project's own `compile`/`runtime` dependencies. Every step or module that consumes them - `Javac`, `Java`, `JLink`, `JPackage`, `NativeImage`, `Bundle`, `Launcher`, `TestModule`, the compiler/quality modules' compile classpath, `Inventory`, `InternalModule`, `ExternalModule` - takes it from a `group(String)` wither that defaults to `main`. - A **tool group**, named after the tool (`kotlinc`, `scalac`, `groovyc`, `spotbugs`, `pitest`, …), holds that tool's own runtime/plugin closure so it never leaks into the project. The tool modules expose a `tool(String)` wither for it, defaulting to the tool name. Because both are withers over a default rather than hard-coded literals, a custom build can define **multiple groups**. For example, point two `KotlinCompilerModule`s at different tool groups (`.tool("kotlinc")` and `.tool("kotlinc2")`, each pinning its own compiler version) to run two compiler versions side by side, or resolve a second project dependency set under its own group and drive a packaging step off it with `.group("…")`. The chosen group flows down through the step into `Dependencies`, which writes it as the leading segment of each index key. The pin grammar abbreviates the `main` group away (a bare `` or `/` is implicitly `main`); any other group is written with an explicit `/…` prefix. ### Layouts and assemblers Two callbacks govern how the build is assembled, and they are pluggable independently: - `Project.Layout` (set via `.layout(...)`) wires the top-level pipeline (the `build` multi-project module, the `stage` step that walks per-module inventories, and on the Maven layouts the `export` step that publishes the staged tree) and returns the `Function` that expands `+`-prefixed selectors. The shipped constants `Layout.MAVEN`, `Layout.MODULAR`, and `Layout.MODULAR_TO_MAVEN` each wire one of the concrete pipelines tabulated below. `Layout.AUTO` (the default) calls `Layout.of(root)` and dispatches to one of the concrete layouts: a root `pom.xml` selects `MAVEN`, otherwise a `module-info.java` under the root selects `MODULAR_TO_MAVEN`, so a module declaration resolves its dependencies (named or automatic) through Maven by default. `MODULAR` (a modular jar resolved only against a module-name registry, with no emitted `pom.xml`) is reachable only explicitly, since by definition it cannot resolve a dependency that is published only as an automatic module. - `MultiProjectAssembler` (set via `Project.assembler(...)`) wires the per-project sub-graph: what each discovered module compiles, packages, and tests. The assembler's `apply(D descriptor, Map repositories, Map resolvers)` receives the per-module descriptor *and* the per-module merged repositories/resolvers (the layout-level maps with each sibling sub-module's `assign` URI prepended, so a coordinate resolved locally never falls back to the global repository), and returns an `AssemblyDescriptor`: the per-module *build* module plus any later packaging phases appended with `then(...)`, which the layout runs as a second, cross-module level after every module has built (so a `native-image` step can read a test module's captured metadata, and packaging never blocks a sibling's compile). `Project` parameterises this over `ProjectModuleDescriptor`, an immutable class that captures the layout's base descriptor (`MavenProject.MavenModuleDescriptor` or `ModularProject.ModularModuleDescriptor`) and adds the project-level flags `test`, `source`, `documentation`; it exposes a wither per property (`sources(...)`, `artifacts(scope, ...)`, `test(...)`, and so on, each with a `String...` overload) so a wrapping assembler can customise any input without reimplementing the descriptor. The default assembler `InferredMultiProjectAssembler` is stateless and reads those flags off the descriptor it receives - no `Context` object: a `prepare` step plus a `JavaToolchainModule` is wired against the descriptor's reference-key sets (`sources`, `manifests`, and the compile/runtime `artifacts` keys) and the module's resources, and when `descriptor.test()` is set and the module's `module.properties` flags it as a test variant a `TestModule` sub-module is wired alongside the `JavaToolchainModule`, with an optional `sources` archive and an optional `documentation` sub-module (the inferred documentation chain, see below) appended when the matching flag is set. The `MAVEN` and `MODULAR_TO_MAVEN` layouts wrap the user's assembler with a `PomAwareAssembler` that emits a per-project `pom` step seeded with project-wide metadata read once from `metadata.properties` (when configured); `MODULAR` does not. Each layout adds a top-level `pin` module (sibling of `build`) that walks the BUILD outputs' per-module `inventory.properties` and rewrites every discovered `pom.xml` / `module-info.java` so the full transitive closure (with checksums where available) is pinned at source level. Each module's descriptor is pinned from **that module's own** resolved closure: every module's `Inventory` step records its resolved closure into `inventory.properties` (the `prefix.dependency` entries, each a coordinate with its jar path and any carried checksum), its own coordinates (`prefix.identity`), and its source path (`prefix.path`). The `pin` module reads those inventories, enumerates the modules to pin from their `prefix.path` entries, and wires one pin step per module that pins only that module's own `prefix.dependency` closure (selected by the module's prefix), so a dependency used by one module never appears in a sibling's dependency-management block. The pinned set is read from that resolved closure (concrete versions and checksums), not from the descriptor's own managed versions, so stale or cross-module entries are never carried over. Pin is opt-in - it's not part of the default target - and it skips coordinates that come from within the project (every module's `prefix.identity`, originally advertised through the `assign` step's `identity.properties`), so internal modules never leak into the dependency-management block. Within a module, pin records one version per dependency coordinate **per resolver group**, and every pin uses the single `@jenesis.pin` tag (in `module-info.java`) or `` comment block (in `pom.xml`). The group is the top isolation axis (scope sits beneath it): the canonical properties key is always `//` (e.g. `main/module/org.junit.jupiter`, `kotlinc/maven/org.jetbrains.kotlin/kotlin-compiler-embeddable`), so the project's own dependencies (group `main`, which mediates its `compile` and `runtime` scopes into one version tree) and a tool's copy of the same library (a separate group - the Kotlin/Scala/Groovy compiler in its `kotlinc`/`scalac`/`groovyc` group, or an `InternalModule` / `ExternalModule` build tool) never collide. The grammar is group-first, `@jenesis.pin // [/]`, with two abbreviations into the default `main` group keyed off the token's slash count: a bare `` (no slash) is short for `main/module/`, and a `/` (one slash) is short for `main/maven//`; a Maven coordinate carrying a type or classifier (more than one slash in the coordinate) is written in full. The repository is otherwise spelled out (`module`, `maven`); a `module-info.java` can therefore pin a Maven coordinate it pulls in transitively (e.g. a non-modular transitive of a named module dependency) as `org.foo/bar` (or `main/maven/org.foo/bar` in full) even though its declared dependencies resolve through the `module` repository. The grammar - including both abbreviations - is identical for the `module-info.java` `@jenesis.pin` tag and the `pom.xml` `` comment block, and a malformed token fails the build in either place rather than being silently ignored. A `module`-repository pin's version value may additionally select an artifact classifier with a leading-colon qualifier, `:[:]` (e.g. `@jenesis.pin org.example.native :windows-x86_64:1.2.3 /`, or `:windows-x86_64` alone to leave the version floating). The pin stays keyed by the bare module name - the classifier is part of the value, never the coordinate - so it applies wherever the module is required, directly or transitively, and only one variant of a module name can be present, mirroring the module path's own uniqueness rule. The leading colon is reserved territory: a legal Maven version can never contain `:` at all and a parseable `module-info.class` version must start with a decimal digit, so a qualified value can never be mistaken for, or forged by, a real version (`ModularJarResolver` rejects declared and compiled module versions that start with a colon for exactly that reason). Internally the resolver addresses the variant by fusing the classifier into the module identifier, `-` - a dash cannot occur in a Java module name, so the fused form is unambiguous - and that fused identifier is the repository-coordinate and filename shape: `JenesisModuleRepository` resolves `-[/][:type]` to `/[/]-.` (the worker's classifier contract), and `JenesisRawGitRepository` switches the lookup to the classifier-scoped `modules-.tsv` / `artifacts-.tsv` and appends the classifier to the resolved Maven filename. The `pin` step folds a fused resolved coordinate back into the value form, so repinning round-trips the classifier. Classifier pins resolve through the `module` repository only: `MavenModuleResolver` (the `MODULAR_TO_MAVEN` layout) rejects them, since a classified artifact shares its GAV's POM and the module-name translation has no per-classifier POM to fetch. Per-machine selection composes on top as plain data, never as code: a pin line may end with a bracketed **platform guard**, `@jenesis.pin org.openjfx.javafx.base :win:21.0.3 / [windows]`, and the manifests step keeps only the line whose guard matches the active platform when it writes `versions.properties`, so everything downstream of manifests is untouched by the mechanism. The same guard applies to pin lines in a `pom.xml`'s `` comment block, where it selects which version of a (typically transitive) coordinate the bill of materials pins per platform; a directly declared `` stays authoritative over the bill of materials, and a guard can never force in a classifier, since Maven dependency management matches classifiers as part of the coordinate key rather than contributing them. The active platform is a set of tokens that starts from the detected operating system and chipset - one of `windows`/`linux`/`macos` plus one of `x86_64`/`aarch64` - which a `-Djenesis.platform.=true` system property adds to and a `-Djenesis.platform.=false` property removes from (so `-Djenesis.platform.fips=true` activates a `[fips]` guard on top of the real machine, while `-Djenesis.platform.linux=false -Djenesis.platform.windows=true` cross-resolves a Windows closure from a Linux host; an in-code build can pass an arbitrary platform through the `platform(...)` builder method). A guard matches when all of its tokens are contained in the active set; the most specific match (largest guard) wins, an unguarded line for the same key is the fallback, two equally specific distinct matches fail the build, and an unmatched guard without a fallback leaves the key unpinned (which strict pinning then rejects). The platform is a field of the manifests step, so the cache identity is honest: with no guards the step's output is byte-identical on every machine and the resolution cache stays portable across machines; with guards, resolution legitimately runs once per platform. The `pin` goal applies the same guard matching: the one line that matched the local platform (a guard or the unguarded fallback) is refreshed from the resolved closure, keeping its guard, and every non-matching line is preserved byte-for-byte, since a local repin only observes the variant the local platform selected; keys without guards are rewritten as usual. Groups whose resolution Maven must not act on (the compiler groups, including their scope `plugin` compiler-plugin entries) stay out of `` and live in the `` comment instead; because an XML comment cannot contain `--`, any such sequence in a POM value is stored as `--`. The Kotlin, Scala, and Groovy compiler modules resolve into the `kotlinc`, `scalac`, and `groovyc` groups respectively (named after the compiler binaries), so the inferred compiler chain isolates them with no extra wiring. Because each compiler resolves in its own group, the compiler process is launched against only its own group's artifacts, so a project that pins a different version of a library the compiler also uses (for example `kotlin-stdlib`) can no longer downgrade the running compiler - that library still reaches the compilation classpath, but not the compiler's own runtime. - A **bill of materials** shares a curated set of version and checksum pins across modules and projects in the modular layouts. A BOM is a plain properties file whose keys follow the pin token grammar minus the group - a bare `` is short for `module/`, a `/` (one slash) for `maven//`, and anything with more slashes is an explicit `/` - and whose values are the pin value grammar, `[ /]`, including leading-colon classifier qualifiers; checksums are optional, and platform guards are not allowed inside a BOM file. A module imports a BOM through the `@jenesis.bom` tag in `module-info.java`: `@jenesis.bom [ [/]] []` names a BOM artifact using the pin token grammar (a bare `` is short for `main/module/`, a qualified `/module/` selects the group its entries merge into) that the dependencies step fetches from the module repository as `//.properties` - or, with no version, as the floating `/.properties` latest, which is only as reproducible as an unpinned module dependency and cannot carry a hash. A token of `[/]bom-.properties` instead reads that file from the project's BOM locations: the `boms` property of `Project` (`-Djenesis.project.boms=` or the `boms(...)` builder method), which defaults to the `configuration` locations - by default `build.jenesis/` under the project root - so one shared file serves every module. The `bom-` prefix keeps a file reference structurally distinct from a module coordinate, since a dash can never occur in a Java module name (the same fact the classifier fusion relies on). The locations are searched in order with the first hit winning, and they are deliberately not profile-resolved, so no profile can swap a BOM and a build cannot come to depend on dynamic configuration. Every `bom-*.properties` file found in the BOM locations is bound as a tracked input of each module's manifests step, so editing a BOM invalidates the build correctly. The declaration - not the BOM file - carries the group and an optional trailing platform guard with the same matching rules as pins (most specific guard wins, the unguarded line is the fallback, an unmatched guard skips the import). Precedence is strictly local-first: an explicit `@jenesis.pin` always overrides a BOM entry, and when several BOMs pin the same coordinate the first declared wins. Under the pinning modes a BOM behaves like its pins: `strict` requires a checksum on the BOM reference itself (the fetched file is content-verified before its entries are trusted) and accepts BOM-provided hashes as pins for the closure check, `versions` keeps the pinned versions but skips all checksum verification, and `ignore` floats everything except classifier qualifiers and the managed version of a dependency whose declaration carries no version of its own (there the management is the only version source, so dropping it could only break resolution, never float it - the test engine's `junit-platform-console`, versioned solely through `dependencyManagement`, is the archetype). Manifests writes the declarations into a per-module `boms.properties` (`bom///` references to fetch, `entry/...` lines already expanded from local files), which merges across modules and feeds the resolution pass beneath `versions.properties`. Publishing a BOM is a matter of placing the properties file at the module repository's standard versioned path; the local `~/.jenesis` repository is consulted first, so a locally exported BOM resolves before the remote one. A module emits such a BOM automatically when a `bom.properties` build-configuration file is present: the modular layouts render the module's full resolved closure (a version and checksum per coordinate, keyed group-less like a hand-written BOM) and `export` publishes it into the Jenesis repository as `//.properties` next to the module jar, ready to be consumed by `@jenesis.bom ` elsewhere. The emission is gated on the file's presence, mirroring how `packaging.properties` gates `jpackage`, and is confined to the Jenesis repository - the Maven export never carries it. The `pin` goal is BOM-aware along two independent dimensions, `-Djenesis.pin.bom=keep|flatten` and `-Djenesis.pin.checksum=true|false`. Under `keep` (the default) a dependency whose resolved version - and checksum, when checksums are pinned - is already supplied by a BOM entry gets no `@jenesis.pin` line, and an existing unguarded line for such a covered coordinate is removed as redundant (a pin that genuinely overrides the BOM forces a different resolution, so it is never "covered" and is refreshed as usual); the BOM itself is pinned instead: every versioned `@jenesis.bom` reference has its content hash written back onto the declaration (guard matching selects the line to refresh exactly as for pins), while floating references stay floating and local files need no pinning. Under `flatten` the unguarded `@jenesis.bom` declarations are removed and the module's resolved closure is pinned in full - the inverse migration - with platform-guarded BOM declarations failing the flatten, since a local repin only observes the local platform's variant. `-Djenesis.pin.checksum=false` switches every written value (pins and BOM references alike) to versions without hashes, and coverage under `keep` then requires only a version match. The resolved BOM data reaches the pin step the same way the dependency closure does: `Dependencies` records the fetched BOM files and the merged entry map in its output `boms.properties`, and `Inventory` mirrors them as indexed `prefix.bom.` entries. - The **inferred documentation chain** (`InferredDocumentationChainModule`) mirrors the compiler chain for API documentation. A `scan` step walks the module's `sources/` and records which languages are present (`.java`, `.kt`, `.scala`, `.groovy`); a `document` sub-module then wires the documentation tools and an `aggregate` step that merges their output into a single `javadoc/` tree (archived into the module's `-javadoc.jar`). The tools are resolved in their own scopes like the compilers: Kotlin through Dokka in its **HTML output format** (`dokka-cli` plus `dokka-base` and `analysis-kotlin-descriptors` on the plugins class-path) - the format Dokka recommends and the one most Kotlin libraries ship in their `-javadoc.jar`, Scala through `scaladoc` (fed the compiled `.tasty` classes since scaladoc reads tasty rather than source), Groovy through `groovydoc` (in source-path plus package-name mode so packages render correctly), and Java through the JDK's `javadoc` (in class-path mode, skipping `module-info.java`). The layout follows what each tool can cover: Dokka also documents Java sources and `groovydoc` also documents Java, but `scaladoc` documents only Scala. So when **one tool can document every language present**, only that tool runs and renders at the archive root - Java + Kotlin is a single Dokka document, Java + Groovy a single groovydoc document, and a single-language module is just that language's tool. Only when the mix is **incompatible** (Java + Scala, or three or more languages, where no single tool covers everything) does `javadoc` render the Java at the root as the baseline and each remaining language render into its own subfolder (`dokka/`, `scaladoc/`, `groovydoc/`). Like Dokka, both `groovydoc` and `scaladoc` float `RELEASE`; keeping the documentation tool in sync with the compiler is the user's responsibility through the usual pins, not something the build detects from the resolved compiler jars. The chain is best-effort: every tool tolerates a non-zero exit so a documentation tool that fails never fails the build, and `aggregate` always guarantees a root `index.html` (linking to any per-language subfolders that rendered) so the produced `-javadoc.jar`, a Maven Central prerequisite, is never empty. - The **inferred code-quality chains** bring the same zero-config activation to static-analysis and formatting tools, split by what they inspect. `InferredByteCodeQualityModule` covers tools that read compiled **class files** (SpotBugs), and `InferredSourceCodeQualityModule` covers tools that read **source files** (Checkstyle, PMD, detekt, ktlint, Scalastyle, scalafmt, CodeNarc). Activation is **config-file-only**: a tool is wired if, and only if, its conventional configuration file is present in the project. Each module checks its inherited input folders for the trigger file, so no extra wiring or source-tree knowledge is needed. The triggers are `spotbugs-exclude.xml` (or `spotbugs.xml`) for SpotBugs, `checkstyle.xml` for Checkstyle, `pmd.xml` for PMD, `detekt.yml` for detekt, `.editorconfig` for ktlint, `scalastyle-config.xml` for Scalastyle, `.scalafmt.conf` for scalafmt, and `codenarc.xml` for CodeNarc. Like the compilers, each tool resolves in its **own dependency group** (named after the tool) so its runtime never mixes with the project's compile classpath, floats `RELEASE` (pinnable through the usual `@jenesis.pin` tags), and runs in a forked JVM against the compiled `classes/` or the bound `sources/`. Each tool **writes its report and does not fail the build by default** (report-only); calling `.strict()` on a tool makes a non-zero tool exit fail the build, gated through `acceptableExitCode`. A tool whose language is absent self-skips (detekt does nothing when a project has no `.kt` sources), so an unrelated trigger file never forces analysis. Each tool is a self-contained `BuildExecutorModule` following the compiler-module shape (`required` -> `dependencies` -> `check`). `InferredMultiProjectAssembler` wires both chains into every build by default - the source linters as a `check` step before compilation, SpotBugs as the `binary` step's `validator` once classes exist - but because activation is config-file-only, a project with no trigger files pays nothing. A discovered tool can be switched off without deleting its configuration file by setting its property to `false`: `jenesis.source.` (Checkstyle, PMD, detekt, ktlint, Scalastyle, scalafmt, CodeNarc) or `jenesis.validator.spotbugs`, each defaulting to `true`, with a matching `.(boolean)` wither that overrides the property in code. Source formatting is covered by the separate inferred formatting chain described below. Mutation testing follows the same config-file-discovery convention: a `pitest.properties` in a module wires a `mutate` step (`PiTestModule`) that resolves PIT and its JUnit 5 plugin in a `pitest` group, seeds faults into the compiled classes and re-runs the tests against each mutant, and reports under `reports/pitest/` which mutants survived; the JUnit 5 plugin's version is taken from the project's resolved `junit-platform` so it always matches the test framework. The [`pitest`](demo/demo-25-pitest/README.md) demo shows it killing both mutants of a covered method. The detekt runner targets the 1.x main class (`io.gitlab.arturbosch.detekt.cli.Main`); detekt 2.x relocates it to `dev.detekt.cli.Main`, so the floated `RELEASE` and runner main class want confirming against the resolved jar. - The **inferred formatting chain** (`InferredSourceFormattingModule`) is the rewriting counterpart to the code-quality chains: where a linter reads sources and writes a report, a formatter reads sources and rewrites them in place. It covers `google-java-format` and `palantir-java-format` for Java (selected with a `javaformat.properties` file naming `google` or `palantir`), `ktlint -F` for Kotlin, and `scalafmt` for Scala. The assembler wires this chain by default too: the Java formatter activates from a `javaformat.properties` file the same way ktlint and scalafmt activate from their linter config files (`.editorconfig`, `.scalafmt.conf`), and a formatter runs whenever its sources are present and self-skips otherwise. Each of the three switches off with `jenesis.format.java` / `jenesis.format.ktlint` / `jenesis.format.scalafmt` (each default `true`). Each tool resolves in its own dependency group, floats `RELEASE`, and runs in a forked JVM; the two Java formatters pass the `--add-exports jdk.compiler/...` set they need on a modern JDK (palantir additionally exports `com.sun.tools.javac.main`). Because a formatter mutates the developer's source tree, it cannot lean on the build's normal bound-input change detection (which would re-format the whole tree on any edit and never converge). Instead each format step keeps its own `formatted.properties` in its persistent step folder, recording a SHA-256 per source file as of the last format. On each run it re-formats only the files whose content differs from that record and then rewrites it; a file already in formatted state is skipped without forking the tool, and a file the formatter cannot change (a parse error) is recorded as-is and not retried until it next changes. `.verify(true)` turns a formatter into a CI gate instead: google-java-format and palantir use `--dry-run --set-exit-if-changed`, ktlint drops `-F`, and scalafmt uses `--test`, so a file that is not already formatted fails the build while no source file (and no hash state) is written. The assembler wires every formatter in verify mode by default, so a normal build never rewrites the developer's sources; `-Djenesis.format.rewrite=true` flips the whole chain to rewriting (it sets `.verify(false)` on each formatter). Groovy formatting is deferred: no `RELEASE`-floatable Maven JAR formatter exists for it (CodeNarc is a linter, npm-groovy-lint is Node-based, and the groovy-eclipse formatter is only a community single-file `-in`/`-out` jar). Layouts always combine their built-in repositories and resolvers (e.g. a Maven default for `MAVEN`, a chained Jenesis module repository for `MODULAR`) with any user-provided ones. The merged map then has each sub-module's `assign` URI prepended inside `MavenProject.make` / `ModularProject.make` and is handed to the assembler per call. User entries with the same key override the layout default. | Layout | Pipeline | Demo | | -------------------- | ----------------------------------------------------------------------------------------- | ---------------------- | | `Layout.MAVEN` | **Input: `pom.xml`. Output: classic JAR + `pom.xml`.** `MavenProject` scan + per-project `JavaToolchainModule` + per-module `Pom` step + `MavenRepositoryStaging` + `MavenRepositoryExport` | `demo/demo-01-java-pom` | | `Layout.MODULAR` | **Input: `module-info.java`. Output: modular JAR (no `pom.xml`).** `ModularProject` over `JenesisModuleRepository` (public overlay, cached under `.jenesis/artifacts/`) with `JenesisModuleRepository.ofLocal()` prepended + per-project `JavaToolchainModule` + `ModularStaging` + `JenesisModuleRepositoryExport` | `demo/demo-02-java-modular` | | `Layout.MODULAR_TO_MAVEN` | **Input: `module-info.java`. Output: modular JAR + `pom.xml`.** `ModularProject` against a `MavenDefaultRepository` driven by `MavenModuleResolver`, which fetches each declared module's `:pom` artifact from a permissive `JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT)` (with `JenesisModuleRepository.ofLocal()` prepended, so a locally exported or published module resolves before the worker overlay - matching `Layout.MODULAR`) to translate the module name into its Maven coordinate, then resolves through `MavenPomResolver` as if the project were a single synthetic POM declaring those coordinates as its ``. No `module-info.class` is ever read; `` is taken solely from the `@jenesis.pin` tags (a pin on a non-declared module is fetched the same way and registered as a managed dependency), never hoisted from the declared modules' own POMs. The discovered first-layer POMs seed the resolver cache, so they are not re-fetched from Maven Central. Per-module `Pom` step on top of the assembler; since the jars are genuine modules, both `stage` and `export` are modules with aligned `maven`/`modular` sub-steps - `MavenRepositoryStaging` + `ModularStaging`, then `MavenRepositoryExport` into the local Maven repository + `JenesisModuleRepositoryExport` into the local Jenesis module repository | `demo/demo-02-java-modular` | | `Layout.AUTO` (default) | Detection: a root `pom.xml` → `MAVEN`; else any `module-info.java` under the root → `MODULAR_TO_MAVEN`. Trees rooted at a nested `.jenesis.skip` marker are skipped. Falling through throws. | - | `MODULAR_TO_MAVEN` translates each `requires` directive into the declaring module's Maven coordinate (discovered from its `:pom` in the overlay) and resolves the transitive closure through `MavenPomResolver` against a `MavenDefaultRepository`, exactly as if the project had declared a `pom.xml` listing those coordinates as its ``. Versions therefore follow Maven's nearest-wins rules and dependency management rather than the Java module system's single-binding requirement, and `` comes solely from the project's own `@jenesis.pin` tags, never from the declared modules' own POMs. The resolver reads no `module-info.class` at all; it also does not check that every transitive jar carries a `module-info.class` or a manifest `Automatic-Module-Name`, and Maven coordinates do not encode a Java module name, so the resolved set may include plain classpath jars or coordinates whose filename is not a legal automatic module name. The artifact may still be module-path-consumable in practice; the layout simply does not prove it. For this reason the layout omits `prefix.module` from `inventory.properties` and `Execute` launches the staged jar on the classpath rather than via `--module-path`. `MODULAR` is the layout that resolves only against a module-name registry and so guarantees a module-path-consumable closure. All three concrete layouts run a `stage` target that depends directly on `BUILD` and materializes the staged tree(s) by walking every per-module `inventory.properties` the assembler produced. The staging and the matching `export` shape differ by layout: - `MAVEN` stages a single `MavenRepositoryStaging` step nested under a `maven` sub-step, so its output lands under `target/stage/maven/output/` - the same absolute path MODULAR_TO_MAVEN uses for its Maven layout, so the staged tree sits at a consistent location regardless of layout. For each main module it parses `prefix.pom` for `groupId` / `artifactId` / `version` and hardlinks the artifacts as `///-.` (suitable for upload to a Maven repository). Test variants (those whose inventory carries a `prefix.test=` marker) are routed onto the main coordinate with a `-tests` classifier, and the test module's `pom.xml` is parsed for its dependencies, which are appended to the staged main POM with `test`. Its `export` is a matching `MavenRepositoryExport` under `export/maven` that copies the staged tree into the local Maven repository (default `~/.m2/repository`, overridable via `jenesis.maven.local` (or the `MAVEN_REPOSITORY_LOCAL` environment variable)) with the right `maven-metadata-local.xml` and `_remote.repositories` markers. - `MODULAR_TO_MAVEN` - whose jars are genuine modules (it builds with the module marker set) - stages and publishes to *both* repositories, so its `stage` and `export` are each a small module with matching `maven` and `modular` sub-steps that line up one-to-one. `stage/maven` runs the same `MavenRepositoryStaging` (Maven layout under `target/stage/maven/output/`) and `stage/modular` runs `ModularStaging` (module layout under `target/stage/modular/output/`, see the next bullet); `export/maven` then runs `MavenRepositoryExport` over `stage/maven` into `~/.m2/repository`, and `export/modular` runs `JenesisModuleRepositoryExport` over `stage/modular` into the local Jenesis module repository (default `~/.jenesis`, overridable via `jenesis.module.local` (or the `JENESIS_REPOSITORY_LOCAL` environment variable)). - `MODULAR` stages a single `ModularStaging` step nested under a `modular` sub-step, so its output lands under `target/stage/modular/output/` (again matching MODULAR_TO_MAVEN's module layout, and its `export` runs as `export/modular`). For each module's inventory it reads `prefix.module` (the Java module system module name) and the optional `prefix.version`, then hardlinks the artifacts as `/.jar` (plus `-sources.jar` / `-javadoc.jar` siblings when produced). When `prefix.version` is present, the version is inserted as one extra path segment: `//.jar`. There is no `pom.xml` to anchor a Maven coordinate. The follow-up `JenesisModuleRepositoryExport` step copies that staged tree into the local Jenesis module repository (default `~/.jenesis`, overridable via `jenesis.module.local` (or the `JENESIS_REPOSITORY_LOCAL` environment variable)), preserving the same `[/]/` shape. When a module is versioned, its files are *also* mirrored to the unversioned `/` root so the module root always reflects the most recently built version (a subsequent build of the same module overwrites the root regardless of which version it produces). Each target directory written in a run is cleaned of pre-existing regular files before the new ones are linked in, so a build that no longer produces a `-javadoc.jar` does not leave a stale one behind; sibling version directories (e.g. `/0.9/` while exporting `/1.0.0/`) are untouched. In either modular layout the staged tree includes a produced `.jmod` (the `jmod` step's output, which `ModularStaging` hardlinks beside the jar), so `export` publishes the `.jmod` to `~/.jenesis` alongside the jar without any extra step. A downstream Jenesis project then consumes it by resolving the `:jmod` coordinate, which `JenesisModuleRepository` serves from the published `.jmod` and otherwise falls back to the jar - the cross-project half of the link-time propagation described in the `jmods/` row of the conventions table. The producer publishes the jmod automatically (whenever the `jmod` step ran); only the consumer opts in, by requesting the `:jmod` qualifier rather than the default jar. Run `java build/jenesis/Project.java stage` to materialize that tree (it's the canonical entry point for release publishing - see [The stage step](#the-stage-step) for the full release pipeline). ### Configuration file locations Build plug-ins activate on the **presence** of a configuration file, and every lookup walks one ordered folder list; the first folder that carries the file wins (only `spdx.properties` merges across all of them). The list is composed of the per-module locations, most specific first, followed by the project-wide locations: | Rank | Location | Layouts | Scope | |---|---|---|---| | 1 | `src/main/build.jenesis/` or `src/test/build.jenesis/` under the pom root | MAVEN | the pom's main or its test module alone | | 2 | `build.jenesis/` next to the `pom.xml` | MAVEN | both of the pom's modules | | 1 | `META-INF/build.jenesis/` under the module sources | MODULAR, MODULAR_TO_MAVEN | the one module (test modules are separate modules with their own folder) | | 3 | the `jenesis.project.configuration` folders | all | project-wide, defaulting to `build.jenesis/` under the project root | Profile subfolders (`//` for each active `jenesis.project.properties` profile) rank before all plain folders, so an active profile's folder beats even a module's unscoped one. The bare project root is deliberately not a default location: a conventionally named file that an editor or a teammate drops at the root (an `.editorconfig`, a `checkstyle.xml` for the IDE) must not silently alter the build - configuration activates only from an explicitly named `build.jenesis/` folder, or from locations the project opts into via `jenesis.project.configuration`. What lives in these folders - presence activates, contents configure: - **Packaging and output**: `packaging.properties` (jmod, jlink, jpackage, native), `sbom.properties`. - **Code quality** (the tool is inferred from its file): `checkstyle.xml`, `pmd.xml`, `spotbugs-exclude.xml`, `detekt.yml`, `codenarc.xml`, `scalastyle-config.xml`. - **Formatting**: `javaformat.properties` (google or palantir), `.editorconfig` (ktlint), `.scalafmt.conf`. - **Compliance**: `licensing.properties`, `vulnerability.properties`. - **Test observability**, read by the test side (hence `src/test/build.jenesis/` in the MAVEN layout): `jacoco.properties`, `graal.properties`, `pitest.properties`. - **Forked-tool arguments**: `process-.properties` (javac, kotlinc, scalac, and so on). - **License normalization**: `spdx.properties`, merged from every location rather than first-hit. - **BOM emission** (modular layouts): `bom.properties`. A few locations sit deliberately outside this search: `[/]bom-.properties` BOM files resolve from the `jenesis.project.boms` locations (defaulting to the configuration locations) and are never profile-resolved; the `jenesis.properties` cascade consists of the project root file, the user-global `~/.jenesis/jenesis.properties` and `jenesis-.properties` profile files resolved relative to whichever file declares them (precedence: explicit `-D` over profiles over the project file over the user-global one); `jenesis.project.metadata` names explicit files rather than searched folders; and pins live in the sources themselves, as `@jenesis.pin`/`@jenesis.bom` tags in `module-info.java` or `` comments in a `pom.xml`'s ``. ### Watching for changes Set `-Djenesis.project.watch=true` to keep the process alive and rebuild on every source change: java -Djenesis.project.watch=true build/jenesis/Project.java The first build runs as usual; Jenesis then registers a `WatchService` over 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 changed (a no-op change settles in well under a second). The watch excludes the output folders (`target/` and the configured cache) and dot-directories, so the build's own writes never trigger a rebuild. Module selectors still apply, so `-Djenesis.project.watch=true build/jenesis/Project.java +mymodule` watches and rebuilds just that module's subgraph. Press Ctrl+C to stop. Setting `jenesis.project.watch=true` in a `jenesis.properties` file makes watch a project's default. **Narrowing tests while watching.** Watch mode already skips a module's test step whenever none of that module's inputs changed; `-Djenesis.test.incremental` adds the finer step of re-running, within a module that *did* change, only the test classes a change can reach. The test step builds a class-to-test dependency graph from the compiled bytecode (the test classes and the code under test, whether it arrives as a directory or inside a resolved jar) and keeps a per-class content hash beside its own output; on the next run it diffs those hashes, takes the changed classes, walks the graph to the tests that reach them, and passes only those to the runner with `--select-class`. A change that reaches no test runs nothing, and any non-class change (a resource, a dependency) falls back to the full suite. This is a development-loop optimisation for `watch`, not a correctness gate: static selection cannot see reflection or other indirect couplings, so continuous integration should keep running the whole suite with selection off. The [`test-selection`](demo/demo-24-test-selection/README.md) demo shows it editing one class and re-running only that class's test. ### Printing the dependency tree Run the `dependencies` selector to print each module's dependency tree, the way `mvn dependency:tree` / `gradle dependencies` do: java build/jenesis/Project.java dependencies `dependencies` is a top-level selector (a sibling of `stage`/`export`/`pin`) that always runs when requested. The resolver returns a `Resolution` in a single pass: the flat materialized artifacts, the parent -> child resolution graph (each `Edge` carries the property-file key, the discovered version and scope, and a `followed` flag that is `false` on a dedup re-encounter), and a `Vertex` per resolved coordinate (its negotiated version, module name and automatic flag, and declared licenses). The `Dependencies` step flattens the artifacts into `dependencies.properties` and writes the graph to `graph.properties` and the dependency licenses to `licenses.properties` beside it on every resolve. As it writes them it **normalizes and classifies** each declared license: the declared name is matched case-insensitively against a comprehensive built-in alias table to a canonical SPDX identifier (`Apache-2.0`, `EPL-2.0`, ...), and that identifier against a category table (`permissive`, `weak-copyleft`, `strong-copyleft`, `network-copyleft`, `public-domain`); both are stored in the sidecar alongside the raw name and url, so every downstream consumer reads the same normalized result rather than re-deriving it. The two tables can be extended through `spdx.properties` files in the configuration location (resolved through the same `configurations(...)` search as the other config files, so every project, profile and per-module folder that carries one contributes and all of them merge). The located files are bound per module and travel as a `ProjectModule.spdx()` key-set - a first-class descriptor input alongside `sources`/`manifests`/ `artifacts`, propagated exactly the way `versions.properties` rides `sources()`. That means the extended tables reach **every** `Dependencies` step, not just the top-level dependency tree: the per-module resolves and the assembler's own resolves (`InternalModule`, `TestModule`, the tool/compiler/documentation modules) all classify against the same tables. A file uses one prefixed key space so the two kinds of entry stay connected: `alias/=` **appends** an alias, `category/=` **appends** a classification (standard properties, spaces in a key escaped as `\ `); supplying one is rarely necessary. The name is distinct from the compliance `licensing.properties` (the `allowed`/`denied` policy), which is a separate concern. The `Inventory` step surfaces each module's `graph.properties` and `licenses.properties`, and the `dependencies` step (`Tree`) reads them back, reconstructs the graph per `/`, and renders it through a `DependencyTreeReport`. Each node shows the version each parent requested, the negotiated version inline when it differs (`[1,2] -> 2`), the Maven scope, any module metadata, and the normalized license identifier inline (`{Apache-2.0}`, falling back to the raw declared name when unrecognized); not-followed duplicates are dimmed and marked `(*)`, and a per-declared-dependency colour gradient tints the tree connectors. A `Resolved dependencies` list of negotiated versions follows each tree. For `MODULAR_TO_MAVEN`, each module's resolved Maven coordinate is what the tree shows (e.g. the `org.slf4j` module resolves to `maven/org.slf4j/slf4j-api/2.0.16`). Below the trees the step prints a colour-coded **summary** over the project's distinct resolved dependencies. It opens with a tally of how many distinct licenses the project implies and, when any dependency declares more than one, how many do. Then a `Licenses` distribution (each SPDX identifier with its count, percentage and a proportional bar, coloured by category) - a dependency that offers several licenses is counted under its **most permissive** one; a `Permissiveness` distribution that rolls those choices up by category (`permissive`/`weak-copyleft`/`strong-copyleft`/`network-copyleft`/`public-domain`/`unknown`, most to least permissive); and a `Modules` breakdown of how many dependency jars are `named` (explicit) modules, `automatic` modules, or `non-modular` plain jars. The same `graph.properties` feeds the SBOM: the `Sbom` step - wired per module by `InferredMultiProjectAssembler` by default, the format chosen by an optional `sbom.properties` (`format=json|xml`, `format=none` to disable) and suppressed by `-Djenesis.sbom.cyclonedx=false` - reads it and emits the CycloneDX `dependencies` array (`ref`/`dependsOn`, each component carrying a `bom-ref`) alongside the components and licenses, so the emitted bill of materials records the resolved dependency relationships, not just a flat component list. Because the licenses were already normalized at extraction, the `Sbom` step and its `CycloneDx` emitter carry no license logic of their own: each license is emitted as a CycloneDX `license.id` when the stored identifier is a recognized SPDX id (a default set, overridable through the `CycloneDx.identifiers(...)` wither) and otherwise as a free-text `license.name` with url. The document is embedded in the jar (`META-INF/sbom/`), collected as a `stage/reports/sbom/` report, and attached to a staged Maven repository as `--cyclonedx.` for publication - see the [`sbom`](demo/demo-12-sbom/README.md) demo. ### Generating IDE metadata The `ide` selector turns the persisted per-module inventories into editor project files, the way `gradle idea`/`gradle eclipse` do: java build/jenesis/Project.java ide Jenesis generates these static project files directly rather than implementing a [Build Server Protocol](https://build-server-protocol.github.io/) (BSP) server. BSP is the JSON-RPC alternative - the build tool runs as a server that an editor queries live for module structure, classpaths, and compile/test actions, instead of writing files the editor reads. It is the more principled long-term shape, but client support across the three editors targeted here is still immature: IntelliJ IDEA treats BSP as a secondary import path behind its native project model, the Eclipse BSP integration is partial, and the VS Code Java extension does not speak BSP at all (it drives its own JDT-based model). A BSP server would therefore reach only a subset of these editors, and only through their least-exercised code paths, whereas every one of them reliably consumes the static metadata files it has read for years. Direct generation is the simpler and more portable choice today; should BSP client support mature, a `bsp` server step could be added alongside these generators without disturbing them. `ide` is a top-level module (a sibling of `stage`/`export`/`pin`/`dependencies`) wired on every layout. Like `pin`, it depends on `BUILD` and writes back into the project tree rather than under `target/`, so the generated files land where each editor expects them. It groups three steps - `ide/idea`, `ide/vscode`, and `ide/eclipse` - and selecting `ide` runs all three; a sub-step name (e.g. `java build/jenesis/Project.java ide/idea`) generates just one editor's files. The `Ide` step reads each module's `inventory.properties` and recovers the module's source folder (`prefix.path`), its resolved dependency jars (`prefix.dependency`, self-anchored under `target/`), and its own published coordinates (`prefix.identity`). A dependency whose coordinate matches another module's identity is emitted as an inter-module project reference rather than a jar library, so a multi-module import links the modules to each other instead of to their stale built jars; every other coordinate becomes a library entry pointing at the resolved jar. Source roots are discovered by probing the conventional locations under each module folder (`src/main/java`, `sources`, `src`, and the language variants, plus `tests`/`test`/`src/test/...` for test sources), falling back to the module folder itself when sources sit directly in it (the modular layout). Each step writes the native format for its tool: - **`ide/idea`** writes one `.iml` per module into its folder, plus `.idea/modules.xml` (registering every `.iml`) and `.idea/misc.xml` (pinning the project SDK to the building JDK's feature release). Source folders are marked `isTestSource`, sibling modules become `type="module"` order entries, and libraries become `module-library` entries. - **`ide/eclipse`** writes a `.project` and `.classpath` per module. Test source folders carry the JDT `test` attribute, sibling modules become `kind="src" path="/"` entries, libraries become `kind="lib"` entries, and the JRE container plus a `bin` output folder complete the classpath. - **`ide/vscode`** writes a single `.vscode/settings.json` at the project root, aggregating every module's source roots into `java.project.sourcePaths` and every external jar into `java.project.referencedLibraries` (paths relative to the workspace root where they fall under it). Because the files are generated from inventories, `ide` regenerates them on every invocation; rerun it after adding or removing a module or dependency to refresh the editor's view. Like `pin`, the generated files can also be edited by hand afterwards - `ide` overwrites them on the next run. ### Running inside Docker Set `-Djenesis.project.docker=true` to run the entire build inside a throwaway container instead of directly on the host JVM: java -Djenesis.project.docker=true build/jenesis/Project.java A minimal image is built on demand the first time and cached for subsequent runs. To target a different image, add `-Djenesis.project.docker.image=`. Every location the project is configured with is represented inside the container at its host path: the project root (writable), the JDK and the local Maven/Jenesis repositories (read-only, with `MAVEN_REPOSITORY_LOCAL`/`JENESIS_REPOSITORY_LOCAL` forwarded so the in-container JVM finds them despite its different home), out-of-root `target`/`artifacts` locations (writable), out-of-root configuration and BOM folders and the folders of `jenesis.project.metadata` files (read-only), and an out-of-root `jenesis.project.cache` or `file://` `jenesis.cache.uri` cache (writable, created on the host first so it is not left root-owned). Anything else the build needs from outside the root - a `build/jenesis` symlinked to a shared engine checkout, a sibling source tree, a generated-sources directory - is invisible inside it. Add those paths with `-Djenesis.project.docker.mount=[:],...`: a bare `host` is mounted at the **same** path inside the container (`host:host`), which is what a symlink or absolute path reference needs to resolve, while `host:container` remaps it. These mounts are **read-only** (the build should not write outside its own tree); use `-Djenesis.project.docker.mountWritable=[:],...` for the rare case that the build must write to a host path outside the project root. Relative host paths are resolved against the project root, and several mounts are comma-separated. For example, a project whose `build/jenesis` is a symlink into `../shared/sources` builds in a container with `-Djenesis.project.docker.mount=../shared/sources`. By default no host environment is forwarded into the container. Pass selected variables with `-Djenesis.project.docker.env=[=],...`: a bare `name` forwards the host's current value of that variable, while `name=value` sets it explicitly. This is the channel for build inputs that legitimately live in the environment (a private-repository token, a proxy setting), and is deliberately opt-in so ambient host secrets do not leak into the build by default. ### Running a module's main entry `build/jenesis/Execute.java` is a companion launcher to `Project.java`. It runs the build first, finds the module that declares a `@jenesis.main` (in its `module-info.java`) or `` (in its `pom.xml`), and spawns a child `java` process for it, forwarding any trailing arguments to the program: java build/jenesis/Execute.java arg1 arg2 If exactly one module in the project declares a main, Execute selects it implicitly. If several do, it aborts and lists the candidates; pass `-Djenesis.execute.module=` (the same path you would use after `+` in a build selector) and `-Djenesis.execute.mainClass=` to specify the target explicitly. Doing so also narrows the build to that module's subtree, skipping siblings: java -Djenesis.execute.module=tools \ -Djenesis.execute.mainClass=org.example.tools.Cli \ build/jenesis/Execute.java --help Execute can also run the launched program inside a container, independently of whether the build itself was dockerised. Set `-Djenesis.execute.docker=true` to dispatch the final `java -m /
` (or `java -cp ...
`) invocation through Docker, with `-Djenesis.execute.docker.image=` overriding the image. `-Djenesis.execute.docker.mount` (read-only) and `-Djenesis.execute.docker.mountWritable` (read-write) add bind mounts, and `-Djenesis.execute.docker.env=[=],...` forwards host environment variables - all with the same syntax as the `jenesis.project.docker.*` flags above. The build runs as usual (locally, or in `jenesis.project.docker.image` if set), and only the launch step crosses the container boundary, so the build image and the runtime image can differ. Architecture ------------ The lowest primitive is a `BuildStep`, a single unit of work that reads from a set of input folders and writes into a fresh output folder. It is a functional interface: ```java CompletionStage apply(Executor executor, BuildStepContext context, SequencedMap arguments); ``` Each invocation is handed a `BuildStepContext` and a map of predecessor outputs. The context holds three folder slots: - `next`: the folder this invocation writes into. It is created fresh for every run; the step never modifies any other folder. - `previous`: the same step's output folder from the prior run, or `null` on a first run. A step can read it to decide what to copy or hard-link instead of regenerating, but it must not write into it. - `supplement`: scratch space tied to the step's lifetime, available for intermediate files the step doesn't want to publish in `next`. The `arguments` map carries one `BuildStepArgument` per registered predecessor. Each argument exposes the folder to read from (`argument.folder()`) and a per-file checksum status (`ADDED`, `ALTERED`, `REMOVED`, `RETAINED`) computed against the previous run. The default `shouldRun(...)` re-runs the step when any input has changed; a step can override it to express finer-grained dependencies (e.g. `Bind` only re-runs when files matching its bound paths changed). Steps are organised into a graph by `BuildExecutor`: - `addSource(name, path)` registers an external folder as an input. - `addStep(name, BuildStep, predecessors…)` adds a step whose `arguments` will be populated from the named predecessors. Predecessors are addressed by their registered names; cross-module references use the `../` prefix (`BuildExecutorModule.PREVIOUS`) to climb out of the current sub-graph. - `execute(selectors…)` runs the graph on a virtual-thread executor, scheduling each node as soon as its predecessors have completed. With no selectors, the full graph runs. Otherwise each selector is a slash-delimited path of identities (`module/step`) that restricts execution to the named steps and their preliminaries; `:` matches any one path segment and `::` matches any depth (zero or more). Wildcards are lenient - branches without a match are silently skipped - while a literal path that doesn't resolve throws. Once a step is matched, its **transitive preliminary closure** runs unconditionally (no further selector filtering) so its inputs are real folders, not lenient-skipped placeholders. Modules along the path are different from steps here: a module's `accept(...)` always runs (modules aren't cached), and `accept` is allowed to read its predecessor folders to wire its sub-graph dynamically. So whenever a module is reached by any selector - including via lenient `::` propagation - its step preliminaries are pinned and run normally (cache-checked but not lenient-skipped), guaranteeing those folders exist when `accept` reads them. Sibling modules whose subtree contains no match still have their `accept` invoked and their declared step preliminaries run; the engine can't determine "no match here" without descending, since module substructure is registered by `accept` itself. In practice this is a hash check per preliminary on a warm cache. If you know the path you want, prefer literal selectors (`module/step`) over `::/leaf` to avoid that residual work on unrelated subtrees. A `BuildExecutorModule` is a sub-graph factory, also a functional interface, with `accept(BuildExecutor, inherited)` populating a nested `BuildExecutor` with its own steps and (transitively) its own sub-modules. The `inherited` map exposes the predecessor folders the parent passed in, addressed under their `../`-prefixed identifiers. Modules can rename their published outputs by overriding `resolve(...)`. Composing steps into modules turns commonly-recurring patterns (compile + jar + test, scan a multi-project tree, …) into reusable units that take only their inputs as configuration. Unlike steps, modules are not cached: `accept(...)` runs on every build to (re-)register its sub-graph, and only the registered steps are then content-hashed and considered for skipping. Logic that lives inside the module body itself - file scans, classpath assembly, conditional step wiring - therefore executes unconditionally on every run; wrap it in a step if you need it skipped on unchanged inputs. Three properties of the model give incremental builds and reproducibility for free: - **Each step's output folder is immutable once produced.** A step only ever writes into its own `next`; downstream steps see predecessor outputs as read-only inputs. There is no shared mutable state, so a step's result is a pure function of its inputs. - **Inputs and outputs are content-hashed.** Every output folder is checksummed when the step finishes; on the next run, those checksums become the predecessors' input checksums. If they all match and `shouldRun(...)` returns `false`, the step's previous output is reused unchanged. Anywhere along the chain that the hashes diverge (a source edit, an upstream re-run, a different dependency), the affected step (and only the affected step) is re-executed into a fresh `next` folder, which transparently replaces its predecessor. - **Each step's configuration is content-hashed too.** `BuildStep extends Serializable`, and a `BuildStepHashFunction` digests the step's serialized form alongside the output checksums (in `/checksum/step`). When a step is reconstructed with different field values - a different `Jar.Sort`, a different `Resolver`, a different placement function - its hash changes and the step re-runs even if its inputs are unchanged. Configuration that should *not* count as part of the build's identity (a `Repository` that by contract returns the same artifact for the same coordinate, a JDK service factory, a `MavenPomEmitter`) is marked `transient` so it never reaches the digest. Lambdas held by step fields use intersection bounds (` & Serializable>`) at the constructor so the compiler generates them serializable. The hash stream also installs a `replaceObject` hook that substitutes any `java.nio.file.Path` for its `toString()`, making `Path`-typed step fields a first-class part of the configuration hash by design - the JDK's `Path` interface is not declared `Serializable`, so without this substitution any step that held a `Path` would fail serialization. Steps that still hold genuinely non-serializable state throw `NotSerializableException` at hash time so the bug surfaces at the first run instead of silently breaking cache invalidation. Declaring an explicit `serialVersionUID` on a `BuildStep` is the Java-native equivalent of adding a manual version field to the type: it replaces the JVM-computed shape fingerprint with a pinned value the author maintains by hand. The trade-off is real, because the auto-computed UID is the only part of the default serialization stream that tracks method signatures at all. The class descriptor itself records only the class name, flags, non-transient field shape, and superclass chain, never methods or bodies. Pinning a UID therefore removes the cache's only handle on behavioural changes: a step whose `execute(...)` gains parameters, whose helpers change signature, or whose superclass adds a method then hashes identically to the prior version, and stale outputs may be reused. The author is then responsible for bumping the UID by hand on every behaviour-affecting change. The implicit UID is not perfect either, since it does not recurse into superclasses or interfaces and ignores method bodies, but it catches more accidental drift than a pinned value and is the default `BuildStep` authors should rely on. Pin one only when stream stability across JVMs or compiler versions outweighs the loss of automatic discovery, and treat the value as something you bump by hand thereafter; once an explicit UID is declared the JDK no longer computes the implicit one, and there is no supported way to ask `ObjectOutputStream` what it would have been. The executor places a `.jenesis.skip` marker at the build root so source scanners (`MavenProject`, `ModularProject`) can skip nested builds, stores all per-step state under `target/`, and uses `.jenesis/artifacts/` by convention for cross-build caches such as downloaded module URIs. ### Best practice: communicate through file/folder conventions, not step names A step or module should treat its `inherited` map as an opaque set of **input folders** and discover what to read by looking for files and folders at well-known relative paths inside each input. It should not pattern-match on the keys themselves to infer which predecessor an input came from. The same applies to its outputs: a step writes file and folder layouts that downstream consumers look up by name, never expecting the consumer to know how the step was wired. Concretely: - **Don't filter `inherited.sequencedKeySet()` by step-name patterns.** If a module needs to distinguish two categories of inputs (e.g. compile-side vs. runtime-side), let the caller wire each category to a distinct predecessor or pass an explicit predicate; don't have the module sniff `key.split("/").contains("runtime")` to guess. - **Don't compose `inherited` keys with extra `BuildExecutorModule.PREVIOUS` (`../`) prefixes** to chase a predecessor that lives one level higher than the descriptor states. Instead, do the lookup at the level where the descriptor's path strings apply directly (typically the outer assembler lambda) and capture the result for any inner sub-module that needs it. - **A module's exposed steps must not publish the same file at the same relative path in more than one of them.** Exposing several intermediate steps is *not* a problem by itself - a consumer that doesn't recognise a given file/folder convention just ignores those entries. The problem is when two of a module's exposed steps both write, say, `versions.properties` at the same relative path: a consumer iterating `inherited.values()` and resolving `folder.resolve(BuildStep.VERSIONS)` will find that file twice with possibly different content (typically an early-pipeline placeholder and a later-pipeline refined version), and which one wins depends on iteration order. Override `resolve(String path)` to return `Optional.empty()` for any leaf whose exposure would create such a collision, keeping only the step that holds the **final** state of each file. When a step genuinely rewrites a file in place that an upstream step already exposed under the same name, expose only the downstream leaf; the upstream output stays available to its in-module successor by name but disappears from the module's published map. (`Dependencies` sidesteps this by design: it writes its resolved closure to a distinct `dependencies.properties` rather than overwriting the declared `requires.properties`, so the two never collide.) Leaves whose files don't collide with any sibling can stay exposed unchanged. `ExternalModule` is the strict end of the spectrum: it hides every internal node (`coordinate`, `dependencies`, `external`, `delegate`) and republishes the delegated module's leaves under its own registered name (see [`ExternalModule`](#externalmodule)). - **Define each step-name constant once, at the class that adds the step**, and have all consumers reference that constant. `MultiProjectModule.IDENTIFIER` / `.COMPOSE` / `.MODULE` belong on `MultiProjectModule` because that's the framework that wires those sub-modules; the `dependencies` step name belongs to whoever adds the `Dependencies` step (the single-language modules add it under that name directly). The per-scope sub-module folder names are derived from `DependencyScope.label()` rather than living as separate constants. A class that wants to point at a predecessor's leaf step uses the owner's constant - no separate "same string" duplicate. - **`*.properties` files exchanged between steps in different files should have a documented schema.** The conventional files (`identity.properties`, `module.properties`, `metadata.properties`, `requires.properties`, `versions.properties`, `exclusions.properties`, `dependencies.properties`, `inventory.properties`) are listed in the table below with their produced/consumed keys and value semantics. The filenames live as constants on `BuildStep`; each property key's contract belongs in the README rather than as a magic string scattered across writer and reader sites. - **Paths inside a properties file should be self-anchored: written relative to that file's own folder.** A consumer resolves the path with `.resolve().normalize()` and never depends on the absolute layout of `target/` or on where the file happens to live in the build graph. Writers achieve this by `context.next().relativize(absolutePath)` before storing the value. This is what `process/*.properties` does for command-line path fragments, what `identity.properties` does for assigned artifact paths, and what `inventory.properties` does for `artifact*`, `pom`, and `runtime`. The convention is load-bearing for reproducible builds: it means the same folder tree linked, copied, or mounted under a different absolute prefix continues to work without rewriting any properties file, and a step's output is therefore safe to hard-link into another build's cache, ship between machines, or move between `target/` directories. The inverse - storing absolute paths or paths anchored to some shared root - couples the file's validity to its physical location and breaks the moment the build tree moves. - **A self-anchored path is an identifier, not a content fingerprint, so pair it with a content hash.** The same path value persists unchanged when the bytes it points at change, so a step whose output records only the path will not re-trigger its consumers when an internal artifact is rebuilt in place: the properties file stays byte-identical, its checksum is unchanged, and the stale artifact is silently reused. Any value that is a stable identifier (a path, or a version) must therefore travel with the referenced content's checksum, written in the project's `BuildExecutor` digest as `/` (`BuildStepArgument.checksum`), so the file's content tracks the artifact's content. The step that emits it must also *watch* the artifact (depend on `ARTIFACTS`) so it actually re-runs and picks up the refreshed checksum. `requires.properties` does this for internal sibling-module coordinates (`MultiProjectDependencies` reuses the build executor's own checksum of the resolved artifact rather than writing an empty checksum), and `versions.properties` does it inline as ` `. - **Schema-level scope tokens are matched as literal strings.** The scope segment that leads every dependency key (e.g. `compile`, `runtime`) is an open-ended token set documented in the table below; new steps and producers are free to introduce additional scopes without touching the shared `DependencyScope` enum. The standard producers and consumers use `DependencyScope.COMPILE.label()` / `DependencyScope.RUNTIME.label()` to derive the string, which keeps writer and reader spellings in sync without forcing every participant to depend on the enum (the wire format is the string, not the enum value). Beyond the two everyday scopes a key may carry a tool scope. A javac annotation processor resolves under group `plugin`, scope `plugin`; a language compiler plugin resolves under the compiler's own group (`kotlinc`, `scalac`, `groovyc`) with scope `plugin` (e.g. `kotlinc/plugin//`); and a compiler's own closure resolves under that same compiler group with scope `runtime` (e.g. `kotlinc/runtime//`); `Inventory` keeps every tool group out of the produced module's runtime closure. The general infrastructure (`BuildExecutor`, `BuildStep`) does not enforce a closed token set: only the bundled `MavenProject.make` / `ModularProject.make` wiring and its helpers (`MultiProjectDependencies`, `Pom`) reference `DependencyScope`, and they only consume the scopes they know about. A custom project type or layout that supplies its own `Manifests` step, its own per-scope prepare step, and its own consumer (or skips `Pom` entirely) can introduce additional scopes with no framework-level changes; the `DependencyScope` enum is a convenience for the bundled flow, not a global registry. The exception is **inline sub-modules of the same enclosing module**: a class that adds several sub-modules and steps in its own `accept(...)` may reference its own sub-module/step names by their (private) constants, since the wiring lives in one file and never crosses the module boundary. `ExternalModule`'s references to its inner `EXTERNAL`, `DEPENDENCIES`, `DELEGATE` step names; `MavenProject`'s references to its private `MODULE`, `DEPENDENCIES`, `PREPARE` constants; and `MultiProjectModule`'s references to its `IDENTIFIER`, `COMPOSE`, `MODULE`, `GROUP` sub-module names are all of this shape. Conventional folders and files ------------------------------ Every step writes its output into `context.next()`. The conventions below define the names a step uses for the artifacts it produces and the names downstream steps look for. The canonical names are constants on `BuildStep`; others are declared next to the step that emits them. | Path | Constant | Purpose | | -------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------- | | `sources/` | `BuildStep.SOURCES` | A directory tree of `.java` source files (mirroring their package structure) consumed by compilation and documentation tooling. The same folder name is also the conventional output location for the packaged source jar produced by a `Jar` step with `Jar.Sort.SOURCES`, which writes a single `sources.jar` file alongside the tree at `sources/sources.jar`. A sources jar is not a deployable artifact, so it lives next to the source tree rather than in `artifacts/`. | | `resources/` | `BuildStep.RESOURCES` | A directory tree of non-source files (configuration, message bundles, static assets) that should appear on the classpath alongside compiled classes and be embedded into produced jars. | | `classes/` | `BuildStep.CLASSES` | A directory tree of compiled `.class` files in their package layout, plus any non-source companion files copied verbatim from `sources/`. Forms a class- or module-path entry for downstream compilation, packaging and execution. | | `artifacts/` | `BuildStep.ARTIFACTS` | A flat directory holding **the module's own produced binary jars** (typically just `classes.jar`, emitted here by a `Jar` step with `Jar.Sort.CLASSES`). Fetched dependency jars deliberately do not live here, see `resolved/`. Source jars and documentation jars do not live here either, since they are not deployable binaries, see `sources/` and `documentation/`. Path consumers (`Javac`, `Java`, `Javadoc`, `TestEngine`) walk this folder without filtering by extension, so every file placed here is treated as a class-/module-path entry; only runtime-usable jars belong here, which is why link-time-only `.jmod` files get their own `jmods/` folder. | | `resolved/` | (private to `Dependencies`) | A **private** flat directory holding the **fetched dependency jars** that the `Dependencies` step pulled down for this step (every transitive jar resolved for the configured scope, whether external Maven or a sibling module's binary that was resolved by coordinate). Downstream consumers MUST NOT walk this folder: they locate these jars exclusively through the `dependencies.properties` index (via `Dependencies.select(folder, scope)` / `Dependencies.all(folder)`). For their own produced jars, classpath/module-path consumers (`Javac`, `Java`, `Javadoc`, `TestEngine`) walk `artifacts/` and read the rest of the closure from `dependencies.properties`. | | `javadoc/` | `Javadoc.JAVADOC` | A generated Javadoc tree (HTML, CSS and supporting resources), ready to be archived into a documentation jar or served as static content. | | `documentation/` | `BuildStep.DOCUMENTATION` | Conventional output location for packaged documentation. A `Jar` step with `Jar.Sort.JAVADOC` writes `documentation/javadoc.jar` here, distinct from both the generated tree under `javadoc/` and from `artifacts/` (a javadoc jar is documentation, not a deployable binary). | | `packages/` | `JPackage.PACKAGES` | Conventional output location for native application images and installers produced by the `jpackage` tool via the `JPackage` step (e.g. an `app-image` directory tree or a platform installer named after the configured `--name`). | | `runtime/` | `JLink.RUNTIME` | Conventional output location for a custom runtime image produced by the `jlink` tool via the `JLink` step (a self-contained JDK image tree with `bin/`, `lib/`, `release`, and so on). When the assembler wires it (via a `jlink=true` `packaging.properties`), `Inventory` records it as `prefix.image` and the `STAGE` module's `runtime` step (`ImageStaging`) collects it into `stage/runtime`. | | `native/` | `NativeImage.NATIVE` | Conventional output location for the standalone executable produced by the `native-image` tool via the `NativeImage` step, named after the module's `--name`. When the assembler wires it (via a `native=true` `packaging.properties`), `Inventory` records it as the scalar `prefix.native` and the `STAGE` module's `native` step (`ImageStaging`) collects it into `stage/native` - the native-image analogue of `stage/packages` (jpackage) and `stage/runtime` (jlink). | | `jmods/` | `JMod.JMODS` | Conventional output location for `.jmod` module files produced by the `jmod` tool via the `JMod` step, named `.jmod`. A `.jmod` is the module's own produced binary but, unlike a modular jar, is valid only on a compile- or link-time module path and never at runtime, so it lives apart from `artifacts/` (whose contents are walked onto the runtime path unfiltered) and is consumed by the `JLink` step as `--module-path` entries. When the assembler wires it (via a `jmod=true` `packaging.properties`), `Inventory` records it as `prefix.jmod` and `ModularStaging` hardlinks it alongside the jar in `stage/modular`. Because it is staged next to the jar, the `JenesisModuleRepository` resolves a `:jmod` coordinate qualifier to the published `.jmod` (analogous to `:pom`), falling back to the jar when no `.jmod` was published - so a consumer (e.g. a custom assembler wiring cross-module link-time propagation) can request the link-time form unconditionally. | | `bundle/` | `Bundle.BUNDLE` | Conventional output location for the runnable bundle produced by the `Bundle` step: a single `bundle.zip` holding every jar the module's launcher needs, split into `classpath/` and `modulepath/` folders, plus an `application.properties` carrying a `mainClass` property (and `mainModule` for a modular launcher). Produced only for modules that declare a main class. It is meant as a self-contained input for building a container image or other deployment that runs the module without the JDK packaging tools. | | `reports//` | `BuildStep.REPORTS` | Conventional output location for a build report. Each report-producing step writes its files into a `reports//` subfolder of its `output`: the lint tools into `reports/checkstyle/`, `reports/pmd/`, `reports/spotbugs/`, `reports/detekt/`, `reports/ktlint/`, `reports/scalastyle/`, `reports/codenarc/`; JaCoCo into `reports/jacoco/` (the HTML tree plus `jacoco.xml`); and `JUnitPlatform` into `reports/tests/` (the Open Test Reporting `junit-platform-events-*.xml`, only when `-Djenesis.test.reporting=true`). `Inventory` records each kind it finds as a `prefix.report.` entry, and the `STAGE` module's `reports` step (`ReportStaging`) hardlinks each into `stage/reports///`. A kind absent from a module simply produces no entry, so nothing downstream is staged for it. | | `nativeimage/` | `NativeImage.METADATA` | Conventional output location for **captured GraalVM reachability metadata** (`reachability-metadata.json` and friends). When a `graal.properties` file enables the tracing agent, `NativeImageAgentModule` copies the tracing agent's capture here. This is deliberately **not** a `reports//` report: the metadata is build-internal - `Inventory` records it as the scalar `prefix.nativeimage`, and the package phase's `NativeImageMetadata` router feeds each native image the capture of the test module that names it - rather than a deliverable staged for an external tool. | | `groups/` | `Group.GROUPS` | One `.properties` file per identified group, listing the other groups whose coordinates the group transitively depends on so cross-project wiring can be derived purely from on-disk state. | | `pom/` | `MavenProject.POM` | A mirror of the directory layout of a Maven multi-module project, with each `pom.xml` hard-linked from its original location to give downstream tooling a stable, sandboxed snapshot of the project's POM tree. | | `maven/` | `MavenProject.MAVEN` | One properties file per discovered Maven module (`module-.properties` for the main artifact, `test-module-.properties` for the test artifact), holding the parsed coordinate, source/resource directories, packaging and dependency list extracted from a single `pom.xml`. | | `identity.properties` | `BuildStep.IDENTITY` | `/` keys (e.g. `maven/groupId/artifactId/[type/[classifier/]]version` or `module/`) mapped to either an empty value (artifact not yet built; identifies the project's own coordinate) or the absolute filesystem path of an already-built jar. | | `requires.properties` | `BuildStep.REQUIRES` | `///` keys (the `identity.properties` `/` shape with leading group and scope segments; the group names the resolver closure the producer assigns - `main` for the project's own `compile`/`runtime` dependencies, the tool name for each compiler/plugin closure - so every scope sharing a group is mediated into one shared version tree), mapped to either an empty value (no integrity validation requested) or an `/` content checksum that `Dependencies` verifies against the fetched artifact (mismatch fails the build). Checksums are pinned in source by the user in a single canonical place: a `` comment inside a `` `` element (the value propagates to whichever direct or transitive dependency resolves to that coordinate), or an `@jenesis.pin // /` Javadoc tag in `module-info.java`. Only the project's own `` (and `@jenesis.pin`) is read for verification: a `` comment on a direct `` ``, on a ``, or in any fetched transitive POM is ignored, so the module's own dependency-management block is the sole source of truth and a third-party dependency cannot self-certify the checksum of one of its own (unpinned) artifacts. Checksums are computed once, by the `pin` step: `PinPom` / `PinModuleInfo` read each module's resolved closure from `inventory.properties` (`prefix.dependency.`, which carries each coordinate's self-anchored jar path), rehash the referenced jar using `-Djenesis.project.digest` (default `SHA-256`), and write the result back into `pom.xml`'s `` comments or `module-info.java`'s `@jenesis.pin // /` Javadoc tags. `Dependencies` then validates every subsequent fetch against the pinned checksum (mismatch fails the build); a coordinate that still has no pinned checksum is fetched without integrity validation - or, when strict pinning is enabled (the `-Djenesis.dependency.pin=strict` property resolved as `Project`'s default pinning via `Pinning.fromProperty()`, or an explicit `Project.pinning(Pinning.STRICT)` override, threaded through `MavenProject.make` / `ModularProject.make` / `ProjectModuleDescriptor` / `InferredMultiProjectAssembler` / `TestModule` into every `Dependencies`), the build fails. `Dependencies` preserves the group and writes the resolved transitive closure to `dependencies.properties` (not back into `requires.properties`); there, module-style coordinates carry an optional trailing `/` segment (`main/compile/module/org.junit.jupiter/5.11.3`) reflecting the version a resolver chose for that module. | | `versions.properties` | `BuildStep.VERSIONS` | `///=[ /]` entries that act as a *bill of materials* for the resolution that follows: every resolver receives this map alongside `requires.properties` and uses the version part to pin any (declared or transitive) dependency that matches the bare coordinate. The optional space-separated `/` suffix is the pre-pinned content checksum for that coordinate; resolvers carry it through into the resolved `dependencies.properties` value so `Dependencies` validates the bytes against it. For Maven the coordinate is `groupId/artifactId[/type[/classifier]]`; for modules it is the bare Java module name, whose value may carry a leading-colon classifier qualifier (`:[:]`) selecting the classified variant of that module wherever it appears in the closure. The leading group keeps an independent resolution closure (e.g. a `kotlinc` compiler closure) separate from the project's own `main` group, so each group resolves and pins on its own. The file is written next to `requires.properties` by producers that have version data to contribute (`ModularProject` from `@jenesis.pin` Javadoc tags, `MavenProject` from `` and the `` comment block). | | `boms.properties` | `BuildStep.BOMS` | Sibling of `versions.properties` written by `ModularProject.Manifests` when a `module-info.java` carries `@jenesis.bom` tags. Two key shapes share the file so declaration order (and with it first-declared-wins precedence) survives: `bom///=[ /]` is a repository BOM reference (an empty value floats to the latest published BOM), and `entry///=[ /]` is an already-expanded entry from a local BOM file (the `[/]bom-.properties` declaration form naming a file in the project's BOM locations - `jenesis.project.boms`, defaulting to the configuration locations - expanded at manifests time; each found `bom-*.properties` is bound as a tracked manifests input, and the locations are fixed and never profile-resolved). Platform guards on `@jenesis.bom` declarations are already resolved when this file is written, exactly like guarded pins in `versions.properties`. `MultiProjectDependencies` merges the per-module files with `putIfAbsent`; `Dependencies` fetches each `bom/` reference as `[/]:properties` through the named repository (validating the reference's own checksum unless pinning is `versions`/`ignore`, and requiring one under `strict` for non-internal items), expands the fetched properties with the same group-less key grammar as local BOM files, and folds all entries beneath `versions.properties` pins via `putIfAbsent` - so an explicit `@jenesis.pin` always wins over any BOM entry. `Dependencies` also writes a **resolved** `boms.properties` into its own output for the `pin` step: `bom////=` per versioned reference plus the merged `entry/...` map; `Inventory` mirrors it as indexed `prefix.bom.` entries (read only from folders that also carry `dependencies.properties`), which is how `PinModuleInfo` learns what a BOM supplies and where to hash the BOM file from. | | `exclusions.properties` | `BuildStep.EXCLUSIONS` | Sibling of `requires.properties` produced by `MavenProject.Manifests` (only when a dependency declaration in a `pom.xml` carries ``). Each key is a `///` from `requires.properties`; the value is a comma-separated list of `/` patterns that the resolver must subtract from this dependency's transitive closure (so e.g. `mockito-core net.bytebuddy/byte-buddy` does not silently re-pull `byte-buddy` through the test classpath). `MultiProjectDependencies` carries the entries through to the per-scope prepare step alongside the matching `requires.properties` rows; `Dependencies` reads the file from its arguments and threads the exclusion set per coordinate into `Resolver.dependencies`, where `MavenPomResolver` populates `MavenDependencyValue.exclusions` so the transitive walk honours them. `ModularJarResolver` rejects any non-empty exclusion set up front because Java modules have no exclusion concept. `Pom` reads the file to emit each `` with its declared `` so consumers of the published POM keep the same closure. The file is omitted entirely when no dependency in the module declares exclusions. | | `module.properties` | `BuildStep.MODULE` | Per-module **graph-state** descriptor written by every `Manifests` step. Carries only keys the framework manages, never the user. Always present with `path=` (the source folder housing this module's `pom.xml` / `module-info.java`) and `modular=` (set by `ModularProject.Manifests` from whether the descriptor is a real module, `false` for Maven modules; read by `Inventory` to route modular artifacts). `ModularProject.Manifests` also writes `module=`. Test variants additionally carry `test=` (or the empty string for the deprecated bare `@jenesis.test` form); the key is absent on main modules, and consumers (`Pom`, `InferredMultiProjectAssembler`, `Inventory`) use that absence/presence as the test-variant signal, with `Inventory` mirroring the value into `inventory.properties` as `prefix.test` so `MavenRepositoryStaging` and `ModularStaging` can route test modules at staging time. Modules with an entry point carry `main=` on the **main** variant (omitted on test variants): `ModularProject.Manifests` populates it from an `@jenesis.main ` Javadoc tag on `module-info.java`, `MavenProject`'s per-module manifests step populates it from a `...` entry in the module's `pom.xml`. `InferredMultiProjectAssembler` runs a `prepare` step that translates `main` into a `process/jar.properties` file with a `--main-class=` flag; the existing `ProcessBuildStep` plumbing then prepends that flag to the `jar` command line, which makes the produced `classes.jar` carry both a manifest `Main-Class:` entry and a `ModuleMainClass` attribute on the bundled `module-info.class`. `Inventory` mirrors `path` into `inventory.properties` as `prefix.path`, which `Project.PinModule` reads to discover which modules to pin without pattern-matching graph paths. | | `metadata.properties` | `BuildStep.METADATA` | Per-module **POM coordinates and descriptive metadata** written by every `Manifests` step. Always carries the three coordinate keys `project=`, `artifact=`, `version=`: `MavenProject`'s per-module manifests step copies them straight from the `pom.xml`, while `ModularProject.Manifests` derives them from the Java module system module name (first two dot-separated segments for `project`, the full name for `artifact`) and defaults `version` to `1-SNAPSHOT`. On top of the coordinates the step adds whatever descriptive metadata is available: `ModularProject.Manifests` parses `name` and `description` from the module-info Javadoc; `MavenProject`'s manifests step lifts ``, ``, ``, every `` (as `license..name` / `license..url`, where `` is the license name lowercased with spaces and dots replaced by `_`), every `` (as `developer..name` / `developer..email`), and the `` block (`scm.connection`, `scm.developerConnection`, `scm.url`) from the module's `pom.xml`. After the framework's own defaults are written, the step folds any upstream `metadata.properties` from its input folders on top (later puts win), which is how user-supplied overrides take precedence over both the framework defaults and the POM-extracted values. `Pom` consumes the file as the single source of truth for the emitted pom and throws if any of `project` / `artifact` / `version` is missing. The optional project-root override file (conventionally `project.properties`, pointed at via `-Djenesis.project.metadata=`) uses the same key schema and is bound into the executor's `metadata` module so its entries reach every per-module `metadata.properties` as upstream input; `-Djenesis.project.version=` is appended last and overrides any `version` from either layer. | | `dependencies.properties` | `BuildStep.DEPENDENCIES` | Written by `Dependencies` next to the private `resolved/` folder it fills: the single authoritative index mapping each resolved `///` (with its trailing `/` segment) to a value of the form ` []` - the **self-anchored relative path** of the jar it produced (`resolved/-.jar`), optionally followed by a space and an `/` content checksum. Folding the checksum into this value lets a single file carry both the resolved closure and its integrity data. The leading group on each index key is what isolates each resolution closure - a compiler's `kotlinc/runtime/...` jars and the project's own `main/compile/...` jars sit in the same index but never collide; neither group nor scope is part of the jar filename, so two keys that resolve the same repository and coordinate share a single jar file on disk. It is the authoritative coordinate-to-artifact index, so consumers never reconstruct a jar filename from a coordinate and never walk the private `resolved/` folder: `Inventory` reads it alone to assemble both the runtime closure (`prefix.runtime`, the `main`-group `compile`/`runtime` scopes only) and the per-module dependency closure (`prefix.dependency`, every group) it publishes, and `pin` then hashes the jar each `prefix.dependency` entry references rather than reading this file itself. Consumers locate jars through `Dependencies.select(folder, scope)` / `Dependencies.all(folder)` and read each path value with `.resolve(value).normalize()`. | | `inventory.properties` | `Inventory.INVENTORY` | Per-module **launchable and stageable summary** written by `Inventory`. Each module produces one file whose keys carry a single-segment prefix derived from the module's path: `module` for the root module (empty `path`), `module-` otherwise (e.g. `module-core`). Keeping the prefix dot-free lets a consumer recover the prefix from any key by taking the substring up to the first `.`. The three folder-listing keys are indexed: each file found in the matching folder among the inventory's predecessors is written under a zero-based key `prefix..` (the path is the value, directly after the index), so `prefix.artifacts.0`, `prefix.artifacts.1`, and so on. The groups are `prefix.artifacts` (the contents of every `artifacts/` folder, i.e. the module's produced binary jars; the staging steps require exactly one entry ending in `.jar`), `prefix.sources` (the contents of every `sources/` folder, typically just the produced `sources.jar`; the staging steps require at most one entry ending in `.jar`), `prefix.documentation` (the contents of every `documentation/` folder, typically just the produced `javadoc.jar`; same at-most-one-jar rule). Plus the scalar keys: `prefix.report.` (one entry per report kind found among the predecessors' `reports//` folders - `tests` for the JUnit Open Test Reporting XML when reporting is enabled, `checkstyle`/`pmd`/`spotbugs`/`detekt`/`ktlint`/`scalastyle`/`codenarc` for the lint XML, `jacoco` for coverage, `sbom` for the CycloneDX bill of materials, `dependencies` for the resolved `graph.properties`; each value points at the `reports//` folder and `ReportStaging` hardlinks it under `stage/reports///`), `prefix.pom` (path to the generated `pom.xml` when the layout emits one), `prefix.version` (mirror of `metadata.properties`' `version`), `prefix.artifact` (mirror of `metadata.properties`' `artifact` - the Java module name under modular layouts, the artifactId under `MAVEN`; this is the key a test module's `prefix.test` is matched against to route a captured reachability report to the right main module's native image), `prefix.test` (mirror of `module.properties`' `test`, set only on test modules), `prefix.module` (mirror of `module.properties`' `module`, written under `MODULAR` and `MODULAR_TO_MAVEN` since both build genuine modules, and omitted under `MAVEN`), `prefix.mainClass` (mirror of `module.properties`' `main`), and `prefix.path` (mirror of `module.properties`' `path`, the module's source folder relative to the project root; `Project.PinModule` reads it to discover which modules to pin). Two further indexed groups carry the per-module dependency state that `pin` consumes: `prefix.identity.` (the module's own coordinates, mirrored from `identity.properties`, so `pin` can exclude internal sibling coordinates project-wide) and `prefix.dependency.` (the module's full resolved closure, one entry per coordinate written as `/// [ /]`, read straight from `dependencies.properties` whose value already carries both the jar path and any known checksum; unlike `prefix.runtime` it **includes** every tool group (annotation processors under group `plugin` scope `plugin`, the `kotlinc`/`scalac`/`groovyc` compiler groups under scope `runtime`, and compiler plugins under scope `plugin`), and it is the single source `PinPom` / `PinModuleInfo` read to recompute and write source-level pins). Finally the indexed `prefix.runtime.` (the binary artifact followed by every `main`-group `compile`/`runtime`-scoped jar of the resolved closure - i.e. `prefix.dependency` minus the tool groups: the runtime classpath that `Execute` uses). All path values are **self-anchored**: written relative to the inventory file's own folder, and consumers resolve them with `.resolve(value).normalize()`. Any key whose value would be empty is omitted entirely. A consumer that reads several modules' inventories can `putAll` them into one `Properties` map without key collisions, then group by prefix to recover per-module records. Consumers: `Project.PinModule` enumerates the modules to pin from every `prefix.path`, then wires one `PinPom` / `PinModuleInfo` per module that reads that module's own `prefix.dependency` closure (selected by its prefix) and every module's `prefix.identity` for internal-coordinate filtering; `Execute` picks candidates with `prefix.mainClass` set and assembles the classpath/modulepath from `prefix.runtime`; `MavenRepositoryStaging` parses `prefix.pom` for coordinates, routes by `prefix.test`, validates the folder-listing keys, and hardlinks the single jars into the Maven repository layout (and, when the module carries a `prefix.report.sbom`, attaches the SBOM as `--cyclonedx.`); `ModularStaging` reads `prefix.module` plus optional `prefix.version`, validates the folder-listing keys, and hardlinks the single jars - and, when present, the `prefix.jmod` `.jmod` (produced by the `jmod` step) - under `/[/]`; `ImageStaging` collects the directory-tree images recorded as the scalars `prefix.package` (the `jpackage` application image), `prefix.image` (the `jlink` runtime image) and `prefix.native` (the `native-image` executable) into `stage/packages`, `stage/runtime` and `stage/native` respectively; the package phase's `NativeImageMetadata` router reads the scalar `prefix.nativeimage` (a test module's captured GraalVM reachability metadata, a build-internal capture that is **not** staged like a report) to feed each native image its own test's metadata; `ReportStaging` reads every `prefix.report.` entry and hardlinks each module's reports under `stage/reports///`. | | `uris.properties` | `DownloadModuleUris.URIS` | `/` keys mapped to an absolute jar URL; populated from line-based `=` registries (default: sormuras/modules) and used during dependency resolution to translate a Java module name into a download URL. When a versioned coordinate is requested (e.g. `org.assertj.core/3.27.0`) and the bare name is mapped to a URL whose final path segments follow the Maven repository layout (`...///-[-].`), an opt-in version-resolver function (`MavenDefaultRepository.versionResolver()`) supplied by the caller rewrites the path's version segment and the filename's version segment to the pinned value, so a single-URL registry still satisfies version pins. Without that function, `Repository.ofUris` performs strict literal lookup only; if the version resolver is supplied but returns `Optional.empty()` for a versioned coordinate (e.g. the registered URL is not in Maven layout), the fetch is a clean miss - the bare-name URL is **not** silently substituted, so a build that asked for `foo/1.2.3` will never quietly receive the registry's default version. A launcher that wires `Repository.ofProperties` over such a registry passes this resolver explicitly, since the dominant Java module URL registries (sormuras/modules and most internal mirrors) point at Maven Central -- making the Maven layout assumption visible at the use site rather than baked into the generic `Repository` infrastructure. The shipped `Layout.MODULAR` does not consume `uris.properties` directly anymore; its `module` prefix is served by the `https://repo.jenesis.build/module/` overlay (which performs the same version rewrite internally), with `JenesisModuleRepository.ofLocal()` prepended. The shipped `Layout.MODULAR_TO_MAVEN` also no longer consumes it: a `MavenModuleResolver` fetches each declared module's `:pom` artifact from `https://repo.jenesis.build/artifact/` (with `JenesisModuleRepository.ofLocal()` prepended, as for `Layout.MODULAR`) instead, translates it into a Maven coordinate, and seeds the bytes into `MavenPomResolver` as the pre-fetched first layer of a single synthetic project POM, so those first-layer POMs are never re-downloaded from Maven Central before transitive resolution. | | `process/.properties` | `ProcessBuildStep.PROCESS` (folder) | Command-line fragments contributed to a downstream `ProcessBuildStep` whose tool name matches `` (`java`, `javac`, `jar`, `javadoc`). Keys are flags (e.g. `--add-modules`); values are flag values, with literal `\n` inside a value emitting the same flag once per piece. Each input folder's file is processed independently and its entries are appended to the command line in folder order, so the same key in two folders becomes two flag instances. Values that name filesystem paths are written relative to the file's containing folder (paths are not resolved until the consumer step needs them), which keeps the on-disk content position-independent so build outputs can be relocated or shared between caches without rewriting. | | `launcher.properties` | (inlined literal; written by `prepare`) | The **neutral entry-point descriptor** the default `InferredMultiProjectAssembler`'s `prepare` step writes once per module that declares a main class, holding `mainClass`, `mainModule` (only for a module-path launcher), and `name` (the artifact). It exists so every artifact-generation step reads the launcher coordinates from the *same* assembler-owned file: `Bundle` and the `Launcher` step translate it into their `application.properties`, and `NativeImage` into its `--module /` or positional main. It is deliberately separate from the per-command `process/jpackage.properties` / `process/jlink.properties` that `jpackage` / `jlink` consume through the `ProcessBuildStep` auto-prepend - those stay private to their own step, so no artifact step depends on a properties file another artifact step consumes. | | `pom.xml` | `Pom.POM` | A generated Maven Project Object Model, ready to be packaged alongside a built jar so the artifact can be published to and consumed from any Maven-aware repository. | | `target/` | (passed to `BuildExecutor.of`) | The root folder under which every step's per-run output and the executor's incremental bookkeeping (output checksums and predecessor checksum snapshots used to decide whether a step needs to re-run) live. Safe to delete to force a clean build. | | `.jenesis/artifacts/` | by convention | A project-root folder for caches that outlive a single build, hardlink-shared with `target/`. The `MODULAR` layout populates `.jenesis/artifacts/.jar` via `Repository.cached(...)` so module jars survive a `target/` wipe; `MAVEN` and `MODULAR_TO_MAVEN` cache into `~/.m2/repository` instead. Relocatable via `Project.artifacts(Path)` or `-Djenesis.project.artifacts=`. See the *Repositories and resolvers* and *The artifact cache* sections below for the full picture. | | `.jenesis/cache/` | by convention (opt-in) | The project-local **build cache** (a `BuildExecutorFileCache`), enabled with `-Djenesis.project.cache` (empty value resolves here) or relocated with `-Djenesis.project.cache=`. Holds step outputs keyed by content hash (`//`), so a served step never re-runs, and survives a `target/` wipe. `Project` roots it under the project root and layers it in front of any `jenesis.cache.uri` backend via `BuildExecutorLayeredCache`. Distinct from the artifact cache above. See *demo-38*. | | `.jenesis.skip` | `BuildExecutor.SKIP_MARKER` | An empty marker file placed at the root of an active build directory. Project-tree walkers honour it as a stop signal so nested builds aren't re-discovered as part of the parent build's project graph. | Build steps ----------- The steps listed here are pre-implemented for convenience; the build tool itself does not depend on any of them, and a build is free to ignore them and supply its own `BuildStep` implementations. | Step | What it does | Inputs (per predecessor folder) | Outputs (under `context.next()`) | | -------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `Bind` | Hard-links files from each predecessor into a target layout under `context.next()`, driven by a `Map` that mirrors specific subtrees under canonical names (used by the static factories `asSources()`, `asResources()`, `asIdentity(...)`, `asRequires(...)`). | a source folder, a named properties file, or any other predecessor subtree named in the map | `sources/`, `resources/`, `identity.properties`, `requires.properties`, or any layout produced by the configured map | | `Javac` | Compiles each predecessor's `sources/` with the `javac` tool, using their `artifacts/` (and, for a non-modular compile, their `classes/`) as class- or module-path entries; writes the resulting `.class` files to `classes/`. When the compiled sources include a `module-info.java`, predecessors' `classes/` directories - the same module's other-language output produced earlier in the inferred chain (Kotlin, Scala) - are instead supplied via `--patch-module =` (the module name read from the `module-info.java` source), so the module declaration can resolve and `exports` packages that only those classes populate. Source files under `sources/META-INF/versions//` are recognised as multi-release overlays and compiled in a separate pass per `` with `--release `, writing the resulting classes to `classes/META-INF/versions//`. For the overlay pass, main sources are made available via the just-compiled main classes - on the class-path when main is non-modular, or via `--module-path` plus `--patch-module =` when main has a `module-info.java`. When any overlay was produced, a `manifest.mf` containing `Multi-Release: true` is emitted alongside `classes/` so the downstream `Jar` step can mark the produced jar as multi-release. | `sources/`, `classes/`, `artifacts/` | `classes/` (plus `classes/META-INF/versions//` for overlay passes and a top-level `manifest.mf` when an overlay was produced) | | `Jar` | Packages the folders selected by the configured `Jar.Sort` into a single jar at the convention path that matches the sort: `CLASSES` writes `artifacts/classes.jar` (the deployable binary), `SOURCES` writes `sources/sources.jar` (alongside the source tree), `JAVADOC` writes `documentation/javadoc.jar` (alongside the docs tree); the latter two stay out of `artifacts/` since they are not deployable binaries. When one or more predecessors supply a top-level `manifest.mf`, those files are parsed and merged (matching attributes collapse, conflicting values for the same attribute fail the step) into a single manifest staged in `context.supplement()`, which is then passed to `jar` via `--manifest` so the merged attributes (notably `Multi-Release: true` emitted by a preceding multi-release `Javac` pass) land in the produced jar's `META-INF/MANIFEST.MF`. | per `Jar.Sort`: `CLASSES` reads `classes/` + `resources/`; `SOURCES` reads `sources/` + `resources/`; `JAVADOC` reads `javadoc/`; all sorts pick up each predecessor's top-level `manifest.mf` | `artifacts/classes.jar`, `sources/sources.jar`, or `documentation/javadoc.jar` (depending on `Jar.Sort`) | | `Javadoc` | Invokes the `javadoc` tool over each predecessor's `sources/` and writes the generated documentation tree to `javadoc/`. | `sources/` | `javadoc/` | | `JPackage` | Stages every jar found in each predecessor's `artifacts/` and `dependencies/` into a single directory under `context.supplement()` and invokes the `jpackage` tool with `--dest packages/`, writing the produced application image or installer to `packages/`. It runs in one of two launch modes, chosen by the supplied `process/jpackage.properties`: a **classpath** launcher (`--main-jar`/`--main-class`) reads the app from `--input `, while a **modular** launcher (`--module /`) reads it from `--module-path ` and bundles a module-tailored runtime. The packaging details (`--name`, `--type`, and so on) come from the same properties file, the way `Javac` reads `--release` from `process/javac.properties`. When neither a `--main-jar` nor a `--module` launcher is configured the step is skipped. | `artifacts/`, `dependencies/`, `process/jpackage.properties` | `packages/` | | `JLink` | Collects the modular jars and `.jmod` files in each predecessor's `jmods/`, `artifacts/` and `dependencies/` into a `--module-path` and invokes the `jlink` tool with `--output runtime/`, writing the produced custom runtime image to `runtime/`. The JDK's own modules are observable to `jlink` automatically, so only the project's modules need staging. The root modules to link in (`--add-modules`) and any other options are supplied through a `process/jlink.properties` file, the same way `Javac` reads `--release` from `process/javac.properties`. The step is skipped when no `--add-modules` is configured (so it does not run for non-modular modules). When `InferredMultiProjectAssembler` wires it (via a `jlink=true` `packaging.properties`), the `prepare` step supplies `--add-modules `, `Inventory` records the image as `prefix.image`, and the `STAGE` module's `runtime` step (`ImageStaging`) collects it into `stage/runtime`. | `jmods/`, `artifacts/`, `dependencies/`, `process/jlink.properties` | `runtime/` | | `JMod` | Packs each predecessor's `classes/` into a single `jmods/.jmod` via the `jmod` tool's `create` mode, naming the file after the module read from `classes/module-info.class`. It also routes a predecessor's `jmodconfig/`, `jmodlibs/` and `jmodcmds/` folders to `jmod --config`/`--libs`/`--cmds`, so a custom step can pack non-class content (config files, native libraries, commands) that `jlink` then lays into a runtime image's `conf/`/`lib/`/`bin/` - content a jar cannot carry into a runtime (see the [`custom-jmod`](demo/demo-32-custom-jmod/README.md) demo). The output goes to `jmods/` rather than `artifacts/` because a `.jmod` is a link-time-only artifact (rejected on the runtime module path), and the path consumers walk `artifacts/` unfiltered; `jmods/` keeps it off the runtime path while still feeding `JLink`. Additional options (`--main-class`, `--module-version`, and so on) are supplied through a `process/jmod.properties` file, the same way `Javac` reads `--release` from `process/javac.properties`. When the classes carry no `module-info.class` the step is skipped. When `InferredMultiProjectAssembler` wires it (via a `jmod=true` `packaging.properties`), `Inventory` records the file as `prefix.jmod` and `ModularStaging` hardlinks it alongside the jar in `stage/modular`. | `classes/`, `jmodconfig/`, `jmodlibs/`, `jmodcmds/`, `process/jmod.properties` | `jmods/.jmod` | | `Bundle` | Collects every jar from each predecessor's `artifacts/` and `dependencies/` and writes a single `bundle/bundle.zip` containing two folders, `classpath/` and `modulepath/`, plus an `application.properties` with a `mainClass` property (and, for a modular launcher, a `mainModule`, plus - whenever `modulepath/` is non-empty - a `selfContainedModuleGraph=true|false` flag: `false` when the module path carries an automatic module or there are `classpath/` jars, telling the consumer it must add `--add-modules ALL-MODULE-PATH` to root the whole module path; `true` when the launched module's `requires` closure covers it and no such flag is needed). The launcher is read from `launcher.properties`, the neutral entry-point descriptor the assembler's `prepare` step writes for every artifact step (rather than `jpackage`'s own `process/jpackage.properties`, so no artifact step depends on another's command file): a `mainModule` entry yields a modular bundle (real and automatic-module jars go onto `modulepath/`, the rest onto `classpath/`, mirroring how `Execute` launches), while a bare `mainClass` yields a class-path-only bundle. The step is skipped when no main launcher is configured, so it produces nothing for a module without a main class. The zip is a ready-to-run input for building a Docker image or similar: the consumer puts `modulepath/` on `--module-path`, `classpath/` on `--class-path`, and reads the entry point from `application.properties`. | `artifacts/`, `dependencies/`, `launcher.properties` | `bundle/bundle.zip` | | `NativeImageMetadata` | Routes captured GraalVM reachability metadata to the module being imaged, so a multi-module build never feeds one image another module's capture. Reads this module's own inventory (the only input outside the `SELECTION` namespace the package phase wires) for its `prefix.artifact`, then, among the sibling inventories, collects the `nativeimage` capture of the test module whose `prefix.test` names that artifact into a single `native-image/` config directory the `native-image` step discovers by content. A main module has at most one test module (staging rejects more), so there is one source and nothing to merge; a module with no capturing test produces no directory. Wired into the package phase by `InferredMultiProjectAssembler` ahead of `native-image` only when `packaging.properties` sets `native=true`. | this module's `inventory.properties` (`prefix.artifact`) and its siblings' (`prefix.test`, `prefix.nativeimage`) | `native-image/` (the merged config directory) | | `Java` | Runs `java` with each predecessor's `classes/`, `resources/` and the jars in `artifacts/` assembled into a class- and module-path; the entry point and command line are supplied by subclasses or `Java.of(...)`. | `classes/`, `resources/`, `artifacts/` | runs `java`; no canonical output | | `Dependencies` | Reads `requires.properties` and (when present) `versions.properties`, groups the keys by their leading resolver-group segment, then asks each group's `Resolver` for the transitive closure with the version map as a pin set, fetches each resolved jar directly into the private `resolved/` folder, and writes `dependencies.properties`, the authoritative `///`-to-jar index (module-style coordinates pick up a trailing `/` segment when a version is known; the group is preserved as the leading segment, so each index key is group-first, e.g. `main/compile/maven/...` and `kotlinc/runtime/maven/...`). Each index value is the jar's self-anchored relative path optionally followed by a space and an `/` checksum, so the same file carries both the resolved closure and its integrity data. Checksums supplied via the `versions.properties` `version checksum` suffix - or via comments in transitive POMs the resolver visits - are propagated into the index value for matching resolved coordinates and validated against the fetched bytes (mismatch fails the build); strict pinning is applied via the `.pinning(...)` builder. It also reads `exclusions.properties` (when present) to drop excluded transitives during resolution, and `boms.properties` (when present) to fetch referenced BOM properties files and fold their entries beneath the `versions.properties` pins. | `requires.properties`, `versions.properties`, `boms.properties`, `exclusions.properties` | `resolved/-.jar` (private), `dependencies.properties` | | `Versions` | Walks each predecessor's `classes/`, hard-links every non-`module-info.class` file under `context.next()/classes/`, and rewrites every `module-info.class` so each `requires ` directive gets a `compiledVersion` set from the matching entry in `dependencies.properties` (module-style `//` coordinates). Uses the JDK's `java.lang.classfile` API; module flags (`OPEN`), the module's own version, `exports`, `opens`, `uses` and `provides` round-trip unchanged. | `classes/`, `dependencies.properties` | `classes/` (non-`module-info` hard-linked, `module-info.class` rewritten in-place) | | `Group` | Reads each predecessor's `identity.properties` and `requires.properties`; for each identified group, writes a `groups/.properties` listing the other groups whose coordinates it depends on. | `identity.properties`, `requires.properties` | `groups/.properties` | | `Assign` | Fills the empty values of `identity.properties` with paths to the jars in the predecessors' `artifacts/`, finalising the coordinate → file mapping. | `identity.properties`, `artifacts/` | `identity.properties` (empty values filled with artifact paths) | | `Inventory` | Builds a per-module **launchable, stageable and pinnable summary**: scans each predecessor for `module.properties` (`path`, `main`, `module`, `test`), `metadata.properties` (`version`, `artifact`), `identity.properties` (the module's own coordinates), any `artifacts/` subdir (each file goes into `prefix.artifacts`), any `sources/` subdir (each file into `prefix.sources`), any `documentation/` subdir (each file into `prefix.documentation`), any `packages/` subdir (the application image produced by the `package` step, recorded whole as the scalar `prefix.package` for `ImageStaging` to collect), any `jmods/` subdir (the `.jmod` produced by the `jmod` step, recorded as `prefix.jmod` for `ModularStaging` to stage alongside the jar), any `runtime/` subdir (the runtime image produced by the `jlink` step, recorded whole as the scalar `prefix.image` for `ImageStaging` to collect), any `native/` subdir (the standalone executable produced by the `native-image` step, recorded whole as the scalar `prefix.native` for `ImageStaging` to collect), any `nativeimage/` subdir (the captured GraalVM reachability metadata, recorded whole as the scalar `prefix.nativeimage` for the package phase's `NativeImageMetadata` router - a build-internal capture, deliberately not a `reports/` report), any `reports//` subdir (a build report - lint XML, JaCoCo coverage, JUnit Open Test Reporting XML - each kind recorded as a `prefix.report.` entry for `ReportStaging` to collect), and a top-level `pom.xml` (the generated POM when the layout emits one). The module's resolved closure is read from each predecessor's `dependencies.properties` (`///` -> jar path and any checksum, both in one value), and emitted as `prefix.dependency` (one entry per coordinate, carrying the self-anchored jar path and any checksum, **including** the tool groups); the `prefix.runtime` classpath is that same closure narrowed to the `main`-group entries (so a compiler / build-module closure never leaks into the produced module). The module's own coordinates become `prefix.identity` and its source path `prefix.path` - both consumed by `pin`. Emits one `inventory.properties` with a single-segment prefix (`module` for the root, `module-` otherwise); all path values are self-anchored to the inventory file's folder. See the row in the conventions table above for the recognised keys. | `module.properties`, `metadata.properties`, `identity.properties`, `artifacts/`, `sources/`, `documentation/`, `packages/`, `jmods/`, `runtime/`, `native/`, `dependencies.properties`, `pom.xml` | `inventory.properties` | | `DownloadModuleUris` | Fetches the configured remote URL lists and concatenates them into a single `uris.properties`. The default registry is [sormuras/modules](https://github.com/sormuras/modules), a community-maintained map of Java module names to Maven Central jar URLs; its refresh is manual, so a brand-new upstream version may not appear in the registry until the next refresh is published. | none (fetches the configured URLs) | `uris.properties` | | `MultiProjectDependencies` | Merges per-project `requires.properties` (and looks up sibling-project paths in their `identity.properties`) into one unified `requires.properties`. Sibling-built coordinates are written with the build executor's content checksum of the resolved sibling artifact (so a consumer re-resolves when the sibling changes); externally-pinned coordinates pass through with their declared checksums intact. | per-predecessor `identity.properties` or `requires.properties`, partitioned by predicate | unified `requires.properties` | | `Pom` | Emits a Maven `pom.xml`, taking the project's own coordinate from the empty entry in `identity.properties` and its dependencies from `requires.properties` entries that share the same prefix. | `identity.properties` (self coordinate = empty value), `requires.properties` | `pom.xml` | | `MavenRepositoryStaging` | Per-module inventory walker that stages the contents of every `inventory.properties` it sees into a Maven-repository tree under `context.next()`. For each main module it parses `prefix.pom` for `groupId`/`artifactId`/`version`, validates that `prefix.artifacts` lists exactly one `.jar` and `prefix.sources`/`prefix.documentation` each list at most one `.jar`, then hardlinks the binary plus the (optional) sources/documentation jars plus the pom as `///-.`. For each test module (`prefix.test=`) it routes the jars onto the named main's coordinate with a `-tests` classifier; the test module's POM is parsed for additional dependencies and merged into the staged main POM with `test`. Refuses duplicate main artifactIds and multiple test modules pointing at the same main. | every `inventory.properties` reachable through the predecessors | `///-[-].{jar,pom}` | | `ModularStaging` | Per-module inventory walker that stages Java-module-named artifacts. For each inventory it reads `prefix.module` (the Java module system module name) and optional `prefix.version`, validates that `prefix.artifacts` lists exactly one `.jar` and `prefix.sources`/`prefix.documentation`/`prefix.jmod` each list at most one, then hardlinks them under `/[/]{,-sources,-javadoc}.jar` plus, when a `.jmod` was produced (the `jmod` step), `/[/].jmod` alongside the jar. Test modules (`prefix.test` set) are skipped by default and emitted under their own Java module system name when `includeTests` is enabled. | every `inventory.properties` reachable through the predecessors | `/[/]{,-sources,-javadoc}.jar` and `.jmod` | | `ImageStaging` | Per-module inventory walker that collects a produced directory-tree image, the staging analogue of `MavenRepositoryStaging`/`ModularStaging`. Constructed with the inventory key it collects: `ImageStaging("package")` gathers each `prefix.package` (the `jpackage` image, staged as the `packages` step into `stage/packages`), `ImageStaging("image")` gathers each `prefix.image` (the `jlink` runtime, staged as the `runtime` step into `stage/runtime`), `ImageStaging("native")` gathers each `prefix.native` (the `native-image` executable, staged as the `native` step into `stage/native`). It hardlinks the recorded tree into `context.next()` so every module's image lands side by side. Inventories without the key are skipped, so the step (registered unconditionally in each layout's `STAGE`) stages nothing when nothing was produced. | every `inventory.properties` reachable through the predecessors | the produced image trees, hardlinked under `context.next()` | | `ReportStaging` | Per-module inventory walker that collects every build report. For each inventory it reads every `prefix.report.` entry and hardlinks the referenced `reports//` folder (or file) into `context.next()` under `//`, so each kind lands in its own folder and each module stays separate (`checkstyle/sources/checkstyle-report.xml`, `jacoco/core/index.html`, `tests/core/junit-platform-events-1.xml`, and so on). Staged as the `reports` step into `stage/reports` in every layout's `STAGE`. Inventories with no `prefix.report.` entries are skipped, so the step stages nothing when no reports were produced. | every `inventory.properties` reachable through the predecessors | `//`, hardlinked under `context.next()` | | `MavenRepositoryExport` | Publishes a staged Maven-repository tree to an external target path (default `~/.m2/repository`, overridable via the `MAVEN_REPOSITORY_LOCAL` environment variable). Always re-runs (`shouldRun = true`) since the destination is outside the executor's control. Walks each predecessor for `.pom` files, copies every sibling in the version directory into the matching target path with `REPLACE_EXISTING`, then writes the `mvn install`-equivalent metadata: a `maven-metadata-local.xml` per artifact (`` set to the highest non-SNAPSHOT version by Maven semantics, `` sorted ascending, `` timestamp), an `_remote.repositories` marker per version directory, and a `modelVersion="1.1.0"` `maven-metadata-local.xml` inside each `-SNAPSHOT` version directory listing per-extension/classifier ``. | a staged Maven-repository tree (typically `MavenRepositoryStaging`'s output) | files copied under the configured target path; nothing is written under `context.next()` | | `PinPom` | Reads the module's resolved closure from `inventory.properties` (the `prefix.dependency.` entries, each a concrete `////` coordinate with its self-anchored jar path and any `/` checksum), excluding the project's own coordinates advertised in every module's `prefix.identity`, selects the `main`-group `compile`/`runtime`-scope entries that resolve through the `maven` repository, and rewrites the configured `pom.xml` source file(s) so that the `` block lists every entry as a `` (with ``/`` when present) plus a `` comment when one is known. The descriptor's own managed `versions.properties` is **not** read back in, so a module is pinned strictly from what it actually resolves and stale entries are dropped. Where the jar a `prefix.dependency` entry points at is present its checksum is recomputed; otherwise the entry's carried checksum is used. Replaces the existing block in place if present, inserts one before `` (or before ``) if absent. Also strips any `` comments from direct `` entries outside ``, since the rewritten BOM is the single source of truth for those checksums. Entries in a tool group (the `kotlinc`/`scalac`/`groovyc` compiler groups, including their compiler plugins, and the annotation-processor `plugin` group) are written instead into a Maven-ignored `` comment block (carrying the group-first `//` key with the scope dropped, `--` stored as `--`), so `` stays the project's own dependencies. Accepts either a single `Path` or a `List` of pom.xml files to update. Always re-runs (`shouldRun = true`) and writes back to the source file outside `context.next()`. | `inventory.properties` (`prefix.dependency` + `prefix.identity` + `prefix.path`) from each predecessor | none under `context.next()`; mutates the configured `pom.xml` file(s) | | `PinModuleInfo` | Reads the module's resolved closure from `inventory.properties` (the `prefix.dependency.` entries with their self-anchored jar paths), excluding the project's own coordinates from every module's `prefix.identity`, and rewrites the configured `module-info.java` source file(s) so that the preceding Javadoc block contains `@jenesis.pin // [ /]` tags for every entry. Non-`main` group coordinates (compiler / plugin / internal / external closures) are always pinned, with checksums recomputed from the jar each `prefix.dependency` entry references. The key is taken straight from the resolved closure, never by inspecting a jar for its module name: a `MODULAR_TO_MAVEN` project resolves each `module`-repository require to a Maven coordinate, so a require pins as its resolved `main/maven//` jar (carrying the checksum) plus a hash-free bare module version pin standing in for the root pom, while a plain non-modular Maven transitive pins under its own `main/maven//` key. Replaces the existing `@jenesis.pin` lines in place if present (preserving other block tags like `@jenesis.release`, `@jenesis.test`); inserts a fresh Javadoc block above the module declaration if none exists. Every tag is written group-first, abbreviating into the default `main` group by slash count: a bare `` (no slash), a `/` Maven coordinate (one slash), or the explicit `//` (two or more slashes). Accepts either a single `Path` or a `List` of module-info.java files. Always re-runs (`shouldRun = true`) and writes back to the source file outside `context.next()`. | `inventory.properties` (`prefix.dependency` + `prefix.identity` + `prefix.path`) from each predecessor | none under `context.next()`; mutates the configured `module-info.java` file(s) | `ProcessBuildStep` and `Java` are abstract bases (used by `Javac`, `Jar`, `Javadoc`, and the inner `executed` step that `TestModule` registers); `Java.of(...)` gives an ad-hoc command runner. The dependency-resolution step parses the dependency files into `(group, scope, repository, coordinate)` entries: `Dependencies` reads `requires.properties`, fetches each jar into the private `resolved/` folder, and writes the resolved closure (jar path plus any checksum) to the `dependencies.properties` index. Before launching its tool, every `ProcessBuildStep` walks each input folder for `process/.properties` (where `` is the tool name supplied to the constructor - `java`, `javac`, `jar`, `javadoc`), loads each folder's file into its own map keyed by argument, and passes the per-folder maps to `process(...)`. Whatever the subclass leaves behind in those maps is then materialised as command-line tokens prepended to the command line the subclass produced, in folder order: each entry becomes a `--key value` pair, and `\n` inside a value emits the same flag once per piece (so a predecessor can write `--add-modules=foo\nbar` to repeat a flag, and the same key contributed by two predecessors yields two flag instances). Path-shaped values are stored as paths relative to the file's containing folder and never resolved by `ProcessBuildStep` itself - keeping the on-disk content position-independent in the same way `requires.properties` does for coordinates. Resolution is the consumer's job. `Java` does this in its own per-folder iteration: in addition to scanning each predecessor's `classes/`/`resources/`/`artifacts/`, it pulls `--module-path` and `--class-path` entries out of that folder's properties map, splits each value on `\n`, resolves each piece against the same `argument.folder()`, and folds the result into the path lists it ultimately joins with the platform path-separator into a single `--module-path` / `-classpath` argument. Removing the keys from the per-folder map as it consumes them keeps `ProcessBuildStep` from also materialising them as repeated flag instances the JVM would treat as last-wins overrides. `MavenRepositoryExport` is the one step that intentionally breaks two of the conventions that every other step holds to. Its job is to publish a build's staged outputs outside the `target/` tree (typically into the user's local Maven repository), so it cannot honour the "immutable, content-hashed output folder" invariant that drives incremental builds: - **Writes outside `context.next()`.** The destination is supplied as a `Path` to the constructor and lives wherever the user wants it - `~/.m2/repository` by default, otherwise a network share, an existing distribution layout, or any other target. Files are copied (not hard-linked, since the target may be on a different filesystem) and `REPLACE_EXISTING` always overwrites whatever is at the destination. `context.next()` itself is left empty. - **Always re-runs.** `shouldRun(...)` returns `true`, so even if all inputs are unchanged the export is performed again. The reason is that the destination is outside the executor's control - anything could have edited or removed those files between builds - so the only safe assumption is that the export needs redoing every time. The step's serialized form is still hashed (config-aware cache invalidation still applies), but consistent predecessor checksums just shorten the diff the step sees, not whether it runs. `MavenRepositoryExport` consumes a tree already shaped by `MavenRepositoryStaging`. It walks each predecessor for every `.pom` file, takes the file's parent directory as the version directory of one artifact, and copies every sibling there (`-[-].{jar,pom}`) into the matching target path. After copying, the same step writes the `mvn install`-equivalent metadata next to each artifact: a `maven-metadata-local.xml` per artifact (`` set to the highest non-SNAPSHOT version by Maven semantics, `` sorted ascending, `` timestamp), an `_remote.repositories` marker per version directory, and a `modelVersion="1.1.0"` `maven-metadata-local.xml` inside each `-SNAPSHOT` version directory listing per-extension /classifier ``. Unhandled today: checksum sidecars (`.sha1`/`.md5`), GPG signatures, and `` version inheritance - the `Pom` step always emits an explicit ``, so the last is fine in practice for artifacts produced by this build. Build executor modules ---------------------- The modules listed here are pre-implemented for convenience; the build tool itself does not depend on any of them, and a build is free to ignore them and supply its own `BuildExecutorModule` implementations. In every diagram below, blue rounded nodes are inputs (folders or files), yellow rectangles are steps, and purple rectangles are nested sub-modules. ### `JavaToolchainModule` Used for compiling and packaging a single Java module from its sources and its resolved dependencies. The record carries four `BuildExecutorModule` components: a `compiler`, an optional `transformer`, an optional `validator`, and an `archiver`. Compilation runs first (`compiled`), then a `Versions` step (`classes`) consults the compile-scope `requires.properties` and rewrites every `module-info.class` to embed the resolved versions on each `requires` directive, so the produced jar carries the same versions that were used to assemble its module path. Whether the compiler and archiver run in-process or out-of-process is decided by the `ProcessHandler.Factory` passed to each step constructor (`new Javac(factory)`, `new Jar(factory, ...)`); `InferredMultiProjectAssembler` resolves it once via `ProcessHandler.Factory.of()`, which supplies the forking variants when `-Djenesis.process.factory=fork` is set so the build can run under a stricter sandbox. A two-argument constructor `JavaToolchainModule(compiler, archiver)` leaves both optional hooks null. When a `transformer` is supplied, it runs as a `transform` module between `classes` and the archiver: it consumes the versioned classes and produces the classes that are archived, and it also becomes what the module exposes as its `classes` output, so downstream consumers transparently see the transformed result. A bytecode rewriter or instrumentation pass plugs in here. When a `validator` is supplied, it runs as a `validate` module that consumes the same (post-transform) classes purely to assert over them; it is an independent side-step that the archiver does not depend on, so it never gates the jar. An enforcer-style check, such as banning a dependency or asserting a package layout, belongs here. Both hooks default to null, in which case the module compiles, version-stamps, and packages exactly as the diagram below shows. Running the compiled tests is not part of `JavaToolchainModule` itself - that is wired separately by `InferredMultiProjectAssembler` as a sibling `TestModule` when the project enables tests and the module is flagged as a test variant (see *`TestModule`* below). ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; src(["sources/"]):::input arts(["dependency artifacts/"]):::input req(["dependency requires.properties"]):::input classes["compiled
(Javac)"]:::step versions["classes
(Versions)"]:::step artifacts["artifacts
(Jar, Sort.CLASSES)"]:::step src --> classes arts --> classes classes --> versions req --> versions versions --> artifacts arts --> artifacts ``` ### `TestModule` A `BuildExecutorModule` that runs a configured `TestEngine` (e.g. the JUnit Platform) against the compiled tests of its predecessors. Construction requires `repositories` and `resolvers` maps; the runner is fetched on the side via an inlined `Dependencies` step (added directly under the name `dependencies`), so the user never has to declare it as a compile-time `requires` of their test module. Empty maps are valid when the runner is already present on the inherited class- or module-path - the `Requires` step then writes an empty `requires.properties` and nothing is fetched: - `resolved` (`TestModule.Requires`) first picks the engine. `TestEngine.scan(...)` reads the module name of every inherited jar (from its `module-info.class`, or a declared `Automatic-Module-Name` - a filename-derived name is ignored), and `TestEngine.of(...)` tries the built-in engines in order `[JUnitPlatform, JUnit4, TestNG]`, selecting the first whose `isEngine(...)` matches any scanned module (`JUnitPlatform` matches `org.junit.platform.engine`, `JUnit4` matches `junit`, `TestNG` matches `org.testng`). It then asks the chosen engine for its runner coordinates: `TestEngine.coordinates()` returns a map of version-less coordinate to *default version* (a `null` value means "no default; let the coordinate float"). `Requires` establishes the accepted prefix from the first key whose `` is served by one of the configured resolvers, then writes *every* coordinate sharing that prefix into `requires.properties` (so an engine may contribute more than one runner dependency). `JUnitPlatform` contributes both `module/org.junit.platform.console` and `maven/org.junit.platform/junit-platform-console`, so the same engine works across the `MODULAR` and `MODULAR_TO_MAVEN` layouts and hand-wired `BuildExecutor` builds. The default version is derived from the version of the discovered `org.junit.platform.engine` module (1.x for JUnit 5, 6.x for JUnit 6); the `maven` entry falls back to `RELEASE` and the `module` entry to `null` (float to latest) when no version can be read. The non-`null` defaults are written into `versions.properties` with `putIfAbsent`, *after* folding in the project's own upstream pins - so a user pin (a `dependencyManagement` entry or `@jenesis.pin` tag for the console launcher) always wins over the derived default, and a user may pin a higher console version than the tests' platform line. The derived default carries no checksum (a checksum only makes sense once a version is pinned). If the runner is already visible on an input folder (`TestEngine.hasRunner(...)`, i.e. some scanned module matches the engine's `isRunner(...)`), the whole block is skipped - neither a coordinate nor a default version is written. - `dependencies` (`Dependencies`) takes the runner coordinate *together* with the project's already-transitively-resolved compile/runtime deps (carried from the upstream `dependencies.properties`) and runs the resolve a second time across the combined set in one step. The resolver dedups by coordinate key and negotiates a single version per key, so a transitive dependency the runner pulls in that the project already resolved collapses to one entry rather than producing two clashing module-path entries downstream. The same step then fetches the unified resolved set into the private `resolved/` folder and writes `dependencies.properties`, validating checksums when present and hard-linking from the local cache when available so the second resolve doesn't re-fetch jars the project's own resolve already brought down. - `executed` (`TestModule.Run` extends `Java`) accepts `filter`, `tag`, `parallel`, and `reporting` arguments. `filter` is a comma-separated list of Java regex entries, each `` or `#`; `tag` is a comma-separated list of test tags; `parallel` and `reporting` are flags. The **default value** of each is resolved from a `jenesis.test.*` system property (`jenesis.test.filter`, `jenesis.test.tag`, `jenesis.test.parallel`, `jenesis.test.reporting`): `TestModule`'s public constructor reads them once (`System.getProperty` / `Boolean.getBoolean`) to seed the fields. The matching `filter`/`tag`/`parallel`/ `reporting` withers override that default directly - an explicit value wins, including an explicit `null` or `false`, which does not fall back to the property. The assembler no longer reads these properties; it wires a bare `TestModule` whose defaults already carry them. The matched class and method names, the parsed tag set, and the two flags are handed to the engine's `commands(supplement, output, classes, methods, groups, parallel, reporting)`, which shapes them into the runner's argument syntax: `JUnitPlatform` emits `--select-class=` / `--select-method=` per entry, one `--include-tag=` per tag, the `junit.jupiter.execution.parallel.*` config parameters when parallel, and the `junit.platform.reporting.open.xml.enabled` / `junit.platform.reporting.output.dir` config parameters when reporting, writing the Open Test Reporting XML into a `reports/tests/` subfolder of the step's `output` folder (under the public `BuildStep.REPORTS` constant, so consumers can locate the reports; the listener ships in `org.junit.platform.reporting`, which the console runner already requires); `TestNG` joins the tags into a single comma-separated `-groups` argument and adds `-parallel methods`; `JUnit4` rejects tags (its console runner cannot select `@Category` by name) and ignores the parallel and reporting flags. `filter` and `tag` are part of the step's serialized state (so changing either invalidates the cache) and, when set, force the step to re-run regardless of cache consistency; `reporting` is also part of the serialized state (so toggling it invalidates the cache and re-runs to produce or drop the report) but, like `parallel`, is not forced to re-run on every invocation; `parallel` is transient - it changes how tests run, not their result, so toggling it neither invalidates the cache nor forces a re-run. `Java` dispatches each argument's `artifacts/` and resolved `runtime` jars to `--module-path` or `--class-path` through its `PathPlacement` - the same `PathPlacement.place` routine `Execute` uses to launch, so a descriptor-bearing or `Automatic-Module-Name` jar lands on the module path and a plain jar on the class path. Unless the launch is a **self-contained module graph** - a pure named-module path where every jar is an explicit module and nothing is on the class path, so the launched module's `requires` closure (plus service binding) resolves the whole path - it also adds `--add-modules ALL-MODULE-PATH`. Two things break self-containment: an automatic module, which declares no `requires` (so a named module it uses only internally, for example a Spring jar's transitive `commons-logging`, is otherwise never resolved), and a non-empty class path, whose unnamed module is readable only through modules that are themselves resolved. This is a launch-only concern - `javac` resolves just the `requires` closure the sources actually reference (and in fact forbids the flag when compiling a named module), so compilation never needs it. ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; arts(["inherited classes/
+ artifacts/
+ requires.properties"]):::input resolved["resolved
(Requires)"]:::step deps["dependencies
(Dependencies)"]:::step executed["executed
(Java/Run)"]:::step arts --> resolved resolved --> deps arts --> deps deps --> executed arts --> executed ``` ### `Dependencies` The single dependency-resolution step. It resolves and fetches external dependencies declared in `requires.properties`: it computes the transitive closure, fetches each jar directly into the private `resolved/` folder, and writes `dependencies.properties` mapping each resolved coordinate to its jar path and any checksum. `Dependencies` validates each fetched jar against the checksum carried in the resolved `dependencies.properties` value when one is present (sourced from POM/`module-info` pins, see the [`requires.properties`](#conventional-folders-and-files) row); coordinates without a pinned checksum are fetched without integrity validation. Single-language modules (the Kotlin/Scala/Groovy/Dokka compiler and documentation modules, and the `InternalModule` / `ExternalModule` / `TestModule` side-channels) add this step directly under the name `dependencies`, so a consumer references it as `dependencies` with no nested sub-step. The multi-project layouts (`MavenProject` / `ModularProject`) instead keep a `dependencies` submodule that wraps a `prepare` step (`MultiProjectDependencies`) ahead of this step, which is added there under the name `artifacts`, so the resolve output sits at `dependencies/artifacts`. ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; deps(["requires.properties"]):::input resolve["dependencies (Dependencies)
fetches resolved/ (private),
writes dependencies.properties"]:::step deps --> resolve ``` ### `MultiProjectModule` Used as the generic shape behind multi-project layouts. An *identifier* sub-module discovers the projects in a source tree and writes their coordinates and dependencies; a `Group` step partitions the cross-project dependency graph; a *factory* then assembles one sub-module per discovered project, wiring cross-project edges between them. Each per-project closure receives a `ProjectModule` exposing the module `name()`, its `dependencies()`, and the standardised inherited keys as helpers (`sources()`, `resources()`, `manifests()`, `coordinates()`, `artifacts(DependencyScope)`, `resolved(DependencyScope)`), each a `SequencedSet` so a descriptor may contribute several folders per kind (the concrete implementations are `ModularModuleDescriptor` and `MavenModuleDescriptor`), so a closure doesn't need to know how the identifier laid out its outputs. The example below shows two projects `A` and `B` where `A` requires `B`, so `B` is built first and its output flows into `A`. ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; classDef module fill:#ede9fe,stroke:#7c3aed,color:#4c1d95; inh(["inherited inputs"]):::input subgraph "identifier" direction TB idA["module-A
(identifier)"]:::module idB["module-B
(identifier)"]:::module end subgraph "build" direction LR group["group
(Group step)"]:::step subgraph "module" direction LR projB["B
(factory output)"]:::module projA["A
(factory output; requires B)"]:::module projB --> projA end group --> projA group --> projB end inh --> idA inh --> idB idA --> group idB --> group idA --> projA idB --> projB ``` ### `MavenProject` Used to drive a build from a Maven project layout. As the identifier inside a `MultiProjectModule`, it mirrors every `pom.xml` into `pom/`, parses each into a per-module `maven/.properties`, and emits one `module-X` sub-module per discovered POM containing source folders, optional resource folders, and a `manifests` step that writes the project's own coordinate (`identity.properties`) and its declared Maven dependencies (`requires.properties`). Each POM's `` block is captured into the same manifests step's `versions.properties`, so the resolver sees the project's BOM entries the same way it would see them if they had been declared in a top-level POM under resolution - pinning applies uniformly to declared dependencies and to transitives that aren't directly required. A `` element in the POM is captured by the same manifests step into a `process/javac.properties` sidecar with `--release=`, which `ProcessBuildStep` forwards to `javac`. A `` comment placed inside a `` `` lands as the optional `version checksum` suffix in `versions.properties` and is propagated by the resolver to whichever direct or transitive dependency resolves to that coordinate. `Dependencies` validates non-empty `dependencies.properties` checksums against the fetched bytes and fails the build on mismatch; coordinates without a pinned checksum are fetched without integrity validation. There is no on-the-fly hash computation in the build - validation is opt-in by declaring hashes in source. A `` `` with `import` (a BOM import) may carry the same comment; when the resolver downloads that referenced BOM POM during resolution, it streams the bytes through a digest and fails the build if they do not match the pinned hash, so the integrity story extends to the POMs the build pulls in for reference, not just to artifact jars. The `` block is the single source of truth for pins: a `` comment on a direct `` ``, on a ``, or in any fetched transitive POM is ignored, so a third-party dependency cannot pin (and thereby self-certify) the artifacts or POMs it pulls in. The `pin` step writes the full resolved closure into `` and strips checksum comments from direct `` elements, keeping that block authoritative. `MavenProject.make(...)` returns the full wrapped `MultiProjectModule` whose factory runs `prepare` (`MultiProjectDependencies`), `dependencies` (a submodule whose `prepare` step feeds the `Dependencies` step added there under the name `artifacts`), `build` (caller-supplied, typically `JavaToolchainModule`), `assign` (`Assign`), and `inventory` (`Inventory`, producing the per-module `inventory.properties` consumed by `Execute`) for each project. ```xml org.junit.jupiter junit-jupiter 5.11.3 ``` ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; classDef module fill:#ede9fe,stroke:#7c3aed,color:#4c1d95; tree(["project tree
with pom.xml files"]):::input subgraph "MavenProject (identifier)" direction LR scan["scan
(mirrors pom.xml
into pom/)"]:::step prepare["prepare
(writes maven/*.properties)"]:::step subgraph "module" direction LR idA["module-A
(sources, resources-N,
manifests step)"]:::module idB["module-B
(sources, resources-N,
manifests step)"]:::module end scan --> prepare --> idA prepare --> idB end subgraph "B (per project)" direction LR pBprep["prepare
(MultiProjectDependencies)"]:::step pBdeps["dependencies
(prepare + Dependencies)"]:::module pBbuild["build
(caller-supplied)"]:::module pBassn["assign
(Assign)"]:::step pBinv["inventory (Inventory)
writes inventory.properties"]:::step pBprep --> pBdeps --> pBbuild --> pBassn --> pBinv end subgraph "A (per project, requires B)" direction LR pAprep["prepare
(MultiProjectDependencies)"]:::step pAdeps["dependencies
(prepare + Dependencies)"]:::module pAbuild["build
(caller-supplied)"]:::module pAassn["assign
(Assign)"]:::step pAinv["inventory (Inventory)
writes inventory.properties"]:::step pAprep --> pAdeps --> pAbuild --> pAassn --> pAinv end pin["pin (sibling of build)
PinPom per module
reads inventory.properties"]:::module tree --> scan idA --> pAprep idB --> pBprep pBassn --> pAprep pAinv --> pin pBinv --> pin ``` ### `ModularProject` Used to drive a build from a Java-modular project layout. As the identifier inside a `MultiProjectModule`, it walks the source tree for `module-info.java` files and emits one sub-module per descriptor, each containing a `sources` source and a `manifests` step that parses the descriptor and writes `identity.properties` plus `requires.properties` from the Java `requires` directives. Javadoc tags of the form `@jenesis.pin // ` on the module declaration are captured into the same manifests step's `versions.properties` as a BOM-style pin map - the tag does not have to name a directly-required module, so a transitive can be pinned the same way: ```java /** * @jenesis.release 25 * @jenesis.pin org.junit.jupiter 5.11.3 * @jenesis.pin org.junit.platform.commons 1.11.4 */ open module build.jenesis.test { requires org.junit.jupiter; } ``` An `@jenesis.release ` tag on the module declaration (independent of the BOM pins above) is captured by the manifests step into a `process/javac.properties` sidecar containing `--release=`, which `ProcessBuildStep` forwards to `javac` when compiling the module. The same `@jenesis.pin` tag pins a coordinate in any group. The canonical key is `//`, where the group names the resolver closure (`main` for the project's own dependencies, a tool name for a compiler or plugin closure). Two abbreviations expand into the default `main` group, keyed off the slash count of the token: a bare `` name (no slash) is short for `main/module/`, and a `/` Maven coordinate (one slash) is short for `main/maven//`. So `org.junit.jupiter` is a module pin, `org.slf4j/slf4j-api` a Maven pin, and the long form `kotlinc/maven/org.jetbrains.kotlin/...` pins a non-`main` (compiler) group; a Maven coordinate that carries a type or classifier (more than one slash in the coordinate) has to be written in full. All forms land in the same `versions.properties` under their canonical `//` key, and a separate group (a compiler closure, a plugin closure) resolves separately from the module's own dependencies. A `@jenesis.plugin` tag declares a compile-time processor dependency: a Java annotation processor (JSR-269), a Kotlin compiler plugin, or a Scala compiler plugin. The grammar is `@jenesis.plugin /` (or a bare module name) for a Java annotation processor, which Jenesis resolves under group `plugin`, scope `plugin`; `@jenesis.plugin /` names a compiler group first (`@jenesis.plugin kotlinc maven/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin`) and resolves under that compiler's own group with scope `plugin` (here group `kotlinc`, so `kotlinc/plugin/maven/...`). No entry point is named - annotation processors (`META-INF/services`), Kotlin compiler plugins (`CompilerPluginRegistrar`) and Scala plugins (`scalac-plugin.xml`) all self-describe, so the dependency alone suffices. The declaration is resolved where the module's own dependencies are - in `ModularProject.Manifests`, which records an annotation processor under group `plugin`, scope `plugin` and a compiler plugin under the compiler's own group (`kotlinc`, `scalac`, `groovyc`), scope `plugin`. The two never collide: an annotation processor keeps group `plugin` while a compiler plugin moves to the compiler-qualified group, so `kotlinc/plugin/...` and `plugin/plugin/...` never overlap, and the compile step distinguishes the compiler's own jars (scope `runtime` under the compiler group) from its plugin jars (scope `plugin`). A `plugin`-group dependency that is also a `requires static` is still pinned independently. The plugin scopes resolve and download like any other scope (with runtime semantics, since a processor is *executed* by the compiler and needs its full closure), but are build-time-only (pinned, but never a runtime dependency of the produced module). The compiler chain consumes the resolved plugin set and each compiler picks its own plugins **by group**: the Java compiler takes the `plugin`-group jars and hands them to `javac` as an explicit processor path; the Kotlin compiler takes its `kotlinc`-group scope `plugin` jars; the Scala compiler takes its `scalac`-group scope `plugin` jars. This is what lets the group axis serve every language and auto-scale to future ones. The Java path is `--processor-module-path` when the module compiles to the module path (the common case, and the only flag that can run a modular processor whose service is declared via `module-info`'s `provides`), or `--processor-path` for a class-path compile; the two flags are mutually exclusive in `javac`, so the whole set goes on one, chosen by the module's own `PathPlacement`. Because the path is explicit, `javac` runs only the declared processors and never scans the compilation class or module path - a dependency that happens to bundle a processor stays dormant unless it is declared. Versions are pinned the usual way, with the `pin` step writing back the `@jenesis.pin` line automatically: ```java /** * @jenesis.release 25 * @jenesis.plugin org.immutables.value * @jenesis.pin org.immutables.value 2.12.2 SHA-256/abcdef0123... * @jenesis.pin plugin/module/org.immutables.value 2.12.2 SHA-256/abcdef0123... */ module sample { requires static org.immutables.value; } ``` Each compiler routes its share of the scope `plugin` jars to that compiler's plugin flag: `javac` (the group `plugin` jars) to the processor path, the Kotlin compiler (its `kotlinc`-group scope `plugin` jars) to `-Xplugin=`, the Scala compiler (its `scalac`-group scope `plugin` jars) to `-Xplugin:`. Each compile step matches only its own module's plugin artifacts (never a sibling module's), so a processor or plugin is loaded only by the compiler it was declared for. `demo/demo-10-annotations` runs a Java annotation processor (Immutables) and `demo/demo-18-kotlin-plugin` a Kotlin compiler plugin (kotlinx.serialization) this way - each showing that the jar is not run until it is declared; the Scala path is wired identically. An optional space-separated `/` after the version on a `@jenesis.pin` Javadoc tag (e.g. `@jenesis.pin org.junit.jupiter 5.11.3 SHA256/abcdef0123...`) is captured into the same `versions.properties` value (as `version checksum`); the resolver propagates it to whichever transitive resolves to that bare module name, so `Dependencies` validates the bytes against it. There is no on-the-fly hash computation in the build - validation is opt-in by declaring hashes in source. `ModularProject.make(...)` returns the full wrapped `MultiProjectModule` whose factory runs `prepare` (`MultiProjectDependencies`), `dependencies` (a submodule whose `prepare` step feeds the `Dependencies` step added there under the name `artifacts`), `build` (caller-supplied, typically `JavaToolchainModule`), `assign` (`Assign`), and `inventory` (`Inventory`, producing the per-module `inventory.properties` consumed by `Execute`) for each project. ```mermaid flowchart LR classDef input fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef step fill:#fef3c7,stroke:#92400e,color:#78350f; classDef module fill:#ede9fe,stroke:#7c3aed,color:#4c1d95; tree(["project tree
with module-info.java files"]):::input subgraph "ModularProject (identifier)" direction LR idA["module-A
(sources + manifests step
writing identity + requires)"]:::module idB["module-B
(sources + manifests step
writing identity + requires)"]:::module end subgraph "B (per project)" direction LR pBprep["prepare
(MultiProjectDependencies)"]:::step pBdeps["dependencies
(prepare + Dependencies)"]:::module pBbuild["build
(caller-supplied)"]:::module pBassn["assign
(Assign)"]:::step pBinv["inventory (Inventory)
writes inventory.properties"]:::step pBprep --> pBdeps --> pBbuild --> pBassn --> pBinv end subgraph "A (per project, requires B)" direction LR pAprep["prepare
(MultiProjectDependencies)"]:::step pAdeps["dependencies
(prepare + Dependencies)"]:::module pAbuild["build
(caller-supplied)"]:::module pAassn["assign
(Assign)"]:::step pAinv["inventory (Inventory)
writes inventory.properties"]:::step pAprep --> pAdeps --> pAbuild --> pAassn --> pAinv end pin["pin (sibling of build)
PinModuleInfo per module
reads inventory.properties"]:::module tree --> idA tree --> idB idA --> pAprep idB --> pBprep pBassn --> pAprep pAinv --> pin pBinv --> pin ``` ### `ExternalModule` Used for loading a `BuildExecutorModule` from a published modular jar at build time, so a build can pull in plug-in modules from a repository instead of vendoring them as source. The plug-in must ship as a Java module that declares `provides build.jenesis.BuildExecutorModule with ...;` in its `module-info.java`. Given a `/` string, a `scope` (the scope to record the tool's closure under, e.g. `compile`), a map of `Repository` instances, and a map of `Resolver` instances, `ExternalModule` registers three internal nodes: - `coordinate` writes the requested coordinate (plus any added via `dependencies(...)`) into a fresh `requires.properties`. It also scans its inherited inputs for a `versions.properties` and copies through every entry in this module's own scope (`//...`) as the plug-in's `versions.properties`, so the project's `// ` pins govern the version and integrity of the plug-in's resolved closure (including transitive modules such as `build.jenesis` itself). To activate this, the host forwards its `manifests` step when registering the module. - `dependencies` is a `Dependencies` step that resolves and fetches the coordinate's transitive closure using the supplied repositories and resolvers, pinning each coordinate to the forwarded versions and validating its checksum on fetch. The plug-in's own `requires build.jenesis;` is followed by the resolver, so its copy of Jenesis is fetched alongside its other dependencies. - `delegate` builds a fresh `ModuleLayer` over the downloaded jars and runs the plug-in's `BuildExecutorModule.accept(...)` against the same inherited folders `ExternalModule` itself received (see *Plug-in isolation* below). `ExternalModule.resolve(...)` hides the three internal nodes from the published output map and strips the `delegate/` prefix from the delegated module's outputs, so the plug-in's nodes surface under `ExternalModule`'s registered name. The hidden steps still execute and participate in the cache; only their published names disappear. ```java new ExternalModule("module/com.example.plugin", "compile", repositories, resolvers) .dependencies("module/com.example.extra"); ``` ### `InternalModule` Used for loading a `BuildExecutorModule` from a local source folder, so a plug-in can be developed alongside the project that consumes it without first publishing it to a repository. The source folder must contain a `module-info.java` that `provides build.jenesis.BuildExecutorModule with ...;` - the plug-in still has to ship as a Java module, just one built from source instead of pulled from a repository. `InternalModule` takes a `repository`, a `scope` (the scope to record the closure under, e.g. `compile`), a `Path` to the source folder, a `Repository` map, and a `Resolver` map - with a three-argument `(repository, scope, source)` convenience form that defaults the two maps to the Jenesis module repository with the local export prepended - and registers: - `source` binds the source folder as Java sources via `Bind.asSources()`. - `compile-requires` and `runtime-requires` each parse the source's `module-info.java` and write a `requires.properties` for the corresponding scope (`requires` for compile, `requires` minus `static` for runtime). Each entry is keyed `//`, so the resolver for that repository can look it up. Both steps also scan their inherited inputs for a `versions.properties` and copy through every entry in this module's own scope (`//...`) as the plug-in's `versions.properties`, so the project's `// ` pins govern the version and integrity of the plug-in's resolved closure (the `Dependencies` step pins the version and validates the checksum). To activate this, the host forwards its `manifests` step alongside the project sources when registering the module. Both steps re-run when `sources/module-info.java` or a forwarded `versions.properties` changes. - `compile` and `runtime` are scope-specific `Dependencies` steps that fetch the two classpaths separately. - `java` is a `JavaToolchainModule` that compiles the source against the compile classpath. - `delegate` builds a `ModuleLayer` over the compiled jar plus the runtime classpath and runs the plug-in. `dependencies(...)` adds extra coordinates (written verbatim, no prefix added) to both `requires.properties` files, for plug-ins that need modules not declared in their own `module-info.java`. The source must declare `requires build.jenesis;` (plus whatever else it uses from Jenesis); the resolver fetches Jenesis like any other module dependency. `InternalModule` errors at build time if the source lacks `module-info.java`. ### Plug-in isolation Both modules load the plug-in into a fresh `ModuleLayer` whose ClassLoader parent is the platform loader, not the host's application loader. As a result: - The host's `build.jenesis` classes are invisible to the plug-in. The plug-in pulls in its own copy of Jenesis via its `requires build.jenesis;` declaration; the resolver downloads it like any other module. The two copies are different `Class` instances in different loaders. - Two plug-ins with conflicting transitive dependencies do not clash, because they each get their own layer with their own copy of every non-platform module. Because the host can't simply call methods on the loaded plug-in (the types live in a different loader), `JenesisClassLoaderBridge` mediates: it creates a `java.lang.reflect.Proxy` implementing the *foreign* `BuildExecutor` and hands that proxy to the plug-in's `accept(...)`. As the plug-in calls `addStep`, `addModule`, etc. on the proxy, the bridge: - Forwards default-method calls through `InvocationHandler.invokeDefault`, so the abstract overloads are all the bridge has to special-case. - Wraps each `BuildStep` argument as a host `BuildStep` that, on `apply(...)`, translates the host's `BuildStepContext` / `BuildStepArgument` into foreign records (via `MethodHandle`s bound to the foreign record constructors), invokes the foreign step's `apply` via `MethodHandle`, and translates the foreign `BuildStepResult` back. `ChecksumStatus` enum values are mapped across loaders by name. - Wraps each `BuildExecutorModule` argument as a host `BuildExecutorModule` that recursively re-enters the same bridge when invoked, so plug-ins can register nested modules. - Passes everything else (Strings, `Path`s, `SequencedMap`, `Function>`) through unchanged - those types live in `java.base` and are shared across loaders. The reflective calls use `MethodHandles`, and the bridge uses `ModuleLayer.Controller.addOpens` plus `MethodHandles.privateLookupIn` to obtain a lookup whose accessing class is in the foreign loader. That lookup is required so the JVM's loader-constraint check (which would otherwise complain that the host and foreign loaders define different `BuildExecutor` `Class` objects for the same name) does not trip. ### Picking a specific plug-in: `@BuildModuleName` and `buildModuleName(...)` The plug-in's layer must resolve to **exactly one** `BuildExecutorModule` service provider. By default (no `buildModuleName(...)` call), only providers without a `@build.jenesis.BuildModuleName` annotation qualify. Annotated providers are selected explicitly: ```java package com.example; @BuildModuleName("publish") public class Publish implements BuildExecutorModule { ... } ``` ```java new ExternalModule("module/com.example.plugin", repositories, resolvers) .buildModuleName("publish"); ``` `InternalModule` exposes the same `buildModuleName(String)` factory. If zero or more than one provider match the (possibly null) requested name, the build fails at `delegate` time with a descriptive error. This makes it possible to ship multiple entry points in a single plug-in jar and let the consumer pick which one to run per `ExternalModule` / `InternalModule` instance. Implementing a `BuildStep` -------------------------- A custom `BuildStep` is a serializable functional implementation of: ```java CompletionStage apply(Executor executor, BuildStepContext context, SequencedMap arguments); ``` A few rules of thumb for new steps: - **Write only into `context.next()`.** Treat predecessor folders (`argument.folder()`) and `context.previous()` as read-only; use `context.supplement()` for scratch files that should not be published. The "immutable output folder" invariant is what makes downstream caches correct. - **Use `shouldRun(...)` for finer dependencies.** The default re-runs the step whenever any input checksum changed. Overriding it lets a step ignore subtrees that do not affect its output (`Bind` only watches its bound paths). - **Decide what counts as "configuration".** Every non-`transient` field is folded into the step's configuration hash via `ObjectOutputStream.writeObject(step)`. Anything that should *not* count as part of the build's identity - a `Repository` that by contract returns the same artifact for the same coordinate, a JDK service factory, a `MavenPomEmitter` - must be marked `transient` so swapping equivalent backends does not invalidate the cache. Conversely, fields that *do* affect the output (a sort order, a placement function, a flag list) must stay non-transient. - **Hold lambdas through serializable bounds.** Constructors that take functional values should declare an intersection bound (` & Serializable>`) so the compiler emits a serializable lambda. A step that holds a non-serializable value will fail outright when `BuildStepHashFunction.ofSerializationDigest` tries to serialize it for the configuration hash, propagating a `NotSerializableException`. This is intentional: silent fallback would hide a bug that breaks cache invalidation, so the surface is loud at build time instead. `java.nio.file.Path` is the one exception that is *not* a bug to hold: the digest stream substitutes any `Path` for its `toString()`, making it a serializable participant in the configuration hash by design even though the JDK's concrete `Path` implementations don't implement `Serializable`. - **Bump `serialVersionUID` to communicate code changes.** The cache's notion of "configuration" is the step's *serialized form* - the values of its non-transient fields plus the class's `serialVersionUID`. Editing the body of `apply(...)` (fixing a bug, changing a tool flag, switching to a different output layout) does not alter the serialized form, and therefore does **not** invalidate previously cached outputs. To force a rebuild after such a change, increment the step's `serialVersionUID`: the new value flows into the stream's class descriptor, the configuration hash changes, and every previously cached run of that step re-executes. (`-Djenesis.executor.rebuild=true` achieves the same thing globally, but discards every other step's cache too.) - **Return a meaningful `BuildStepResult`.** A successful result with `next() == true` atomically promotes `context.next()` over the previous run. A result with `next() == false` keeps the previous folder and discards the temp (useful for steps that detect their inputs would yield the same output as last time). A failed `CompletionStage` or a thrown exception deletes the temp and propagates as a build failure. Steps that follow these rules participate fully in incremental builds: identical inputs and identical configuration reuse the previous output unchanged, and a meaningful change anywhere - fields, `serialVersionUID`, or predecessor outputs - re-executes the step into a fresh `next` folder. Configuration ------------- The following system properties and environment variables tune the build at launch time. | Name | Kind | Effect | | ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `jenesis.executor.digest` | system property | Algorithm passed to `HashDigestFunction` and `BuildStepHashFunction.ofSerializationDigest(...)` by `BuildExecutor.of(Path)` for both the per-file content checksums (used to compute step input/output diffs) and the per-step serialization hash (used to detect config changes). Any `MessageDigest` algorithm name is accepted. Default `MD5`. | | `jenesis.print.progress` | system property | Default `true`. Gates the per-step build progress lines (`[STARTED]`/`[EXECUTED]`/`[RESOLVED]`/`[SKIPPED]`/`[COMPLETED]`): when `true`, `BuildExecutor.Configuration.of(target)` wires `BuildExecutorCallback.printing(...)`; when `false`, it wires `BuildExecutorCallback.nop()` so the build runs silently. On by default, like `jenesis.print.docker`; the other `jenesis.print.*` flags are off by default. | | `jenesis.print.checksum` | system property | When `true`, the default `BuildExecutorCallback.printing(...)` appends each executed step's input/output file checksums under its `[EXECUTED]` line, instead of just the high-level status line. Default `false`. | | `jenesis.print.command` | system property | When `true`, `ProcessBuildStep` prints the full command line of each external tool it launches (a `[EXECUTED] ` line). Default `false`. | | `jenesis.print.process` | system property | When `true`, every `ProcessBuildStep` runs in verbose/debug mode: the command line and every line the launched tool writes are echoed to `System.out`, each prefixed with ` >>>>` and coloured by origin (a muted grey for the output stream, a muted red for the error stream). The lines are still captured to the `output`/`error` supplement files exactly as before; they are additionally streamed line by line as they are produced. For a forked process the output and error streams are drained concurrently through the build `Executor` so the process never blocks on a full pipe. This is the generic default; a per-command property `jenesis.print.` (e.g. `jenesis.print.javac`, `jenesis.print.checkstyle`, `jenesis.print.native-image`) overrides it for that one command, and the `ProcessBuildStep.verbose(boolean)` wither (inherited by every implementation) overrides both in code. The modules that wrap a forked tool (the language compilers, the code-quality and formatting checks, coverage, mutation, documentation, and `TestModule`) expose the same override as a `printing(boolean)` wither that resolves the per-command property when left unset. Default `false`. | | `jenesis.print.tests` | system property | The per-command form of `jenesis.print.process` for the forked test JVM: `-Djenesis.print.tests=true` streams the test process's command and output to the console. `TestModule`'s runner resolves its verbose flag under the `tests` key (rather than the `java` command it actually launches under), so test output can be toggled independently of other `java`-launched steps. The `TestModule.printing(boolean)` wither overrides it in code (leaving it unset resolves the property). Default `false`. | | `jenesis.print.fetch` | system property | When `true`, repositories print a `[FETCHED]` line for every artifact they download (`Repository.cached`, `Repository.ofUris`, and `MavenDefaultRepository`). Default `false`. | | `jenesis.print.cache` | system property | When `true`, `BuildExecutorCache.printing(...)` wraps the configured shared build cache so each step served from it prints a `[LOADED]` line and each step written to it a `[STORED]` line (the same `[FETCHED]`-style trace as `jenesis.print.fetch`). Effective whenever a cache is configured (`jenesis.cache.uri` and/or `jenesis.project.cache`), across the file, HTTP, and layered backends. Default `false`. | | `jenesis.print.docker` | system property | When `true`, `Project` (build-in-container) and `Execute` (run-in-container) print the Docker image they wrap the JVM in. Default `true` (set `false` to suppress), the same default as `jenesis.print.progress`. | | `jenesis.executor.timeout` | system property | ISO-8601 duration (e.g. `PT5M`, `PT30S`) applied to every `BuildStep` by `BuildExecutor.of(Path)`. Each step's returned `CompletionStage` is wrapped with `orTimeout`, so the build fails fast with a `TimeoutException` (surfaced as a `BuildExecutorException`) when a step exceeds the limit. Note that the future is only completed exceptionally; the underlying virtual thread is not interrupted and only winds down when the surrounding `ExecutorService` closes at the end of the build. Default `PT0S` disables the timeout. | | `jenesis.executor.rebuild` | system property | When `true`, `BuildExecutor.of(Path)` recursively deletes the target folder before constructing the executor, forcing a full rebuild from a clean tree. Equivalent to `rm -rf target/` ahead of the build. The `of(target, timeout, hash, stepHash, callback, rebuild)` overload accepts the flag directly; the convenience `of(Path)` overload reads this property. Default `false`. | | `jenesis.process.factory` | system property | Selects how the JDK tool steps (`Javac`, `Javadoc`, `Jar`, `JMod`, `JLink`, `JPackage`) launch: `tool` runs them in-process via `ToolProvider` (the default), `fork` runs them as separate `java`-home processes (`bin/javac`, …) - use it under stricter sandboxes that disallow in-process tool runs. Read once by `ProcessHandler.Factory.of()` wherever a step is constructed. Unset defaults to `tool`, except in a GraalVM native image (which has no in-process JDK tools), where it defaults to `fork`. An unknown value fails fast. | | `jenesis.test.filter` | system property | Read by `TestModule` as an implicit default for the test filter: a comma-separated list of `[#]` entries. When set, `TestModule.executed` only emits selectors for classes (and optionally methods) matching those patterns, and the value becomes part of the step's serialized state, forcing a re-run. `TestModule` applies it **unless** the `filter(String)` wither was set explicitly (an explicit wither always wins); the assembler wires a bare `TestModule` and no longer reads the property itself. | | `jenesis.test.tag` | system property | Read by `TestModule` as an implicit default for the test tag selector (overridable by the `tag(String)` wither): a comma-separated list of tags mapped per framework (JUnit Platform tags, TestNG groups). Like the filter it becomes part of the step's serialized state and forces a re-run when set; `JUnit4` rejects it, since its console runner cannot select `@Category` by name. | | `jenesis.test.engine` | system property | Read by `TestModule`'s default constructor to force the test engine instead of auto-detecting it: `junit-platform` (`JUnitPlatform`), `junit4` (`JUnit4`), or `testng` (`TestNG`), case-insensitive; an unknown value fails fast. Unset (the default) leaves the engine `null`, so `TestModule` resolves it from the test module's resolved dependencies (`TestEngine.of(...)`) - the right choice for almost every project. The `engine(TestEngine)` wither overrides the property in code. Forcing it is for the ambiguous case where more than one engine is on the test path and auto-detection would pick the wrong one. | | `jenesis.test.parallel` | system property | Read by `TestModule` as an implicit default (overridable by the `parallel(boolean)` wither); enables parallel test execution where the framework supports it (JUnit Platform parallel config parameters, TestNG `-parallel methods`; ignored by `JUnit4`). It is transient on the test step, so toggling it neither invalidates the cache nor forces a re-run. | | `jenesis.test.reporting` | system property | Read by `TestModule` as an implicit default (overridable by the `reporting(boolean)` wither); when `true`, `JUnitPlatform` enables the JUnit Platform Open Test Reporting listener (`junit.platform.reporting.open.xml.enabled` plus `junit.platform.reporting.output.dir`), writing the lossless Open Test Reporting XML into a `reports/tests/` subfolder (under the public `BuildStep.REPORTS` constant) of the test step's `output` folder. The listener ships in `org.junit.platform.reporting`, which the console runner (`org.junit.platform.console`) already requires, so no extra coordinate is resolved. `TestNG` and `JUnit4` ignore the flag. Unlike `filter`/`tag` it is part of the step's serialized state without forcing a re-run, so toggling it invalidates the cache and re-runs to produce or drop the report, but a cached run with the flag unchanged is reused. Default `false`. | | `jenesis.test.incremental` | system property | Read by `TestModule` when it wires the test step (alongside the existing `jenesis.test.skip` check). When set, and no explicit `filter`/`tag` is in effect, the test step narrows execution to the test classes a change can reach: it builds a class-to-test dependency graph from the compiled bytecode (the test classes plus the code under test, read from `classes/` directories and resolved runtime jars), records a per-class content hash as `test-selection.properties` in its own output, and on the next run diffs that snapshot (via `context.previous()`), takes the changed classes, and emits `--select-class` only for the tests that transitively reference them. The property value names the message-digest algorithm used for that snapshot (e.g. `MD5` or `SHA-256`); an empty value defaults to `MD5`, matching the executor's own change-detection digest. A change that reaches no test runs nothing; a removed class or any non-class change falls back to the full set. It is transient on the step (toggling it neither invalidates the cache nor forces a re-run), and is intended for `jenesis.project.watch` development loops, not as a correctness gate, since static selection cannot observe reflection or other indirect couplings. Default unset (selection off). See *Watching for changes*. | | `jpackage` | `packaging.properties` | A key in the `packaging.properties` file `InferredMultiProjectAssembler` reads from the configuration location (a module's `META-INF/build.jenesis/` folder on the modular layouts, or its `build.jenesis/` folder on `MAVEN`, falling back to the project-wide configuration directory, the project root by default; the first match wins, so a per-module file overrides a project-wide one, selecting bundles per module or for all modules at once). When set, it wires a `jpackage` step in the package phase that runs `jpackage` for every module declaring a main class (a `@jenesis.main` Javadoc tag, or a `` POM property); modules without one are skipped. The value is the `jpackage --type` and must be given explicitly: `jpackage=app-image` (a self-contained launcher plus runtime, the only type that needs no platform-native tooling) or `jpackage=deb|rpm|dmg|pkg|exe|msi` (a native installer). An absent or empty value means no per-module `jpackage` step is wired, so nothing is produced. The `--name` and the launcher are derived by the `prepare` step from the module's metadata and main class: a module built for the module path (`descriptor.pathPlacement().modular()`, i.e. the `MODULAR`/`MODULAR_TO_MAVEN` layouts) gets a modular launcher (`--module /`, run from `--module-path`) - and unless its staged jars form a self-contained module graph (all explicit named modules), the step adds `--java-options --add-modules=ALL-MODULE-PATH` so the generated launcher roots the whole module path. Both an automatic module (declares no `requires`, so a named module it uses only internally, such as a Spring jar's `commons-logging`, is otherwise unresolved) and a plain jar (no descriptor, which becomes a filename-derived automatic module on the module path) break self-containment; jpackage stages everything into one `input/` directory, so there is no separate class path to consider. A classpath module (the `MAVEN` layout) instead gets `--main-jar`/`--main-class` run from `--input`. Each produced image is recorded in `inventory.properties` as `prefix.package`, and the `STAGE` module's `packages` step (always registered, alongside `maven`/`modular`) collects every module's image into `stage/packages/` - the staging analogue of `stage/maven`/`stage/modular`. When no module produced an image, the `packages` stage step simply stages nothing. Application images are staged but never exported (they do not belong in a Maven or module repository). | | `jmod` | `packaging.properties` | A boolean key in `packaging.properties` (located as described for `jpackage`). When `true`, it wires a per-module `jmod` step that runs the `jmod` tool over every modular module's `classes/`, producing `jmods/.jmod` (modules with no `module-info.class` are skipped). `Inventory` records it as `prefix.jmod`, and `ModularStaging` hardlinks it **alongside the jar** as `/[/].jmod` in `stage/modular`. A `.jmod` is the link-time form of the module (consumable by `javac`/`jlink`, never at runtime), which is why it is staged into the module repository next to the jar rather than into `artifacts/`. Default `false`. | | `jlink` | `packaging.properties` | A boolean key in `packaging.properties` (located as described for `jpackage`). When `true`, it wires a `jlink` step in the package phase that runs the `jlink` tool over every modular module plus its runtime dependency jars, producing a custom runtime image in `runtime/`. The root module (`--add-modules `) is supplied by the `prepare` step from the module's name, so non-modular modules (which get no `--add-modules`) are skipped. `Inventory` records the image as `prefix.image`, and the `STAGE` module's `runtime` step (`ImageStaging`) collects every module's image into `stage/runtime/`. Like the runtime image embedded by `jpackage`, it is staged but not exported. When `jmod=true` is also set, `jlink` links from the produced `.jmod` (the richer link-time form, which can carry native libraries `jlink` places into the image) instead of the jar - the two never both feed `jlink`, which would be a duplicate module. Default `false`. | | `bundle` | `packaging.properties` | A boolean key in `packaging.properties` (located as described for `jpackage`). When `true`, it wires a `bundle` step in the package phase that writes `bundle/bundle.zip` for every module declaring a main class (a `@jenesis.main` Javadoc tag, or a `` POM property); modules without one are skipped. The zip carries every jar the module's launcher needs, split into `classpath/` and `modulepath/` folders (the modular split mirrors `Execute`: real and automatic modules go on the module path, the rest on the class path; a non-modular launcher puts everything on the class path), plus an `application.properties` with a `mainClass` property and, for a modular launcher, a `mainModule` property (and, when `modulepath/` is non-empty, a `selfContainedModuleGraph=true|false` flag - `false` signals the consumer must add `--add-modules ALL-MODULE-PATH` because an automatic module or class-path jar is present). It is meant as a single, self-contained input for building a container image or other deployment, the lightweight counterpart to `jpackage` (which invokes the `jpackage` tool). Default `false`. | | `launcher` | `packaging.properties` | A boolean key in `packaging.properties` (located as described for `jpackage`). When `true`, it wires a `launcher` module (`LauncherModule`) in the package phase for every module declaring a main class. The module resolves the published `build.jenesis:build.jenesis.launcher` artifact (floating `RELEASE`, in its own `launcher` group) from the configured repositories, then a `Launcher` step produces a **single executable jar** (`launcher/.jar`): the launcher's `build/jenesis/launcher/*.class` are shaded into the jar root as the `Main-Class` (`build.jenesis.launcher.Launcher`, its own `module-info`/manifest dropped), each dependency is exploded into its own `classpath//` or `modulepath//` subfolder (the same modular split as `bundle`/`Execute`), and `application.properties` carries `mainClass`, `mainModule`, and the `classpath` order. At run time the launcher reconstructs the module graph from those subfolders, so `java -jar foo.jar` keeps full modularity without flattening into a fat jar. The counterpart to `bundle` that needs no launch script. Default `false`. | | `native` | `packaging.properties` | A boolean key in `packaging.properties` (located as described for `jpackage`). When `true`, it wires a `native-image` step into the **package phase** (the cross-module level that runs after every module's build) for every module declaring a main class (a `@jenesis.main` Javadoc tag, or a `` POM property), ahead-of-time compiling the launcher and its closure into a single native executable; modules without a main class are skipped. The launcher is read from the neutral `launcher.properties` descriptor the `prepare` step writes (`mainModule`/`mainClass` for a modular launcher, otherwise `mainClass` with the jars on the class path) - not `jpackage`'s own command file. The closure is split the same way the run does (descriptor-bearing jars onto `--module-path`, plain jars onto `-cp`); unless that is a self-contained module graph (a pure named-module path), it additionally gets `--add-modules ALL-MODULE-PATH` - the same root-the-whole-module-path fix the run uses, triggered by an automatic module or a class-path jar. The reachability metadata captured by the module's test (enabled by a `graal.properties` file, see below) is collected from the sibling inventories by a `reachability` step (`NativeImageMetadata`, routing each capture by the `prefix.test`/`prefix.artifact` pair) and passed as `-H:ConfigurationFileDirectories`, so the image builds in one pass with no committed `META-INF/native-image/`. Each produced executable is recorded in `inventory.properties` as `prefix.native`, and the `STAGE` module's `native` step (`ImageStaging`, always registered alongside `packages`/`runtime`) collects every module's binary into `stage/native/` - the native-image analogue of `stage/packages` (jpackage) and `stage/runtime` (jlink). Like those images it is staged but never exported (a native binary does not belong in a Maven or module repository). Unlike the JDK tool steps, `native-image` is always an external process located via `GRAALVM_HOME`, then `java.home`, then `PATH`, so a GraalVM that ships `native-image` must be installed. Default `false`. | | `jenesis.sbom.cyclonedx` | system property | Read by `InferredMultiProjectAssembler`. A CycloneDX software bill of materials is emitted **by default**: the assembler wires a per-module `sbom` step (before the jar is sealed) that emits it in-process from the module's resolved dependency graph, content hashes, and captured licenses (already normalized to SPDX identifiers at extraction, so each is emitted as a CycloneDX `license.id` when the identifier is recognized, otherwise as a `license.name`). This property is a default-`true` **suppress** override (a boolean): set `-Djenesis.sbom.cyclonedx=false` to skip the SBOM entirely, regardless of any file. The format is chosen by an optional `sbom.properties` in the configuration directory: `format=json` (the default, also used when the file or the `format` key is absent) or `format=xml` selects the format, `format=none` disables the SBOM, and any other value fails the build with an `IllegalArgumentException`. The document is placed three ways: **embedded** in the jar at `META-INF/sbom/.` (with `Sbom-Format`/`Sbom-Location` `MANIFEST.MF` headers and a `META-INF/NOTICE`); recorded in `inventory.properties` as `prefix.report.sbom` and collected by `ReportStaging` into `stage/reports/sbom//`; and, when a Maven repository is staged (`MAVEN`/`MODULAR_TO_MAVEN`), attached as `--cyclonedx.` next to the pom and jar so `export` publishes it to Maven Central. The document describes the project itself as its `metadata.component` from the captured POM metadata (its declared license, its developers as CycloneDX `authors`, its `description`, and its homepage/SCM as `website`/`vcs` external references), filling in only what is declared: an unset version (the `1-SNAPSHOT` placeholder) is omitted rather than fabricated. It also carries a `serialNumber` (`urn:uuid:...`) derived deterministically from the document content (a name-based UUID, no wall-clock `timestamp`), so the SBOM stays byte-for-byte reproducible. Default `true`. | | `jenesis.source.` | system property | Read by `InferredSourceCodeQualityModule` (wired by `InferredMultiProjectAssembler` as the per-module `check` step), one boolean per source linter: `checkstyle`, `pmd`, `detekt`, `ktlint`, `scalastyle`, `scalafmt`, `codenarc`. Each defaults to `true`, so a tool is wired whenever its configuration file is present (`checkstyle.xml` for Checkstyle, `pmd.xml` for PMD, and so on); setting the property to `false` keeps the configuration file in place but skips that one tool while the others stay wired. The matching `.(boolean)` wither on the module overrides the property in code (defaults-then-overrides). Default `true`. | | `jenesis.validator.spotbugs` | system property | Read by `InferredByteCodeQualityModule` (wired as the `binary` step's `validator`, since SpotBugs reads compiled classes), the byte-code-quality counterpart of `jenesis.source.`: SpotBugs is wired whenever `spotbugs-exclude.xml` (or `spotbugs.xml`) is present, and `false` skips it without deleting the filter file. The `.spotbugs(boolean)` wither overrides the property. Default `true`. | | `jenesis.format.java`, `jenesis.format.rewrite`, `jenesis.format.` | system property | Read by `InferredSourceFormattingModule` (wired as the per-module `format` step). A Java formatter is selected by a `javaformat.properties` file (`formatter=google\|palantir`), the same way ktlint / scalafmt activate from `.editorconfig` / `.scalafmt.conf`; `jenesis.format.java` / `jenesis.format.ktlint` / `jenesis.format.scalafmt` (each default `true`) switch the respective formatter off. `jenesis.format.rewrite` (default `false`) flips the whole chain from verify mode (a CI gate that fails on an unformatted source without writing it) to rewriting sources in place (it sets `.verify(false)` on each formatter). Default: no Java formatter unless a `javaformat.properties` file is present, ktlint/scalafmt on, verify mode. | | `jenesis.observe.jacoco` | system property | Read by `InferredTestObservationModule` (wired as the per-module `observed` step that wraps the test run). JaCoCo is enabled by the presence of a `jacoco.properties` file in the configuration directory (the project root by default), which adds JaCoCo as a coverage agent that `-javaagent`-attaches to the test JVM and writes `jacoco.exec`, which the `JaCoCoModule` then renders into an HTML/XML report under `reports/jacoco/`. This property is a default-`true` **suppress** override: set it to `false` to switch JaCoCo off even when the file is present (the test run is then unwrapped). The `.jacoco(boolean)` wither sets it directly in code. Default `true`. | | `jenesis.observe.native` | system property | Read by `InferredTestObservationModule`, the GraalVM counterpart of `jenesis.observe.jacoco`. The tracing agent is enabled by the presence of a `graal.properties` file in the configuration directory, which adds the `NativeImageAgent` engine, `-agentlib:native-image-agent=config-output-dir=...`-attaching the GraalVM tracing agent to the test JVM so it records the reflection, JNI, resource, proxy and serialization the tests exercise. This property is a default-`true` **suppress** override: set it to `false` to switch the agent off even when the file is present. The agent ships in the GraalVM runtime (no coordinate to resolve), so the build must run on a GraalVM JDK (or with `GRAALVM_HOME` set, as the test JVM inherits the build's `java.home`); on a stock JDK the test run fails to load the agent. `NativeImageAgentModule` then stages the captured config (`reachability-metadata.json` and friends) under `nativeimage/` - a build-internal location, deliberately not a `reports/` report, since the metadata is fed back into the image build (by the package phase's `NativeImageMetadata` router, recorded in the inventory as `prefix.nativeimage`) rather than read by an external tool. The same config can also be committed into `META-INF/native-image/` where `native-image` (the `native` key in `packaging.properties`) auto-discovers it. The `.nativeImage(boolean)` wither sets it directly in code. Default `true`. | | `jenesis.mutate.pitest` | system property | Read by `InferredTestObservationModule` (wired as the per-module `mutate` step). PiTest mutation testing is enabled by the presence of a `pitest.properties` file in the module (the same config-file-discovery convention as the linters), which resolves PIT and its JUnit 5 plugin in a `pitest` group, seeds faults into the compiled classes, re-runs the tests against each mutant via `PiTestModule`, and reports under `reports/pitest/` which mutants survived. This property is a default-`true` **suppress** override: set it to `false` to keep the `pitest.properties` file in place but skip the mutation run. The `.pitest(boolean)` wither sets it directly in code. Default `true`. | | `licensing.properties` | configuration file | Read by `InferredComplianceModule` (wired by `InferredMultiProjectAssembler` as the per-module `compliance` module). License compliance is a convention: it is enabled purely by the presence of a `licensing.properties` file in the configuration location (resolved through the same `configurations(...)` search as the other config files, so profile folders and per-module folders are honoured), which drives a `LicenseCheck` over the resolved dependency licenses: `allowed` / `denied` are comma-separated lists of license names or category keywords (e.g. `permissive`), `unknown=ignore\|warn\|fail` (default `fail`) sets the policy for a dependency whose license cannot be determined, and `override.=` records a license for a dependency that declares none. An unrecognized key fails the build. To skip the check, omit the file (or set `jenesis.compliance=false` to skip both compliance checks at once). | | `vulnerability.properties` | configuration file | Read by `InferredComplianceModule`, the vulnerability counterpart, and enabled the same way: purely by the presence of a `vulnerability.properties` file in the configuration location. It wires an `OsvDownload` step (querying the OSV database; `osv.endpoint=` overrides the API endpoint) feeding a `VulnerabilityCheck` over the resolved dependencies: `severity=low\|medium\|high\|critical` sets the threshold at or above which a known advisory fails the build, and `warn=true` (default `false`) reports findings without failing. An unrecognized key fails the build. To skip the check, omit the file (or set `jenesis.compliance=false`). | | `jenesis.compliance` | system property | A default-`true` **suppress** override for the whole `InferredComplianceModule`: set it to `false` to keep the `licensing.properties` / `vulnerability.properties` files in place but skip both the license and the vulnerability check, in the same manner as the other inferred plugins (`jenesis.source.checkstyle`, `jenesis.validator.spotbugs`, and so on). The `.enabled(boolean)` wither sets it directly in code. Default `true`. | | `jenesis.dependency.pin` | system property | A pinning mode - `strict`, `versions`, or `ignore` (unset is the lenient default). `strict` makes every `Dependencies` step the project wires (through the layouts' `MavenProject.make`/`ModularProject.make`, and through `InferredMultiProjectAssembler`'s `TestModule`) fail the build with an `IllegalStateException` for any resolved coordinate that has no checksum pinned in `requires.properties` - locking the build down so every artifact has to come with a SHA pin from a `pom.xml` `` comment or a `@jenesis.pin // /` Javadoc tag. `ignore` is the opposite: every Jenesis pin (managed version and checksum) is dropped, so versions float to the latest the repository serves and checksums are not verified; run the `pin` step under it on a trusted machine to refresh the pinned closure to the latest versions and freshly computed checksums. `versions` is in between: it keeps the managed versions but strips their checksums before the `Resolver` runs, so the build selects exactly the pinned version without validating any artifact digest - useful to pin versions for reproducibility without committing or maintaining checksums (and it disables the per-artifact SHA validation that the resolver otherwise performs when a checksum is pinned). Unset applies existing pins but tolerates missing ones. The mode is a `Pinning` enum whose **default value is resolved from this property**: `Project`'s default constructor reads it once via `Pinning.fromProperty()` (a bare `Project()` is pinned according to `jenesis.dependency.pin`), and that value is threaded down through `MavenProject.make` / `ModularProject.make` / `ProjectModuleDescriptor` / `InferredMultiProjectAssembler` / `TestModule` into every `Dependencies`, so a user never has to remember to propagate it. The `pinning(...)` withers override that default - `Project.pinning(Pinning.STRICT)` to lock down or `Project.pinning(Pinning.IGNORE)` to opt out - and the override (including an explicit `null`, which stays lenient regardless of the property) wins all the way down; `Dependencies` also takes it directly (`new Dependencies(repositories, resolvers).pinning(Pinning.STRICT)`). | | `jenesis.platform.` | system property | Adds (`=true`) or removes (`=false`) a single platform token, where the active platform starts from the detected operating system (`windows`/`linux`/`macos`) and chipset (`x86_64`/`aarch64`). The token set is read once by `Platform`'s no-arg constructor (a value record) when `MavenProject` / `ModularProject` / `PinModuleInfo` / `PinPom` are constructed, and threaded into the manifests step where it selects which platform-guarded pin lines apply (a `@jenesis.pin ... [,...]` Javadoc tag, or a guarded line in a `pom.xml`'s `` block): a guard applies when all its tokens are in the active set, the most specific match wins, the unguarded line for the same coordinate is the fallback. `=true` activates a custom flavor (`-Djenesis.platform.fips=true` selects a `[fips]` guard on top of the real machine), `=false` disables a detected default (`-Djenesis.platform.linux=false -Djenesis.platform.windows=true` cross-resolves a Windows closure from a Linux host). The platform is part of the manifests step's serialized cache identity, so changing it re-runs exactly the affected selection; an in-code build overrides it wholesale through the `platform(Platform)` builder method. | | `jenesis.project.digest` | system property | Content digest algorithm threaded through the build as the project's `HashDigestFunction` (default `SHA-256`). Used by `PinPom` / `PinModuleInfo` to recompute checksums over the resolved jar artifacts during the `pin` step - pin always rehashes whatever is sitting in the upstream `artifacts/` folders, so the pinned `` / `@jenesis.pin` lines always reflect the bytes the build actually used. (The intra-project sibling-artifact fingerprint that `MultiProjectDependencies` writes into `requires.properties` is instead the build executor's own per-file checksum, so it follows `jenesis.executor.digest`, not this property.) Any `MessageDigest` algorithm name is accepted (`SHA-512`, `SHA-1`, etc.). | | `jenesis.project.version` | system property | When set, stamps the version onto every artifact this build produces. It is appended last into every per-module `metadata.properties` (after the framework defaults and the project-root override file), so it overrides the `version` from either layer. `Javac` passes `--module-version ` when compiling a `module-info.java`, so the produced `module-info.class` carries it as `Module.version` (and downstream consumers automatically pick it up as `compiledVersion` on their `requires` directives). `Pom` writes it into ``; dependency versions are unaffected. `MavenRepositoryStaging` reads coordinates from the produced `pom.xml`, so the staged folder path, artifact filenames and `MavenRepositoryExport`'s `maven-metadata-local.xml` follow along. | | `jenesis.project.layout` | system property | Read by the `new Project()` constructor as the default `Layout` (overriding auto-detection), which an in-code `.layout(...)` then overrides in turn. Accepts `auto`, `maven`, `modular`, `modular_to_maven` (case-insensitive). Unknown values throw on construction. | | `jenesis.test.skip` | system property | When set (any value, including the empty string from a bare `-Djenesis.test.skip`), `TestModule.accept(...)` returns immediately and registers none of its `resolved`/`dependencies`/`executed` sub-steps, so no tests run. The check lives in `TestModule` itself, so it holds wherever a `TestModule` is wired (the default assembler or a custom one); the separate programmatic `Project.tests(false)` drops the whole test-observation wiring up front instead. | | `jenesis.stage.tests` | system property | Read by `MavenRepositoryStaging` and `ModularStaging` (their no-arg constructors, wired by the layouts' `STAGE` module). When set to `true`, the `STAGE` step includes test-variant artifacts. For `MAVEN` and `MODULAR_TO_MAVEN` that means the `-tests.jar` (plus `-tests-sources.jar` / `-tests-javadoc.jar` when those flags are on) and the test module's dependencies merged into the main `pom.xml` with `test`. For `MODULAR` it means the test module is staged as its own `/.jar` directory. Default `false`: tests still run during the build but their artifacts are not placed into the staging tree. | | `jenesis.project.root` | system property | Overrides the project root that `Project` scans for `module-info.java` / `pom.xml` (default `.`). | | `jenesis.project.properties` | system property | A comma-separated list of profile names that `Project.loadJenesisProperties` loads; each name `` resolves to a `jenesis-.properties` file (a `.properties` suffix on the name is ignored), relative to the folder of the file that declares them. Any loaded file (a `-D`, the project `jenesis.properties`, the user-global `jenesis.properties`, or another profile) may set `jenesis.project.properties` to chain further profiles transitively until all are loaded; a profile whose `jenesis-.properties` file does not exist is skipped rather than an error, since a profile may exist only as a `/` configuration folder (see `jenesis.project.configuration`). Everything merges with `putIfAbsent` in four precedence tiers, highest first: **explicit `-D`** > the **profiles** > the project `jenesis.properties` > the user-global `jenesis.properties`. So profiles override the project's base file (note: this differs from the pre-existing behaviour, where the base file won over profiles), and the project file overrides the user-global one. See *Configuration* above and the [`profiles`](demo/demo-15-profiles/README.md) demo. | | `jenesis.project.global` | system property | Names the folder holding the user-global `jenesis.properties`, read at `/.jenesis/jenesis.properties` as the lowest-precedence layer (shared defaults across every project, beaten by the project file, the profiles, and `-D`). Default is the JVM's `user.home`, so `~/.jenesis/jenesis.properties` is read when present; a missing file or folder is silently ignored, like the base `jenesis.properties`. Set it to an empty string (`-Djenesis.project.global=`) to disable user-global loading entirely. It is read only from a `-D` flag or the project `jenesis.properties` (it has to be known before the user-global file is loaded). The user-global file may itself declare profiles via `jenesis.project.properties`, resolved relative to its own `.jenesis` folder and merged into the profile tier. | | `jenesis.project.target` | system property | Overrides the per-build output folder passed to `BuildExecutor.of(...)` (default `target`). Safe to delete to force a clean build. | | `jenesis.project.artifacts` | system property | Overrides the artifact cache folder (default `.jenesis/artifacts`) under which the `MODULAR` layout stores `.jar` for each downloaded module jar (see *The artifact cache*). Effectively ignored by `MAVEN` and `MODULAR_TO_MAVEN` since they cache through `~/.m2/repository` instead. | | `jenesis.project.watch` | system property | When `true`, `Project.doMain(...)` does not return after one build: it registers a `java.nio` `WatchService` over the project root (via `ProjectWatch`) and re-runs the requested target on every file change, excluding the output folders (`target/`, the configured cache) and dot-directories so the build's own writes do not re-trigger. Each rebuild reuses the incremental cache, so only changed steps re-execute; module selectors are honored. See *Watching for changes*. Default `false` (single build). | | `jenesis.project.sources` | system property | When `true` (bare flag accepted), resolves the project-level `source` flag to `true`, so `InferredMultiProjectAssembler` also assembles a per-module sources jar. | | `jenesis.project.documentation` | system property | When `true` (bare flag accepted), resolves the project-level `documentation` flag to `true`, so `InferredMultiProjectAssembler` also assembles a per-module javadoc jar. | | `jenesis.project.metadata` | system property | Path to a project-level metadata override file (conventionally `project.properties`) whose entries are merged into every module's `metadata.properties` between the framework defaults and `jenesis.project.version`. | | `spdx.properties` | configuration file | An optional file in the configuration location that extends the built-in license normalization/classification tables the `Dependencies` step applies at extraction. It is resolved through the same `configurations(...)` search as the other config files (so profile folders and per-module folders are honoured) and bound alongside the metadata overrides to reach the `Dependencies` step. Each file uses one prefixed key space: `alias/=` adds a name alias, `category/=` adds a classification. The `spdx.properties` name is deliberately distinct from the compliance `licensing.properties` (the enforcement policy, a separate concern). Supplying it is rarely necessary (built-in tables only by default). | | `bom.properties` | configuration file | Read by `Project`'s `BomAwareAssembler`, which the `MODULAR` and `MODULAR_TO_MAVEN` layouts wrap around the assembler (the `MAVEN` layout does not, so the feature is unavailable there). Its presence in the configuration location (resolved through the same `configurations(...)` search as the other config files, so per-module `META-INF/build.jenesis/` and profile folders are honoured) switches the layout to *emit* a bill of materials of the module's own resolved dependencies. A `Bom` step reads the resolved `dependencies.properties` (the `main` group, `compile`+`runtime`) and `module.properties`, then writes `bom-.properties` into its `bom/` output folder: the module's full closure, keyed group-less in the pin token grammar (a bare ``, `/`, or a verbatim `/` - the exact inverse of `Dependencies.bomEntries`, so it round-trips) with a version and checksum per coordinate. `Inventory` collects it under `prefix.bomfile` (deliberately **not** in the module's `runtime`/module-path closure, since it is a properties file), and `ModularStaging` hardlinks it into the module repository as `/[/].properties` - exactly the `:properties` type a consuming `@jenesis.bom ` resolves, so another project imports the published closure the same way it imports a hand-written BOM. It is published **only** into the Jenesis repository: `MavenRepositoryStaging` is untouched, so the BOM never appears in the Maven `export`. Presence alone activates emission (the file is otherwise empty and reserved for future options), mirroring how `packaging.properties` gates `jpackage`. See the [`bom`](demo/demo-43-bom/README.md) demo, which both consumes and publishes a BOM. | | `jenesis.project.configuration` | system property | Project-wide directories (each relative to the project root) the inferred quality and formatting tools, plus the `packaging.properties` selector and the `licensing.properties` / `vulnerability.properties` / `spdx.properties` compliance and license files, search for their configuration files (`checkstyle.xml`, `pmd.xml`, `spotbugs-exclude.xml`, `.editorconfig`, `.scalafmt.conf`, and so on); a path-separated list, defaulting to `build.jenesis/` under the project root. The bare root is deliberately not searched by default, so a conventionally named file dropped at the root (say an `.editorconfig` an editor creates) cannot silently alter the build; add `.` to the list to opt into root-level files. Each module is also searched first in its own per-module folder (`build.jenesis/` next to its `pom.xml` in the MAVEN layout, `META-INF/build.jenesis/` under its sources in the modular layouts), so per-module configuration overrides these project-wide ones. In the MAVEN layout each pom builds a main and a test module, and each additionally searches a scope-specific folder before the shared per-pom `build.jenesis/`: the main module reads `src/main/build.jenesis/` and the test module `src/test/build.jenesis/` (`MavenModuleDescriptor.configurations()` supplies the implicit list) - so e.g. coverage (`jacoco.properties`) can be switched on for one module's tests alone, or a checkstyle rule set can apply to main sources without also gating the test sources. The modular layouts need no such split, since their test modules are separate modules with their own `META-INF/build.jenesis/`. Set to an empty string to drop the project-wide directories and rely only on per-module configuration. An entry may be a **reference** instead of a path: a bare `@` splices in the default (`build.jenesis/` under the project root) at that position, so `-Djenesis.project.configuration=../shared:@` adds a shared folder without dropping the default, and `@` splices the value of the system property `` (env fallback ``), parsed with the same grammar; unresolved or circular references fail. The same reference syntax applies to `jenesis.project.boms`, where a bare `@` splices in the resolved configuration locations, making "the configuration folders plus one more" expressible. Held on `Project` as a `SequencedSet` (settable in code via the `configuration(Path...)` wither, the empty set meaning none). Read by the `new Project()` constructor; the searched folders are made absolute and de-duplicated, and any folder that does not exist is ignored. | | `jenesis.repository.insecure` | system property | When `true` (bare flag accepted), permits artifact fetches over plaintext schemes such as `http://`. By default `Repository.open` accepts only `https://` and `file://` and throws on any other scheme, so a misconfigured or man-in-the-middled mirror cannot feed bytes into a build over an unauthenticated channel. Set this only when pointing at a trusted mirror that genuinely has no TLS endpoint. | | `jenesis.maven.uri` | system property (env fallback `MAVEN_REPOSITORY_URI`)| Overrides the default `MavenDefaultRepository.of()` upstream URL (`https://repo1.maven.org/maven2/`). Useful for pointing at an internal mirror; a trailing slash is added automatically if missing. The value may be a comma-separated list of URLs: the factory chains one repository per entry via `prepend`, so entries are queried left to right and the first hit wins; checksum sidecars are validated against the entry that is being queried. Each entry may chain `\|`-separated group ids after its URL - `\|\|...` wraps that entry in `MavenRepository.filter`, so it only serves a group id that equals one of the arguments or sits below it at a dot boundary (`com.corp` matches `com.corp` and `com.corp.core`, not `com.corporate`); every other request is a miss that falls through to the next entry. `https://nexus.corp/maven\|com.corp,https://repo1.maven.org/maven2/` thus routes `com.corp.*` to the mirror and everything else to Maven Central. An entry may also be a **reference** instead of a URL: a bare `@` splices in the default configuration at that position (the `MAVEN_REPOSITORY_URI` value if set, else the built-in default; inside the environment value itself, `@` means the built-in default), and `@` splices the value of the system property ``, falling back to the environment variable `` - the spliced text is parsed with the same grammar, so it may itself be a list with filters and further references. `https://nexus.corp/maven,@` thus tries the mirror first and falls back to whatever the default would have been, without restating it. Filter arguments on a reference (`@corp.mirrors\|com.corp`) wrap the entire spliced chain. An unresolved or circular reference fails the build with an `IllegalStateException`. A non-HTTPS scheme (e.g. `http://`) is rejected unless `-Djenesis.repository.insecure=true` is set. | | `jenesis.maven.local` | system property (env fallback `MAVEN_REPOSITORY_LOCAL`)| Overrides the local Maven repository directory (default `~/.m2/repository`) for both reads and writes: `MavenDefaultRepository` reads from and hardlinks downloaded jars into it; `MavenRepositoryExport` publishes the staged tree into it. On the read side, an explicit override must point at an existing directory or `MavenDefaultRepository` throws on construction (unset is permissive: a missing default silently disables the local cache and every fetch streams from upstream). On the write side, the directory is created on demand. | | `jenesis.maven.token` | system property (env fallback `MAVEN_REPOSITORY_TOKEN`)| When set, `MavenDefaultRepository` sends the value verbatim as an `Authorization` header on every HTTP fetch (artifact bytes and `.sha1` sidecars). Useful for upstreams that require token-based auth: set the full header value, e.g. `Bearer ` for OAuth-style endpoints or `Basic ` for HTTP Basic. Ignored for `file://` URIs and any non-HTTP scheme. The header is sent only to the configured origin: when a fetch is redirected to a different scheme, host, or port, the redirect is followed but the `Authorization` header is dropped, so the token is never disclosed to a redirect target. | | `jenesis.module.uri` | system property (env fallback `JENESIS_REPOSITORY_URI`)| Overrides the upstream base URL that the static factory `JenesisModuleRepository.of(JenesisRepository.Scope)` points at (default `https://repo.jenesis.build/`, the public overlay; see *Jenesis Repository layout for Java modules*). A trailing slash is added automatically if missing; the factory then appends `module/` (for `Scope.MODULE`) or `artifact/` (for `Scope.ARTIFACT`) to form the actual repository root. The value may be a comma-separated list of base URLs: the factory chains one repository per entry via `prepend`, so entries are queried left to right and the first hit wins (the subpath is appended to every entry). Each entry may chain `\|`-separated module ids after its URL - `\|\|...` wraps that entry in `JenesisRepository.filter`, so it only serves a module id (classifier and version stripped) that equals one of the arguments or sits below it at a dot boundary; every other request is a miss that falls through to the next entry. Entries may also be references, exactly as for `jenesis.maven.uri`: a bare `@` splices the default configuration (the `JENESIS_REPOSITORY_URI` value, else the public overlay), `@` the value of the system property or environment variable ``, recursively and with unresolved or circular references failing the build. Useful for pointing at a mirror or a privately hosted publication of the same on-disk shape. The explicit `URI`-arg constructors bypass this variable entirely (and bypass the subpath append - the caller passes the full root). | | `jenesis.module.local` | system property (env fallback `JENESIS_REPOSITORY_LOCAL`)| Overrides the local Jenesis module repository directory (default `~/.jenesis`) used by `JenesisModuleRepositoryExport` (writes the staged module tree into it; directory created on demand) and by the static factory `JenesisModuleRepository.ofLocal()` (reads module jars back from it). The `JenesisModuleRepository.of(...)` factory does **not** consult this variable; the remote overlay is governed by `JENESIS_REPOSITORY_URI` / `JENESIS_REPOSITORY_TOKEN` instead. | | `jenesis.module.token` | system property (env fallback `JENESIS_REPOSITORY_TOKEN`)| When set, repositories created by `JenesisModuleRepository.of(...)` send the value verbatim as an `Authorization` header on every HTTP fetch (e.g. `Bearer ` or `Basic `). Ignored for `file://` roots and any non-HTTP scheme. As with `MAVEN_REPOSITORY_TOKEN`, the header is sent only to the configured origin and is dropped when a fetch is redirected to a different scheme, host, or port - so the public overlay's 302 to Maven Central (a different host) is followed without forwarding the token there. The `(URI, String token)` constructor takes an explicit token instead, bypassing this variable. The set of three `JENESIS_REPOSITORY_*` variables mirrors the `MAVEN_REPOSITORY_*` set above. | | `jenesis.cache.uri` | system property | Points the build executor at a shared cache that serves a step's output across builds instead of re-running it. The value is a URI: a `file://` URI resolves a `BuildExecutorFileCache` (tuned by an optional `cache.properties` at its root), an `http(s)://` URL a `BuildExecutorHttpCache` against a cache server. A non-URI value is rejected; use `file://` for an on-disk shared location. Unset or empty, the cache is a no-op. The HTTP client reads `jenesis.cache.project` and `jenesis.cache.key` (sent as headers; env fallbacks `JENESIS_CACHE_PROJECT` / `JENESIS_CACHE_KEY`), `jenesis.cache.timeout` (connect timeout, default `PT1S`) and `jenesis.cache.insecure` (permit the key over plaintext `http://` off loopback). See *demo-38*. | | `jenesis.project.cache` | system property | A project-local, on-disk build cache (a `BuildExecutorFileCache`) that `Project` roots under the project root, distinct from the artifact cache at `.jenesis/artifacts`. The value is a **filesystem path**, never a URI (a URI value is rejected; use `jenesis.cache.uri` for that); an empty value resolves to `.jenesis/cache` under the project root, so `-Djenesis.project.cache` with no value enables it at the default location. With no `jenesis.cache.uri` set it is the sole cache; with one set, `Project` puts it **in front of** the configured cache via `BuildExecutorLayeredCache` — reads hit local first and fall through to the remote on a miss, a remote hit populates local, and stores write through to both. A read served from the local cache still sends the remote a best-effort `HEAD` touch (no `GET`), so an entry served locally is not evicted remotely — the layers keep independent but warm LRUs. Unset (the default) disables it. | | `JAVA_HOME` | environment variable| Consulted by `ProcessBuildStep`/`ProcessHandler` to locate the `java`/`javac`/`javadoc` binaries when the `java.home` system property is not set (typical when launching from a non-JDK runtime). | Properties are passed on the JVM command line, e.g. java -Djenesis.executor.rebuild=true build/jenesis/Project.java ### Selectors on the command line The build script forwards its `String[] args` to `BuildExecutor.execute(...)` (e.g. `root.execute(args)` in a hand-wired launcher), so any positional arguments after the launcher are interpreted as selectors. With no arguments, the full graph runs. | Invocation | What runs | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `java build/jenesis/Project.java` | Whole graph. On a warm cache, every step is `[SKIPPED]`. | | `java build/jenesis/Project.java ::/test` | Every `test` sub-module at any depth, plus its transitive preliminary closure. Modules along the path have their step preliminaries cache-checked; sibling sub-graphs that happen to be scheduled by `::` lenient-skip. | | `java build/jenesis/Project.java build/::/test` | Same, but anchored under the top-level `build` module. Top-level entries that aren't on the path to `build` (e.g. the `stage` step that depends on `build`) are not scheduled at all. | | `java -Djenesis.test.filter='.*FooTest' build/jenesis/Project.java ::/test` | Same selector, but `TestModule.executed` re-runs unconditionally and only selects classes matching the regex; upstream `classes`/`artifacts` etc. stay cached. | Literal selectors that don't resolve throw `Unknown selector: …`. Wildcards (`:` for one segment, `::` for any depth) silently skip non-matching branches, but over-schedule sibling subtrees: each such module's `accept(...)` runs and its declared step preliminaries are pinned, guaranteeing the predecessor folders exist wherever the wildcard lands. Prefer literal paths when you know them. Implementation details ---------------------- ### `BuildExecutor` `BuildExecutor` is the engine that owns one level of the graph. It does two things in sequence: collect *registrations* into an ordered map, then *execute* that map under a set of selectors. The same class plays both roles at every nesting depth - a module is implemented as a child `BuildExecutor` whose `target` directory is a subfolder of its parent's and whose `location` prefix is "parent-path/", so log lines and error messages stay addressable as slash-delimited paths. **Registrations.** Every `addSource`/`addStep`/`addModule` call funnels into the private `add(...)` helper, which stores a `Registration(Bound bound, Set preliminaries, Map dependencies)` keyed by the caller-supplied identity. `dependencies` is the raw declaration (which may contain slashes for paths into sub-modules); `preliminaries` is the derived set of *top-level* identifiers the registration depends on (the substring before the first slash). Preliminaries drive scheduling order; dependencies drive how predecessor summaries are filtered into a registration's input map. Sources reach the same code path: a source is a step that publishes a fixed folder. **`Bound`.** Each registration carries a `Bound`, a tiny internal interface with a single `apply(...)` method that returns a `CompletionStage>>` - the outer map's single key is the registration's identity, the inner map is its published outputs (one entry per leaf for steps; potentially many, keyed by sub-paths, for modules). `Bound` also carries a `module()` flag (default `false`, overridden to `true` by `bindModule`). The flag is the only externally-visible distinction between steps and modules at execution time; everything else falls out of `Bound.apply`'s behavior. **`bindStep`.** Wraps a `BuildStep` in a `Bound` whose `apply` does the per-step caching. On entry it bails early with an empty result if any forwarded selector reaches it (so lenient-skipping costs nothing). Otherwise it locates the step's persistent folder under `target//`, reads the previously written input checksums and step-config hash, and computes `consistent` by comparing them against the current step-config hash and the current output folder's contents. If `consistent && !shouldRun(arguments)`, the cached output is returned as-is. Otherwise the step runs into a fresh temp directory; on success the temp directory atomically replaces the persistent folder (or, if the step set `BuildStepResult.next() == false`, the temp is deleted and the previous run is reused). Crashed runs always delete the temp. **`bindModule`.** Wraps a `BuildExecutorModule` in a `Bound` whose `apply` is *not* short-circuited by selectors: modules always run their `accept(buildExecutor, folders)` to register a sub-graph. The child `BuildExecutor` it hands to `accept` shares the parent's hash functions and callback, but has its own `target` subfolder and its own empty `registrations`. After `accept` returns, the child's `doExecute(...)` is invoked with the same selectors that reached the parent module, so filtering descends with the user's selector intact. Each result published by the child is run through the module's optional `resolver` to give the parent its public name. **`doExecute`.** The selector resolver. It produces three sets: `scheduled` (registrations that will be dispatched in this level), `pinned` (registrations whose preliminary chain must run unconditionally), and `direct` (registrations that should not receive any forwarded selectors). It also produces a `forwarded` map of selectors to pass into each registration: - A literal first segment matching a registration name pins it. With no tail, it also goes into `direct`. With a tail, the tail is forwarded. - `:` and `::` schedule every registration at this level. With no tail, all are pinned and direct. With a tail, the descend selector is forwarded to all and (for `::`) the tail is also re-queued so it can match at this depth. - A literal first segment with no matching registration throws unless the selector is lenient. After the queue drains, two passes pin preliminaries: every preliminary of any pinned registration is added to `scheduled`, `direct`, and `pinned`, transitively. The second pass adds every *scheduled module* to `pinned` so the same walk picks up the module's step preliminaries - this is what guarantees a module's `accept` finds its predecessor folders even when the selector reached it only via lenient propagation. Finally, registrations in `direct` have their forwarded selectors stripped, so they execute unconditionally instead of lenient-skipping. Dispatch is then a topological loop: pick scheduled entries whose preliminaries have all been dispatched, build their input map by translating each declared dependency into a `StepSummary` (cross-module dependencies via the inherited map, intra-level dependencies by name, sub-path dependencies by exact path lookup into the predecessor's published outputs), and call `bound.apply(...)` on the executor. Results are collected per-registration in `dispatched`; the final result is the union of all top-level published summaries. **Caching invariants.** Three things have to match for a step to be reused: the step's *configuration hash* (a digest of `ObjectOutputStream.writeObject(step)`), the *predecessor input checksums* (compared against what was recorded on the previous run), and the *current output folder* (re-hashed and compared against the previously written `checksums` file). A divergence anywhere - a constructor field changed, a predecessor produced different bytes, the output was tampered with - flips `consistent` to `false` and the step re-runs. Selectors are *not* part of the hash; they only gate scheduling, so a step that runs under selectors produces the same cached output a full build would have, and subsequent unselected runs hit the cache as expected. ### Repositories and resolvers `Repository` and `Resolver` are the two pluggable surfaces for dependency lookup: a `Repository` goes from a coordinate to bytes, a `Resolver` goes from a root set of coordinates to a transitive closure. Layouts wire defaults; user code overrides them per prefix. `Repository` is a `@FunctionalInterface` with a single method `fetch(Executor, String coordinate) -> Optional`, where `RepositoryItem` is a thin wrapper exposing an `InputStream` and optionally a `Path` (so consumers can hardlink instead of copy). Two default methods compose: `repo.prepend(other)` tries `other` first and falls back to `this`; `repo.cached(folder)` wraps `this` with an on-disk hardlink cache that names each fetched artifact `folder/.jar`. Static factories on the interface itself: - **`Repository.empty()`** - never resolves anything. Useful as the default for a prefix with no associated repository. - **`Repository.ofUris(Map)`** and the overload **`Repository.ofUris(uris, versionResolver)`** - coordinate-to-URI lookup. The optional `versionResolver` (a `Serializable BiFunction>`) rewrites the registered URL for a versioned coordinate; when omitted, the lookup is strict-literal. `MavenDefaultRepository.versionResolver()` is the canonical implementation: it parses the registered URL as a Maven-layout path and substitutes the requested version into both the directory segment and the filename, so a single-URL registry can satisfy arbitrary version pins. `file://` URIs are returned as `RepositoryItem.ofFile(...)` so the framework can hardlink the existing on-disk bytes instead of streaming. - **`Repository.ofFiles(Map)`** and **`Repository.files()`** - coordinate-to-`Path` lookups (the second interprets the coordinate string itself as a filesystem path). Used when bytes are produced locally. - **`Repository.ofProperties(suffix, folders, resolver, [versionResolver,] cache)`** - bulk-loads a `Map` keyed by prefix from line-based `/=` registries found in `folders`. Each prefix's URIs become `Repository.ofUris(..., versionResolver).cached(cache)`. The a launcher resolving modules from a URL registry uses this to convert the `uris.properties` output of `DownloadModuleUris` into a per-prefix repository map with `.jenesis/artifacts/` as the cross-build hardlink cache. The shipped `Layout.MODULAR` instead points its `module` prefix at the public Jenesis overlay's `https://repo.jenesis.build/module/` root directly (see *Jenesis Repository layout for Java modules* and the `JenesisModuleRepository` paragraph above). - **`Repository.prepend(left, right)`** - per-prefix `prepend` of two repository maps (the right map's entries are tried first, falling back to the left's for the same prefix). `MavenRepository` extends `Repository` with a structured `fetch(executor, groupId, artifactId, version, type, classifier, checksum)` and an optional `fetchMetadata(executor, groupId, artifactId, checksum)` returning the artifact's `maven-metadata.xml`. `MavenRepository.of(Repository)` adapts any plain `Repository` by serialising the parts into a `groupId/artifactId[/type[/classifier]]/version` coordinate string. `MavenRepository.filter(Predicate)` wraps a repository so both `fetch` and `fetchMetadata` only delegate when the predicate accepts the group id and report a miss otherwise - the building block for routing group-id subsets to different repositories in a `prepend` chain. `JenesisRepository` is the analogous refinement for the Jenesis module coordinate grammar: it extends `Repository` with a structured `fetch(executor, module, classifier, version, type)`, and its default coordinate-level `fetch` parses `[-][/][:]` (type defaulting to `jar`, with the `jmod`-to-`jar` fallback) before delegating, so implementations only deal in parsed parts. `JenesisRepository.of(Repository)` adapts any plain `Repository` by serialising the parts back into that coordinate string, and `cached`/`prepend` are refined covariantly like on `MavenRepository`. Its nested `Scope` enum specifies the detail a repository serves modules at: `Scope.MODULE` restricts it to named Java modules, `Scope.ARTIFACT` also admits automatic modules and adjacent artifacts such as POMs. `JenesisRepository.filter(Predicate)` is the module-id counterpart of the Maven group-id filter: it only delegates when the predicate accepts the parsed module id (classifier and version already stripped). `JenesisRawGitRepository.groups(Predicate)` returns a copy of that repository that only serves modules whose **resolved Maven group id** the predicate accepts. Implementations: `JenesisModuleRepository` and `JenesisRawGitRepository`. `MavenDefaultRepository` is the concrete implementation: it talks HTTP to the upstream Maven repository (default `https://repo1.maven.org/maven2/`, overridable via the `MAVEN_REPOSITORY_URI` environment variable; the static factory `MavenDefaultRepository.of()` reads that configuration and accepts a comma-separated URL list, chaining one repository per entry via `prepend` so entries are queried left to right; an entry may chain `|`-separated group ids after its URL, wrapping it in `MavenRepository.filter` so it only serves matching group ids - exact or below a dot boundary - and everything else falls through to the next entry; an `@` entry splices in the default configuration at that position and `@` the value of the system property or environment variable ``, parsed with the same grammar, so `https://nexus.corp/,@` extends the default chain instead of replacing it - unresolved or circular references fail), and hardlinks fetched bytes through the user's **local Maven repository**, defaulting to `~/.m2/repository` and overridable via the `MAVEN_REPOSITORY_LOCAL` environment variable (this is **not** the project's `.jenesis/artifacts/` folder). When `MAVEN_REPOSITORY_LOCAL` is set explicitly, the directory must exist or the constructor throws; the default `~/.m2/repository` is permissive in the other direction, silently bypassing the local cache layer when absent. For upstreams that require authentication, `MAVEN_REPOSITORY_TOKEN` is sent verbatim as the `Authorization` header on every HTTP fetch (set the full value including the scheme, e.g. `Bearer ` or `Basic `); the token is also threaded through to `.sha1` sidecar fetches and ignored for `file://` URIs. The token is sent only to the configured origin: a redirect to a different scheme, host, or port is still followed, but the `Authorization` header is dropped before the redirected request so the credential never reaches a redirect target. All fetches go through `Repository.open`, which refuses any scheme other than `https`/`file` unless `-Djenesis.repository.insecure=true` is set, closing the door on a build that silently pulls bytes over plaintext `http`. Each download is validated against its `.sha1` sidecar; a mismatch deletes the cached file and any cached digests so the next request re-downloads from upstream. Transient fetch failures - a connection reset or an HTTP status other than 404 - are retried up to four times with exponential backoff (0.5s, 1s, 2s) before the build fails; a `FileNotFoundException` (a 404, e.g. an absent optional `.sha1` sidecar) is a clean miss and is never retried. `JenesisModuleRepository` retries its own fetches the same way. `JenesisModuleRepository` is the read-side counterpart to `JenesisModuleRepositoryExport`: it fetches module jars (or other artifacts) from a tree shaped by the export step (`/[/]/.`). The static factory `JenesisModuleRepository.of(JenesisRepository.Scope)` builds the configured instance and defaults to the public overlay at `https://repo.jenesis.build/`, appending `module/` (for `Scope.MODULE`, restricting consumers to named Java modules) or `artifact/` (for `Scope.ARTIFACT`, allowing automatic modules and adjacent artifacts such as POMs) to produce the actual repository root. The default base URL is overridable via the `JENESIS_REPOSITORY_URI` environment variable (the subpath is always appended on top of whatever that variable is set to), which also accepts a comma-separated list of base URLs: the factory chains one repository per entry via `prepend`, so entries are queried left to right and the first hit wins. An entry may chain `|`-separated module ids after its URL, wrapping it in `JenesisRepository.filter` so it only serves matching module ids - exact or below a dot boundary, with classifier and version stripped before the match - and everything else falls through to the next entry; `@` and `@` reference entries splice in the default configuration or a named property/environment value, exactly as for the Maven repository list. `JENESIS_REPOSITORY_TOKEN` is sent verbatim as an `Authorization` header on every HTTP fetch (e.g. `Bearer ` or `Basic `, ignored for `file://` and any non-HTTP scheme). The `URI` constructor accepts any root explicitly and uses it as-is - **no `module/` / `artifact/` subpath is appended** - so a `file://` or `https://` URL pointing at a publication of the desired shape works the same way (a plaintext `http://` root requires `-Djenesis.repository.insecure=true`, as everywhere else); the `(URI, String token)` overload additionally accepts an explicit auth token, bypassing `JENESIS_REPOSITORY_TOKEN`. The static factory `JenesisModuleRepository.ofLocal()` returns a repository rooted at `~/.jenesis` (overridable via `jenesis.module.local` (or the `JENESIS_REPOSITORY_LOCAL` environment variable), the same variable the export honours) and likewise uses that path as-is. `ofLocal()` does not consult the `JENESIS_REPOSITORY_TOKEN` variable - tokens only apply to the remote overlay or to a custom `JenesisModuleRepository` wired explicitly through `Project.repositories(...)`. The coordinate string accepted by `fetch(executor, coordinate)` is `[/][:]`: a bare name fetches `//.jar`; a coordinate with a version fetches `///.jar`; an explicit `:` suffix (e.g. `:pom`) replaces the `.jar` extension on the resolved file (so `foo:pom` fetches `/foo/foo.pom`, and `foo/1.0:pom` fetches `/foo/1.0/foo.pom`). When the type is omitted it defaults to `jar`. For `file://` URIs the returned `RepositoryItem` exposes the underlying `Path` so downstream caches can hardlink instead of copy; for HTTP URIs the stream is opened eagerly and an HTTP 404 surfaces as `Optional.empty()` so resolvers can fall back cleanly. The `MODULAR` layout wires the `module` prefix as `JenesisModuleRepository.of(JenesisRepository.Scope.MODULE)` (the public overlay's `module/` root, wrapped with `.cached(.jenesis/artifacts/)`) with `JenesisModuleRepository.ofLocal()` prepended, so a local hit at `~/.jenesis` short-circuits the network fetch and a miss falls back to the overlay transparently. The `MODULAR_TO_MAVEN` layout uses `JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT)` (the public overlay's `artifact/` root) as the discovery repository passed to `MavenModuleResolver`, which fetches each declared module's `:pom` artifact once and feeds the bytes straight into `MavenPomResolver`. `https://repo.jenesis.build/` is the public Jenesis overlay; it serves two parallel trees rooted at `module/` and `artifact/`, both run as thin redirect layers in front of Maven Central. The `module/` tree answers requests for `[/]/.jar` by looking the module up in the [sormuras/modules](https://github.com/sormuras/modules) catalogue and 302-ing to the matching jar at `repo.maven.apache.org`, with the version segment in the resolved URL rewritten when the request carried one - this is the entry point `Layout.MODULAR` consumes. The `artifact/` tree accepts the same shape with an additional `:` suffix (typically `:pom`) and is what `Layout.MODULAR_TO_MAVEN`'s `MavenModuleResolver` queries to fetch each declared module's POM. The returned bytes are handed to `MavenPomResolver` as the already-resolved root, so transitive resolution then continues against `repo1.maven.apache.org` without re-downloading the POMs the overlay already produced. Both paths exist so that out-of-the-box every Jenesis project can resolve standard Java modules without any per-project configuration. The catalogue itself is refreshed manually, so brand-new versions may not be reachable through the overlay until the next sormuras refresh; a project that needs a missing module/version can layer its own `JenesisModuleRepository(URI.create(...))` on top, or feed `DownloadModuleUris` an updated registry URL when using a custom layout. The local repository at `~/.jenesis` (returned by `JenesisModuleRepository.ofLocal()`) is **not** a cache equivalent of Maven's `~/.m2/repository` - it is a *supplement* to the remote map, used to publish modules locally so a project can deviate from whatever version the overlay would resolve. A module exported there via `JenesisModuleRepositoryExport` (or hardlinked in by hand) takes precedence over the overlay because the layout prepends it; remove or empty that entry and the overlay's mapping wins again. Caching of remote fetches is a separate concern handled per project under `.jenesis/artifacts/` (see *The artifact cache* below), so even a fresh `~/.jenesis` does not trigger a re-download on the next build. `Resolver` is a `Serializable @FunctionalInterface` whose method takes the root coordinates, the `Map` to fetch from, a `versions` pin map (with optional space-separated `/` checksum suffix), and a `DependencyScope` (`COMPILE` or `RUNTIME`), and returns a `Resolution`: the materialized `artifacts` (`SequencedMap`, each `Resolved` carrying the file, chosen checksum, and an internal flag), the parent -> child `edges`, and a `Vertex` per resolved coordinate (negotiated version, module name and automatic flag, and declared licenses). The downstream `Dependencies` step flattens the artifacts into `dependencies.properties` (validating the checksums), and writes the graph and licenses to `graph.properties` and `licenses.properties`. `MODULAR_TO_MAVEN` wires `MavenModuleResolver` directly under the `module` prefix (no `ModularJarResolver`, so no `module-info.class` is read), fetching each `module/` coordinate's `:pom` from a `JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT)` to learn its Maven coordinate and handing the bytes to `MavenPomResolver`'s pre-fetched-root entry point. Declared modules become the synthetic project's direct dependencies; `@jenesis.pin` entries on non-declared modules are fetched the same way and passed as managed dependencies, so `` derives only from the pins and the first-layer POMs are never re-downloaded from Maven Central. Static factories: **`Resolver.identity()`** emits its inputs unchanged under the supplied prefix without any transitive walk. The two graph-walking implementations live in the per-layer sections below: `ModularJarResolver` under *Java support* and `MavenPomResolver` under *Maven support*. **Per-prefix dispatch.** Every `requires.properties` line is prefixed `/`, and the framework routes each line to `resolvers.get(prefix)` with the entire repository map attached (resolvers may read from sibling prefixes when chaining). The two built-in prefixes are `maven` and `module`. Users define new prefixes - or override the layout's defaults on the same prefix - by passing `Map` and `Map` through `Project.repositories(...)` / `Project.resolvers(...)`; the user maps are `putAll`-merged *after* the layout defaults, so a user entry under the same key wins, and the merged maps are forwarded through `InferredMultiProjectAssembler` into every per-module `JavaToolchainModule` / `TestModule`. ### Jenesis Repository layout for Java modules The Jenesis Repository for Java modules is the on-disk format that `JenesisModuleRepositoryExport` writes and `JenesisModuleRepository` reads - the persistent, cross-project home for built modules, analogous to `~/.m2/repository` for the Maven layout. `JenesisModuleRepositoryExport` and the static factory `JenesisModuleRepository.ofLocal()` both default to `~/.jenesis` (overridable via the `JENESIS_REPOSITORY_LOCAL` environment variable); the static factory `JenesisModuleRepository.of(JenesisRepository.Scope)` instead defaults to the public overlay at `https://repo.jenesis.build/`, appending `module/` (strict named modules) or `artifact/` (also serves automatic modules and adjacent artifacts like POMs) on top of the base URL (see *The public overlay* at the end of this section). A `URI`-arg constructor on `JenesisModuleRepository` accepts any root and uses it as-is (no subpath append), so a `file://`, `http://`, or `https://` URL pointing at a publication of the same shape works equally well. **On-disk shape.** Each module owns a directory at the root named after its Java module name. Versioned builds live under one extra path segment named after the version; unversioned builds live directly at the module root. Inside either, the produced jars are named after the module, with optional `-sources.jar` and `-javadoc.jar` siblings when the build emits them: ``` / com.example.foo/ # unversioned export com.example.foo.jar com.example.foo-sources.jar com.example.bar/ # versioned export com.example.bar.jar # root mirror of the most recently built bar com.example.bar-sources.jar 1.0.0/ com.example.bar.jar # immutable version slot com.example.bar-sources.jar 2.0.0/ com.example.bar.jar com.example.bar-sources.jar ``` **Writes.** `JenesisModuleRepositoryExport` walks the `ModularStaging` output tree and hardlinks every file into the matching slot under the root (copy fallback when the filesystem refuses the link). Each target directory (module root or version subdirectory) is cleaned of pre-existing regular files on the first write to it per run, so a build that no longer produces a `-javadoc.jar` does not leave a stale one behind. Sibling version directories under the same module root are non-recursively untouched: exporting `/2.0.0/` cleans `/` and `/2.0.0/` but leaves `/0.9/`, `/1.0.0/`, etc. exactly as they were. When the module is versioned, the export *also* mirrors each jar to the module root, so `//.jar` always reflects the most recently built version regardless of which version that was. **Reads.** `JenesisModuleRepository.fetch(executor, coordinate)` parses the coordinate as `[/][:]` and maps it to the file path: - `` (no version, no type) -> `//.jar` (the root mirror). - `/` -> `///.jar`. - `:` -> `//.` (e.g. `foo:pom` -> `/foo/foo.pom`). - `/:` -> `///.`. The `:` suffix replaces the default `.jar` extension on the resolved file, which is what `MavenModuleResolver` uses to pull each declared Java module's `.pom` for `Layout.MODULAR_TO_MAVEN`. A missing file produces `Optional.empty()` so resolvers can fall back. For `file://` roots the returned `RepositoryItem` exposes the underlying `Path`, letting downstream caches hardlink instead of copy; for HTTP roots the stream is opened eagerly and an HTTP 404 surfaces as `Optional.empty()` the same way. **Versioned versus unversioned interplay.** Version subdirectories are *immutable points in history*: once `//` is populated, exporting any other version of the same module never touches those files. A consumer that pins `@jenesis.pin compile/module/ ` always reads from the versioned subdirectory and is therefore unaffected by later re-exports of the same module at different versions. The root mirror, by contrast, is *rolling latest*: every export of the module overwrites it, so an unversioned fetch returns whichever build was most recently exported. The resolver pipeline funnels coordinates through the versioned path after pinning - once a `@jenesis.pin compile/module/ ` lands in source, downstream `Dependencies` always asks for `/`, never the bare name - so the root mirror's volatility cannot leak into a checksum-pinned build. The shape is identical to the tree `ModularStaging` produces in `target/`, so the export is a straight hardlink mirror with no format transformation. Republishing the local repository elsewhere (uploading it to an HTTP server, copying it onto another machine, mounting a network share) is also a straight file-tree mirror; the resulting URL or path is a valid `JenesisModuleRepository` root. **The public overlay.** The default base URL `https://repo.jenesis.build/` is a publicly-served redirect layer that exposes two parallel trees - `module/` and `artifact/` - both mapping the `[/]/.` request shape onto Maven Central. The `JenesisRepository.Scope` passed to the `JenesisModuleRepository.of(...)` factory selects between them: `Scope.MODULE` (strict, used by `Layout.MODULAR`) appends `module/`; `Scope.ARTIFACT` (permissive, used by `Layout.MODULAR_TO_MAVEN`'s `MavenModuleResolver` for `.pom` lookups) appends `artifact/`. It is a thin Cloudflare Worker that looks the requested module up in the [sormuras/modules](https://github.com/sormuras/modules) registry and 302s to the matching upstream jar (or POM, when the request carries `:pom`); when a version segment is supplied, the Worker rewrites the version inside the resolved URL before redirecting, so any version that exists at the upstream coordinate is reachable - not just the one the registry pins. The catalogue itself is refreshed manually, so brand-new versions may not be reachable through the overlay until the next sormuras refresh; a project that needs a missing module/version can layer its own `JenesisModuleRepository(URI.create(...))` on top, or feed `DownloadModuleUris` an updated registry URL in a custom layout. The overlay holds no bytes of its own and performs no authentication of its own; cache and transport behaviour are whatever Maven Central serves at the redirect target. ### The artifact cache `.jenesis/artifacts/` is the **artifact cache**: the project-root home for dependency artifacts fetched from a remote repository, kept so they outlive a single build but stay local to the project tree. It is a different thing from the **build cache** (`jenesis.cache.uri` for a shared backend, `jenesis.project.cache` for a project-local one defaulting to `.jenesis/cache/`), which caches build-step outputs by content hash; this section is only about the artifact cache. It sits between `target/` (incremental per-run state, deletable to force a clean rebuild) and `~/.m2/repository` (shared across every project on the user's machine). The path defaults to `.jenesis/artifacts/` at the project root and can be relocated via `Project.artifacts(Path)` or `-Djenesis.project.artifacts=`. What lives there today: hardlinked artifacts fetched through any `JenesisModuleRepository` wrapped with `.cached(.jenesis/artifacts/)`. `Layout.MODULAR` wires its `module` prefix this way to cache the module jars it resolves; `Layout.MODULAR_TO_MAVEN` also wraps the permissive `JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT)` it hands to `MavenModuleResolver`, so each declared module's `.pom` artifact is hardlinked here on first fetch and served locally on subsequent builds. Each entry is named `.jar` (the suffix is literal, regardless of the artifact's actual type); the encoded coordinate is a content-stable function of the coordinate string, so two builds asking for the same coordinate map to the same filename and the second build hardlinks from `.jenesis/artifacts/` instead of going to the network. `Layout.MAVEN` does not populate this folder: its canonical `MavenDefaultRepository` already caches into `~/.m2/repository`, so for that layout `.jenesis/artifacts/` is typically empty. `Layout.MODULAR_TO_MAVEN` caches into both: discovery POMs land in `.jenesis/artifacts/`, while the transitive Maven artifacts go to `~/.m2/repository`. **Local-repository artifacts are referenced in place, never copied here.** An artifact that already resides in a local repository - the local Maven repository (`~/.m2/repository`) or the local Jenesis module repository (`~/.jenesis`) - is read straight from that location and is *not* written into `.jenesis/artifacts/`. `Repository.cached(...)` recognises such items (`RepositoryItem.local()` for local Maven files, `RepositoryItem.internal()` for first-party Jenesis modules) and returns them without creating a cache entry; only artifacts fetched from a *remote* repository are hardlinked here. The split exists because a remote versioned coordinate is immutable by contract, whereas a local repository is mutable: you can re-`install` a new build under the same version. Since `.jenesis/artifacts/` deliberately outlives `target/`, caching a mutable local artifact here would let a stale copy shadow the local repository. (The two flags are distinct on purpose: `internal` additionally marks an artifact as first-party and exempt from strict checksum pinning, which a third-party jar sitting in `~/.m2` must not be; `local` only means "already on local disk," so it skips the cache without granting that trust.) Together with the per-build `target/.../resolved/` snapshot, this gives a deliberate contract for locally-built dependencies: - **Without a rebuild,** previously-resolved local artifacts stay frozen at the last resolve. The `resolved/` snapshot under `target/` (kept across builds by the `Dependencies` step's `shouldRun` gate and its carry-forward of the prior `resolved/` folder) is reused as-is, so re-`install`-ing the same version of a locally-built dependency does not silently change an in-progress build. Refreshing a locally-built dependency is therefore an explicit choice. - **With `-Djenesis.executor.rebuild=true`,** `target/` (and therefore `resolved/`) is wiped and every dependency re-resolves. Because `.jenesis/artifacts/` never holds local artifacts, the locally-built ones are re-read *fresh* from `~/.m2` / `~/.jenesis`, while remote dependencies are still served offline from `.jenesis/artifacts/`. The build reaches the network only for a genuinely new remote coordinate. Properties of the cache layer: - **Content-addressable by coordinate.** Filenames derive from the coordinate, not the build run, so replays are filesystem lookups and the folder can be moved between machines without rewriting. - **Refresh on demand only.** No TTL, no automatic invalidation: entries persist until something deletes them. The assumption is that jars at versioned coordinates are immutable by contract; force a refresh by deleting the file (or the whole folder) and re-running. - **Safe to delete.** Nothing in `target/` references `.jenesis/artifacts/` directly and no build identity hashes through cache contents, so a wiped `.jenesis/artifacts/` only costs the next build's downloads. Conversely, deleting `target/` while keeping `.jenesis/artifacts/` is the fastest path to a clean rebuild that does not re-fetch anything. - **Hardlinks, not copies.** Both the cache write (`Files.createLink` in `Repository.cached`) and downstream consumption use hardlinks where the filesystem allows, so a populated `.jenesis/artifacts/` does not multiply disk usage when its contents flow into `target/`. ### Java support Generic infrastructure (`BuildExecutor`, `BuildStep`, `BuildExecutorModule`) doesn't know anything about Java. The Java-specific classes are a thin layer of `BuildStep`/`BuildExecutorModule` implementations that plug into it. - **`ProcessBuildStep`** is the abstract base for every step that shells out to an external command. Subclasses return their command-line via `process(...)`; the base class assembles the process, captures stdout/stderr, validates the exit code, and reports a `BuildStepResult`. It also defines the `process/.properties` convention used by upstream steps to inject command-line arguments (see `InferredMultiProjectAssembler.Prepare`, which writes `process/jar.properties` with `--main-class=…` and `process/javac.properties` with `--module-version=…`). - **`JdkProcessBuildStep`** extends `ProcessBuildStep` with a single twist: it serializes `Runtime.version()` into its config hash so a JDK upgrade invalidates every cached `javac`/`java`/`javadoc` output without any per-step opt-in. - **`ProcessHandler`** wraps the actual invocation (forked `Process` or in-process `ToolProvider` call). The `ProcessHandler.Factory` passed to a step's constructor decides which: `ProcessHandler.Factory.TOOL` runs `javac` in-process via `java.util.spi.ToolProvider`; `ProcessHandler.Factory.FORK` forks. `ProcessHandler.Factory.of()` selects between them from `jenesis.process.factory` (defaulting to `FORK` only under a native image). The same factory drives `Jar`, `Javadoc`, `JMod`, `JLink` and `JPackage`. - **`Javac`, `Jar`, `Javadoc`, `Java`** are the concrete tool drivers. They consume the conventional folders (`sources/`, `classes/`, `resources/`, `artifacts/`) and produce the conventional outputs documented in the *Conventional folders and files* section. - **`TestModule`** is a `BuildExecutorModule` that wires `Java` into a runner. `TestEngine` plus the built-in `JUnitPlatform`, `JUnit4`, and `TestNG` records encode per-framework metadata (the runner's `mainClass()` and its `runnerModule()` name for a modular `-m` launch, the `isEngine`/`isRunner` predicates that detect the framework among the scanned modules, and the runner coordinates) and each implements `commands(supplement, classes, methods, groups, parallel)` to shape the CLI arguments for picking and running tests: `JUnit4` emits class names positionally and throws `IllegalArgumentException` if individual methods are requested, `JUnitPlatform` emits `--select-class=` / `--select-method=` per entry, and `TestNG` joins everything into the single comma-separated `-testclass` / `-methods` arguments the `org.testng.TestNG` runner expects. The same method maps the test `groups` (JUnit Platform tags, TestNG groups; `JUnit4` cannot select `@Category` from its console runner) and the `parallel` flag onto each runner's own syntax. When no engine is passed, `TestEngine.of(...)` selects one by scanning the inherited jars' module names (tried in order, first match wins); `JUnitPlatform` then derives a *default* `junit-platform-console` runner version from the discovered `org.junit.platform.engine` module (`TestEngine.coordinates(...)` maps each runner coordinate to its default version), so it serves both JUnit 5 and JUnit 6 without configuration - while a user pin for the console launcher still overrides that default. New frameworks slot in by implementing `TestEngine` and choosing whatever argument shape the runner needs. - **`JavaToolchainModule`** is the canonical `BuildExecutorModule` for "compile sources, version-stamp `module-info.class`, package as a jar". It delegates to `Javac`, `Versions`, and `Jar`. Build scripts that don't have multi-project structure can wire it directly; test execution is a separate `TestModule` that `InferredMultiProjectAssembler` wires as a sibling when the project enables tests. - **`ModuleInfoParser` / `ModuleInfo`** parse `module-info.java` via the `javax.tools` / `com.sun.source` APIs and surface the module name, its `requires` set (including `requires transitive`, `requires static`, and `requires static transitive`), and a `versions` map extracted from `@jenesis.pin // ` Javadoc tags on the module declaration - the input that `ModularProject` writes to `versions.properties` so transitives can be pinned without listing them as direct `requires`. - **`ModularJarResolver`** is a `Resolver` (the generic resolution interface) backed by parsing each fetched jar's `module-info.class` straight from bytecode (no class loading). It walks the `requires` edges into a transitive closure and emits resolved coordinates of the form `/[/]`. The version is chosen from (in order): the `versions` SPI input passed by `Dependencies` (Javadoc pin); the `rawCompiledVersion()` recorded on the parent's `requires ` directive when first encountered (first parent wins); the `ModuleDescriptor.rawVersion()` recorded in the fetched jar's `module-info.class`; or omitted entirely if none of these are present. "First parent" is concrete and deterministic: the traversal is a single BFS over a queue seeded with the coordinates from `requires.properties` in file order, and each visited module emits its transitive `requires` into the queue sorted by module name. The propagated `rawCompiledVersion` map is updated via `putIfAbsent`, and once a module name has been visited the next pop short-circuits on the `resolved` set, so a *later* requirer that bakes a different version into its own `requires ` directive is silently ignored. One practical consequence: re-ordering the lines of `requires.properties` is a semantically meaningful edit that can change which transitive version of a shared module wins (the cache hashes the file faithfully, so the next build re-resolves correctly, but a code review that only diffs content will miss it). When a version is chosen *before* the fetch (pin or propagated compiledVersion), the resolver asks the `Repository` for `/` so a Maven-conventional URL carrying a registry default can be rewritten by `Repository.ofUris` into the requested version; the resolver fetches the bare name instead when no version is pinned or propagated, so repositories that don't understand versioned coordinates continue to work. Java module rules differ from Maven's (no Maven-style nearest-wins; transitivity is opt-in per `requires transitive`), so the resolver is small but distinct. - **`Versions`** is the `BuildStep` that closes the loop: it reads `requires.properties` after resolution, builds a `` map from every `//` entry, and rewrites every `module-info.class` it finds under the predecessor `classes/` folders to stamp those versions onto each matching `requires` directive's `compiledVersion`. Non-`module-info` files are hard-linked through to the output unchanged. `JavaToolchainModule` wires this step between `Javac` and `Jar` so the packaged jar's `module-info.class` carries the resolved versions. - **`DownloadModuleUris`** is a `BuildStep` that materializes a properties file mapping module name → URI. A launcher resolving modules from a URL registry feeds it the `sormuras/modules` registry plus a project-local override. - **`ModularProject`** is the equivalent of `MavenProject` but for module-based multi-project builds: it walks a directory tree, identifies modules from their `module-info.java`, and registers a `JavaToolchainModule` per module via `MultiProjectModule`. The wiring pattern is uniform: anything Java-specific that runs as part of a step is a `BuildStep` (cached on config hash + I/O); anything that wires sub-graphs is a `BuildExecutorModule`; anything that resolves coordinates implements `Resolver`. The generic infrastructure treats all three as opaque. ### Maven support Maven support is layered on top of the same generic interfaces, with one extra wrinkle: Maven repositories serve POMs in addition to artifacts, so they get a refined `Repository` interface. - **`MavenRepository`** extends the generic `Repository` with a structured `fetch(executor, groupId, artifactId, version, type, classifier, checksum)` and an optional `fetchMetadata(executor, groupId, artifactId, checksum)` returning the artifact's `maven-metadata.xml`. Implementations: `MavenDefaultRepository` (HTTP, with on-disk cache in the user's local Maven repository, default `~/.m2/repository`; the project's `.jenesis/artifacts/` folder is **not** used here) and any user-supplied subclass (e.g. an internal Nexus mirror). The `MAVEN_REPOSITORY_URI` environment variable overrides the default upstream URL and accepts a comma-separated list queried left to right, where a `||...` entry only serves matching group ids (exact or below a dot boundary); `MAVEN_REPOSITORY_LOCAL` overrides the local repository directory; and `MAVEN_REPOSITORY_TOKEN`, when set, is sent verbatim as an `Authorization` header on every HTTP fetch (e.g. `Bearer `). See the *Repositories and resolvers* section above for the generic interface and its factories. - **`Pom`** is a `BuildStep` that emits or transforms `pom.xml` files in the build graph (used by `MavenProject`'s scan/prepare flow). - **`MavenPomEmitter`** is a stateless serializer: takes a `Pom` model and writes a `pom.xml`. Used both for publishing and for materializing per-module POMs during multi-project builds. - **`MavenPomResolver`** is the resolver. Implements `Resolver` and is the densest piece of code in the repo: parses POMs (with parent inheritance), applies dependency-management overrides, walks the transitive closure with Maven's nearest-wins version conflict resolution, honors ``, distinguishes `compile`/`runtime` /`provided`/`test` scopes (and prunes them per Maven's transitive-scope rules), and resolves BOM (`pom`-type) imports. Any entries supplied via the `Resolver` SPI's `versions` parameter are folded into the resolver's internal `managedDependencies` map as if they had been declared in a virtual outermost `` block, so an external pin (e.g. a `` entry from the local POM emitted by `MavenProject`) overrides what each visited POM would have selected on its own. The output is a flat list of resolved coordinates that the `Dependencies` step consumes. - **`MavenDependencyKey` / `MavenDependencyName` / `MavenDependencyValue` / `MavenDependencyScope`** are the data records the resolver operates on. `Key` is the conflict-resolution identity (groupId+artifactId+type +classifier, version excluded so the resolver can pick one); `Name` is `groupId+artifactId` for BOM/parent lookups; `Value` carries everything else (version, scope, optional, exclusions). - **`MavenLocalPom`** captures a parsed POM (coordinates, parent, dependencies, dependency-management, properties). The resolver expands these lazily. - **`MavenVersionNegotiator` / `MavenDefaultVersionNegotiator`** handle Maven's version-range syntax (`[1.0,2.0)`, `LATEST`, `RELEASE`, etc.) - picking a concrete version from a candidate list per the rules described in the Maven version comparison spec. - **`MavenRepositoryStaging`** is the `BuildStep` that materialises the Maven repository layout under `target/stage/maven/output/`. It walks every per-module `inventory.properties`, parses `prefix.pom` for `groupId`/`artifactId`/`version`, validates that `prefix.artifacts` lists exactly one `.jar` and `prefix.sources`/`prefix.documentation` each list at most one, then hardlinks the binary plus the (optional) sources/documentation jars plus the pom as `///-.{jar,pom}`. Modules carrying `prefix.test=` are routed onto the named main's coordinate with a `-tests` classifier, and their POMs are parsed for additional dependencies that get merged into the staged main POM with `test`. - **`MavenRepositoryExport`** is the `BuildStep` that publishes the staged tree to an external Maven repository directory (default `~/.m2/repository`, overridable via the `MAVEN_REPOSITORY_LOCAL` environment variable). It always re-runs, walks each predecessor for `.pom` files, copies every sibling in the version directory into the target, and writes the `mvn install`-equivalent metadata (`maven-metadata-local.xml` per artifact, `_remote.repositories` markers per version dir, per-version snapshot metadata for `-SNAPSHOT` versions). - **`MavenUriParser`** maps coordinate strings to/from URI form, used by repository implementations. - **`MavenModuleResolver`** is a `Resolver` that fetches each input Java module's POM as `[/]:pom` from a configured `Repository` (typically `JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT)`), then hands the bytes to `MavenPomResolver`'s pre-fetched-root entry point. The walked transitives still come from a `MavenDefaultRepository`, but the first-layer POMs the discovery repo already produced are reused as the root of the resolution rather than re-downloaded from Maven Central. `Layout.MODULAR_TO_MAVEN` plugs it into `ModularJarResolver` as the fallback, so declaring `requires foo.bar` in `module-info.java` is enough to drive a full Maven-style transitive resolution against the project's chosen Maven repository. - **`MavenModuleDescriptor`** is `MavenProject`'s implementation of `ModuleDescriptor` - the bridge between Maven's per-module data and `MultiProjectModule`'s generic factory contract. - **`MavenProject`** is the `BuildExecutorModule` entry point: scans a directory tree of `pom.xml` files, produces one `MavenModuleDescriptor` per module, and feeds them into `MultiProjectModule` along with a `JavaToolchainModule` factory. The scan is itself a step (`Pom`) so it caches; the per-module wiring runs in `MultiProjectModule`'s body each build. Same uniform pattern as Java support: `BuildStep` for cached units of work, `BuildExecutorModule` for sub-graph wiring, `Resolver`/`Repository` for dependency lookup. A user wiring Maven into a build never touches the resolver mechanics directly - they pass a `Map` keyed by prefix (`"maven"`, `"module"`) to the `Dependencies` step (or to `TestModule` for the test runner side-channel), and the generic infrastructure dispatches coordinates to the right resolver by prefix. Project metadata ---------------- Jenesis carries POM descriptive metadata (name, description, url, license, developer, SCM) through a single hash-tracked channel keyed off a properties file named by convention `metadata.properties`. The same channel is fed by per-layout defaults extracted from the project's own sources (module-info or pom.xml), and overridden key-by-key by the user-supplied file. ### Pointing Jenesis at the file The file path is set in one of two equivalent ways: - System property: `-Djenesis.project.metadata=project.properties` (path resolved relative to the project root). - Programmatic API: `Project.builder().metadata(Path.of("project.properties"))`. A `null` (unset) value means no project-level metadata file; Jenesis still emits POMs that contain only the fields the active layout supplies. When the value is set, the assembler binds the file in through the top-level `metadata` module so its entries reach every per-module `Manifests` step as upstream input and are folded into each per-module `metadata.properties` last (winning on any overlapping key). Because the file's content participates in the build hash chain, any edit to it invalidates downstream `pom` and `stage` outputs the same way a source change would. ### Recognised keys ```properties # Maven coordinates. The framework writes these into every per-module # metadata.properties from the project's own source (pom.xml for MAVEN, the # Java module system module name for MODULAR). Placing them in the project-root override # file is rarely needed; the most common reason is overriding the version # from outside the project (typically via -Djenesis.project.version=, # which appends a `version` entry to this same channel as the last layer). project=build.jenesis artifact=build.jenesis version=1-SNAPSHOT # Descriptive metadata - usually supplied by the layout (see below) and only # placed here when overriding. Emitted as , , . name=Jenesis description=A build tool for Java projects, written and configured in Java itself. url=https://github.com/raphw/jenesis # One or more entries, keyed by id. The `` segment is the # license name lowercased with spaces and dots replaced by `_` (so the key # segment itself can never contain a dot, which is what makes the # `license..` parse unambiguous). Add another license by # using a different id segment. license.apache-2_0.name=Apache-2.0 license.apache-2_0.url=https://www.apache.org/licenses/LICENSE-2.0.txt # One or more entries, keyed by id. The key after `developer.` # names the developer's id (used verbatim as ); the remaining suffix # selects the attribute (`name`, `email`). Add another developer by using # a different id segment. developer.raphw.name=Rafael Winterhalter developer.raphw.email=rafael.wth@gmail.com # block. is omitted-then-derived: when # scm.developerConnection is missing, the emitter writes scm.connection # into as well, so most projects only need the two # keys below. scm.connection=scm:git:https://github.com/raphw/jenesis.git scm.url=https://github.com/raphw/jenesis ``` Jenesis writes per-module descriptive metadata to a separate file from per-module graph state: - **`module.properties`** (constant `BuildStep.MODULE`) carries graph-coordination keys only - `path`, `test`, `main`, and (on `MODULAR`) `module=`. The framework writes it; the user never edits it. `Project.PinModule` reads `path` from it to locate source files; `Inventory` mirrors `module`, `test`, and `main` into the per-module `inventory.properties` so the staging steps (`MavenRepositoryStaging`, `ModularStaging`) and the launcher (`Execute`) see them through a single, self-anchored channel. - **`metadata.properties`** (constant `BuildStep.METADATA`) carries Maven coordinates and POM-descriptive keys - `project`, `artifact`, `version`, `name`, `description`, `url`, `license..*`, `developer..*`, `scm.*`. Each layout's `Manifests` step writes per-module defaults: coordinates come straight from `pom.xml` for MAVEN and are derived from the Java module system module name for MODULAR (with `1-SNAPSHOT` as the default version); descriptive keys are extracted from the module-info Javadoc or the source `pom.xml`. The project-root override file (conventionally `project.properties`, pointed at by `-Djenesis.project.metadata`) overlays project-wide values on top in `Manifests` (later puts win), so a user-supplied entry beats both the framework default and the source-extracted value. ### Per-layout defaults Each layout's `Manifests` step writes its own `metadata.properties` into the per-module manifests folder (omitted when no descriptive keys apply), so defaults travel through the same predecessor channel as the user-supplied override file. The user's file is iterated last, so user keys override layout-derived keys on a key-by-key basis. `MODULAR` extracts `name` from the module-info's javadoc first sentence (trailing `.` stripped) and `description` from the rest of the body. The same parser pass that already reads `@jenesis.release` and `@jenesis.pin` reads the description, so the cost is essentially free: ```java /** * Jenesis. * * A build tool for Java projects, written and configured in Java itself. * * @jenesis.release 25 */ module build.jenesis { ... } ``` contributes `name=Jenesis` and `description=A build tool for Java projects, ...` automatically. A javadoc with no body produces neither key. A single sentence with no trailing body produces only `name`. `MAVEN` (and `MODULAR_TO_MAVEN`) parses each module's source `pom.xml` for ``, ``, ``, every ``, every ``, and the `` block, and writes the same property keys into `metadata.properties`. The extraction is a direct DOM read - property expansion (`${var}`) and parent POM inheritance are deliberately not applied to these specific fields. A project that needs `${project.url}` substituted, or a value inherited from a parent POM, must put the resolved value into the project-root `metadata.properties` so it overrides the literal default. ### The Pom step's predecessor order `Pom` runs once per module and emits exactly one `pom.xml` from the merged metadata. It reads each predecessor folder for `requires.properties` (whose keys carry the `//...` scope directly), `exclusions.properties`, and `metadata.properties`, and parses `groupId`/`artifactId`/`version` and the descriptive fields from the merged `metadata.properties`. The override chain for the file's values is: 1. The framework defaults (`project`/`artifact`/`version`) written by the layout's `Manifests` step land first. 2. The descriptive metadata extracted from the source (`pom.xml` for MAVEN, module-info Javadoc for MODULAR) is merged on top of those defaults inside the same `Manifests` step. 3. The project-root override file (the file `-Djenesis.project.metadata` points at, conventionally `project.properties`) is merged last via the executor's `metadata` module, so user-supplied entries win on any overlapping key. 4. `-Djenesis.project.version=` is appended after all of the above and overrides any `version` from any layer, so a release build can pin a single version on the command line without editing any file. Releasing to Maven Central -------------------------- This section describes the release pipeline that any project using Jenesis can adopt. The next section documents how this repository wires those mechanisms together for its own releases. ### The stage step Each of the three `Project.Layout` constants wires a `stage` *module* that depends directly on `BUILD`, nesting the staging step under a layout-named sub-step (`maven` and/or `modular`) so the produced tree always sits at a consistent path. The Maven-side layouts use `MavenRepositoryStaging`, which combines Maven repository placement with test-aware POM merging in one pass; `MODULAR` uses the simpler `ModularStaging`; `MODULAR_TO_MAVEN` wires both: ```java // MAVEN: one "maven" sub-step executor.addModule("stage", (s, in) -> s.addStep( "maven", new MavenRepositoryStaging(project.stageTests()), in.sequencedKeySet()), BUILD); // MODULAR: one "modular" sub-step executor.addModule("stage", (s, in) -> s.addStep( "modular", new ModularStaging(project.stageTests()), in.sequencedKeySet()), BUILD); // MODULAR_TO_MAVEN: both "maven" and "modular" sub-steps in the one stage module. ``` Both staging steps walk every per-module `inventory.properties` reachable through the `BUILD` predecessor and resolve its self-anchored paths against the inventory file's own folder. `MavenRepositoryStaging` writes its tree under `target/stage/maven/output/` and `ModularStaging` under `target/stage/modular/output/`. Files are hard-linked rather than copied; like every other Jenesis step, its output is content-hashed and skipped on re-runs when inputs are unchanged. Under `MavenRepositoryStaging` (used by `MAVEN`, `MODULAR_TO_MAVEN`), each inventory is classified by reading its `prefix.test` value, then staged differently: - **Main modules** (no `prefix.test` in the inventory) have their `prefix.artifacts` jar (plus the optional `prefix.sources` and `prefix.documentation` jars) hard-linked at the standard Maven repository path `///-[-].`. The staging step validates that `prefix.artifacts` lists exactly one `.jar` and that each of `prefix.sources` / `prefix.documentation` lists at most one. Coordinates come from parsing `prefix.pom` (the staged POM emitted by the `Pom` step). The pom is either hard-linked as-is, or - when at least one test variant points at this main - written out via DOM merge: the main POM's `` gains one `` per test-variant dep, each carrying `test`. Test deps that point back at any main artifact (a Java module system test module's `requires
;` becomes a `` on `
`) are dropped from the merge to avoid self-references. - **Test variants** (`prefix.test=` in the inventory) have their jars hard-linked under the **main's** Maven coordinate (not their own) with a `-tests` classifier suffix: `
--tests.jar`, `
--tests-sources.jar`, `
--tests-javadoc.jar`. No separate POM is staged for the test variant; the merged main POM is the single canonical POM for the coordinate. Duplicate main artifactIds and multiple test modules naming the same main fail loudly. The `test` key on the inventory mirrors the same key in `module.properties`, set from existing metadata: `MavenProject.Manifests` flags any per-module variant whose generated coordinate carries the `tests` classifier, and `ModularProject.Manifests` flags any module whose `module-info.java` declares an `@jenesis.test` Javadoc tag (parsed by `ModuleInfoParser` into `ModuleInfo.testOf()`). Its value is the `artifactId` of the main module the tests cover (or empty for the deprecated bare `@jenesis.test` form, which only resolves when exactly one main module is present). It does not refer to a Maven `` POM relationship. Under `ModularStaging` (used by `MODULAR`), each inventory's `prefix.module` (the Java module system module name) becomes the staging directory and jar prefix; no POM is required or written. The `test` marker on the inventory is **ignored** by `ModularStaging` when `-Djenesis.stage.tests=true`; test modules are then staged under their own Java-module-named directory with no `-tests` suffix and no merging. When the flag is unset (default), test modules are simply omitted from the staging tree. When `prefix.version` is present (set on the inventory from `metadata.properties`' `version` key, which both layouts always populate today), it is inserted as an additional path segment between the module name and the jar files. The resulting trees (with `=build.jenesis`, `=1.0.0`, `-Djenesis.project.sources=true`, `-Djenesis.project.documentation=true`); the Maven layout stages under `target/stage/maven/output/` and the module layout under `target/stage/modular/output/`: `MAVEN` and `MODULAR_TO_MAVEN` (Maven repository layout, identical for the Jenesis project): ``` target/stage/maven/output/ └── build/ └── jenesis/ └── build.jenesis/ └── 1.0.0/ ├── build.jenesis-1.0.0.jar ├── build.jenesis-1.0.0-sources.jar ├── build.jenesis-1.0.0-javadoc.jar ├── build.jenesis-1.0.0.pom ├── build.jenesis-1.0.0-tests.jar ├── build.jenesis-1.0.0-tests-sources.jar └── build.jenesis-1.0.0-tests-javadoc.jar ``` `MODULAR` with `-Djenesis.project.version=1.0.0`: ``` target/stage/modular/output/ └── build.jenesis/ └── 1.0.0/ ├── build.jenesis.jar ├── build.jenesis-sources.jar └── build.jenesis-javadoc.jar ``` `MODULAR` with the project's default `1-SNAPSHOT` version (still produces a version segment, since `metadata.properties` always carries a `version`): ``` target/stage/modular/output/ └── build.jenesis/ └── 1-SNAPSHOT/ ├── build.jenesis.jar ├── build.jenesis-sources.jar └── build.jenesis-javadoc.jar ``` (`MAVEN` and `MODULAR_TO_MAVEN` route the test module's jars onto the main artifact's coordinate with a `-tests` classifier suffix and merge the test-variant dependencies into the main POM with `test`; the per-module `prefix.test=` marker on the inventory triggers this in `MavenRepositoryStaging`. `MODULAR` ignores that marker and stages every discovered Java module under its own Java-module-named directory at the same level when `-Djenesis.stage.tests=true`.) A module can also **depend on** another module's test jar - the standard way to share test fixtures. In a `pom.xml` declare the dependency with either `test-jar` or `tests`; Jenesis resolves both to the sibling's `-tests` artifact, because `MavenPomResolver` normalizes Maven's `test-jar` type to the `tests` classifier (along with the other classifier-bearing types `javadoc`, `java-source` and `ejb-client`, which it maps to `javadoc`/`sources`/`client`). Within a multi-module build the consumer is wired against the sibling test module's compiled classes directly; against an external project it resolves the published `-tests.jar`. The `MAVEN` and `MODULAR_TO_MAVEN` layouts additionally wire a `MavenRepositoryExport` step after `stage`, which copies the staged tree into the user's local Maven repository (default `~/.m2/repository`) and writes the `mvn install`-equivalent metadata. Run `java build/jenesis/Project.java export` to perform both `stage` and the local-repository copy in one invocation. A release build is therefore typically: ``` java -Djenesis.project.version= \ -Djenesis.project.sources=true \ -Djenesis.project.documentation=true \ -Djenesis.project.metadata=project.properties \ build/jenesis/Project.java stage ``` `-Djenesis.project.version=` is what `Javac` stamps as `--module-version` and what the `Pom` step writes into ``; the staged paths use the same value to form `-[.].`. ### Handing off to JReleaser [JReleaser](https://jreleaser.org) consumes the staged directory directly. A `jreleaser.yml` at the project root with `deploy.maven.mavenCentral.sonatype.stagingRepositories` pointing at `target/stage/maven/output/`, plus the standard `JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME`/`_TOKEN` and `JRELEASER_GPG_*` environment variables, lets a single `jreleaser deploy` (or `full-release` from `jreleaser/release-action@v2` in CI) sign and upload the staged artifacts to Maven Central. `JRELEASER_PROJECT_VERSION` should be set to the same value passed as `jenesis.project.version` so JReleaser and the emitted POM agree on the coordinate. How Jenesis itself releases --------------------------- The release configuration that this repo actually uses lives in three files: `project.properties` at the root, `jreleaser.yml` at the root, and `.github/workflows/release.yml`. `project.properties` carries the SCM, license, developer, and url metadata, passed via `-Djenesis.project.metadata=project.properties` so it is folded into every per-module `metadata.properties`. `name` and `description` are not in this file because the `MODULAR` layout reads them from the module-info's javadoc, and `module-info.java` has: ```java /** * Jenesis. * * A build tool for Java projects, written and configured in Java itself. * * @jenesis.release 25 */ module build.jenesis { ... } ``` `pom.xml` at the root mirrors the same metadata for IDE/Maven consumers and for Jenesis itself if anyone forces the `MAVEN` layout. The published coordinate is therefore `build.jenesis:build.jenesis:`, and a successful release produces: ``` target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis-.jar target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis--sources.jar target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis--javadoc.jar target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis-.pom ``` `.github/workflows/release.yml` runs after the "Test Jenesis Tool" workflow completes on `main`, gated on the head commit's first line beginning with `[release]`. The release version is resolved as follows: - `[release 1.2.3]` or `[release v1.2.3]` (explicit version inside the marker) - use `1.2.3`. - `[release]` (marker alone) - take the highest `v?X.Y.Z` git tag, bump its minor by one, reset patch. So `v0.1.0 -> 0.2.0`, `v0.9.0 -> 0.10.0`, `v1.4.0 -> 1.5.0`. - `[release]` with no semver tags in the repo - bootstrap at `0.0.1`. The workflow then runs the release build (the canonical command above, with `jenesis.project.version` set to the resolved version) and hands off to `jreleaser/release-action@v2` with `full-release`. JReleaser signs, uploads, and (because `release.github.skipRelease: false`) cuts a matching `v` git tag, which the next `[release]` commit will pick up to compute its own auto-incremented version. Using Jenesis as a CLI ---------------------- Alongside the Maven Central artifacts, every release produces a self-contained CLI distribution (`jenesis-.zip`) that bundles the published jar with a small set of launcher scripts. This zip is attached to the matching GitHub release and is the same artifact served by SDKMAN. Once unpacked it looks like: jenesis-/ bin/ jenesis, jenesis.bat jenesis-exec, jenesis-exec.bat jenesis-init, jenesis-init.bat jenesis-validate, jenesis-validate.bat jenesis-version, jenesis-version.bat jenesis-switch, jenesis-switch.bat lib/ build.jenesis-.jar sources/ build.jenesis--sources.jar LICENSE Each script comes in two flavours: a POSIX bash script (no extension) and a Windows batch file (`.bat`); the batch versions mirror the bash logic exactly. Every script derives `JENESIS_HOME` from its own location, so the unpacked tree is fully relocatable. ### Shell scripts - **`jenesis`** is a thin launcher around the bundled jar. It locates a Java 25+ runtime (`JAVA_HOME` first, then `java` on `PATH`), verifies the major version, and runs `java -p /lib -m build.jenesis "$@"`. All arguments pass through, so `jenesis +sources` is the SDK equivalent of `java build/jenesis/Project.java +sources` from a project root. `JAVA_OPTS` is honoured. - **`jenesis-exec`** is the companion launcher around `Execute` (see [Running a module's main entry](#running-a-modules-main-entry)). Same runtime resolution and `JAVA_OPTS` handling as `jenesis`, but runs `java -p /lib -m build.jenesis/build.jenesis.Execute "$@"`, so `jenesis-exec arg1 arg2` is the SDK equivalent of `java build/jenesis/Execute.java arg1 arg2` from a project root. The `jenesis.execute.module`, `jenesis.execute.mainClass`, `jenesis.execute.docker`, and `jenesis.execute.docker.image` system properties apply. Trailing arguments to `jenesis` and `jenesis-exec` pass straight through as selectors (for `jenesis`) or as program arguments to the launched main (for `jenesis-exec`); they reach `Project.main` / `Execute.main` as `String... args`, not the JVM. Set JVM-level flags such as the `jenesis.project.*` and `jenesis.execute.*` system properties via `JAVA_OPTS`, which the scripts splice in before `-m`: JAVA_OPTS="-Djenesis.project.layout=maven -Djenesis.test.skip=" jenesis JAVA_OPTS="-Djenesis.execute.module=tools -Djenesis.execute.docker=true" jenesis-exec arg1 arg2 Multiple `-D…` (or `-X…`) flags can be chained inside the single `JAVA_OPTS` string. - **`jenesis-init`** extracts the bundled `*-sources.jar` into each target's `build/jenesis` directory, deleting any existing `build/jenesis` first and writing the SDK's version into `build/jenesis/jenesis.version`. The bundled `module-info.java` is dropped, since the consuming project carries its own. With no arguments the current directory is the target; with one or more arguments each path is processed in turn and lines are prefixed with the target name. This is the no-submodule analog of the `ln -s` step from [Installing](#installing). - **`jenesis-validate`** extracts the bundled sources to a temporary directory and SHA-256-compares every file against the matching file in the target's `build/jenesis`. It reports per-file `differs` / `missing` / `additional` lines and a final summary with counts, and emits a `version differs` line when `build/jenesis/jenesis.version` does not match the SDK's version. As with `jenesis-init`, the current directory is the default target. - **`jenesis-version`** prints the SDK version and the version recorded in `build/jenesis/jenesis.version` for each target. It exits 0 only when every target's recorded version matches the SDK version, and 1 otherwise (including when `build/jenesis` or the version file is missing). It is intended as a CI check that a submodule update has been propagated to all consuming projects. - **`jenesis-switch`** discovers the version recorded in `build/jenesis/jenesis.version` across the given targets and switches the current shell to that version via SDKMAN, installing it on the fly if it is not already present locally. Unlike the other scripts, it **must be sourced**, so that `sdk use` modifies the calling shell rather than a subprocess: . jenesis-switch # source from current directory . jenesis-switch project-a project-b Every target must agree on the recorded version; a mismatch aborts the switch with exit code 1. A version that is not known to SDKMAN (i.e. `sdk install jenesis ` does not produce a candidate directory) also exits 1. The install runs non-interactively and **does not** set the installed version as the SDKMAN default - the switch only affects the current shell. Run `sdk default jenesis ` separately if you want new shells to start on that version as well. The script ships only as a POSIX shell script; the `.bat` stub errors out because native Windows is not a supported SDKMAN target (use WSL instead). Because `jenesis-switch` modifies the current shell's `PATH`, a chained call resolves to the newly-switched SDK's binaries. A useful "align then verify" idiom for CI is: . jenesis-switch && jenesis-validate This installs the project's pinned version on the fly (if needed), activates it for the shell, and then SHA-256-checks the checked-in `build/jenesis` against the bundled sources of that exact version. The combined exit code is non-zero if either step fails, so the chain is safe to drop into a CI pipeline as a single verification command. ### Installing via SDKMAN [SDKMAN](https://sdkman.io) packages Jenesis as the `jenesis` candidate. On any system where SDKMAN runs (Linux, macOS, WSL), installation is a single command: sdk install jenesis # install the latest version sdk install jenesis 0.0.2 # pin a specific version sdk use jenesis 0.0.2 # switch the active version in this shell sdk current jenesis # show the active version SDKMAN unpacks the zip under `~/.sdkman/candidates/jenesis//` and adds its `bin/` directory to `PATH`, so `jenesis`, `jenesis-init`, `jenesis-validate`, and `jenesis-version` become immediately available. From a project root that contains a `module-info.java` or `pom.xml`, the usual entry point reduces to: jenesis On systems without SDKMAN (notably Windows without WSL), the same zip can be downloaded from the matching GitHub release and unpacked anywhere on disk; adding the unpacked `bin/` directory to `PATH` makes the scripts available with no further setup.