Dependencies
Every non-trivial build pulls in libraries. This chapter is about where you declare them, how Jenesis turns each declaration into a downloaded jar, and - because a build is only as trustworthy as the bytes it pulls - how to pin every one of those jars to an exact version and checksum, recorded in your own sources.
Declaring a dependency
You never add a dependency in a build script. You declare it the same way the ecosystem already does, and the place depends on your layout (see Core concepts):
-
A
pom.xmlproject lists a dependency the normal Maven way, in<dependencies>:<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.12.0</version> </dependency> -
A modular project (
module-info.java) declares arequires, and nothing else - the module name is the dependency:module demo.app { requires com.fasterxml.jackson.databind; }
That is the whole surface. Jenesis reads these existing files, resolves the transitive closure, and puts the result on the compile and runtime paths.
The two repositories
Jenesis resolves through two named repositories, one per kind of coordinate:
maven- Maven coordinates (groupId:artifactId:version). Fetched over HTTP from Maven Central (https://repo1.maven.org/maven2/) and hardlinked into your local Maven repository (~/.m2/repository), exactly wheremvnkeeps them.module- Java module names. Resolved through repo.jenesis.build, the module-name index that maps a name likecom.fasterxml.jackson.databindto its artifact and 302-redirects to the file on Maven Central.
Which one a dependency uses follows from the layout. A pom.xml declares Maven coordinates, so it resolves
through maven. A requires names a module, so it resolves through module - and this is the step that turns
a module name into something downloadable. The Jenesis Modules section documents that lookup
in full; the short version is that it is a thin, module-name-addressable mirror of Maven Central.
modular_to_maven layout, a requires is resolved to the declaring
module's Maven coordinate (its POM is fetched through the module index), and transitive resolution
then proceeds through Maven - so a module project reaches automatic-module and plain-classpath libraries too.
The strict modular layout resolves purely by module name. Core concepts covers the
difference; the dependencies selector below shows it concretely.
Pointing at a different repository
To resolve through a corporate mirror or a private repository instead of the public defaults, set an environment variable before the build - no project change required:
| Variable | What it overrides |
|---|---|
MAVEN_REPOSITORY_URI |
The Maven upstream. Accepts a comma-separated list, queried left to right; an entry may append |-separated group ids to serve only those groups, and a bare @ splices the default chain back in (https://nexus.corp/,@). |
MAVEN_REPOSITORY_TOKEN |
Sent verbatim as the Authorization header on every Maven fetch (e.g. Bearer … or Basic …). |
MAVEN_REPOSITORY_LOCAL |
The local Maven repository directory (default ~/.m2/repository). |
JENESIS_REPOSITORY_URI |
The module-index base URL (default https://repo.jenesis.build/), with the same list/filter/@ grammar. |
JENESIS_REPOSITORY_TOKEN |
The Authorization header for module-index fetches. |
http - only https and file are
allowed. A build that must pull from an internal http mirror has to opt in explicitly with
-Djenesis.repository.insecure=true. A credential token is dropped before any redirect to a
different host, so it never leaks to a redirect target.
Seeing what resolved
The dependencies selector prints each module's resolved tree, the way mvn dependency:tree does:
java build/jenesis/Project.java dependencies
Each node shows the version every parent requested, the negotiated version inline when it differs
([1,2] -> 2), the scope, and the dependency's licence ({Apache-2.0}). A per-module Resolved dependencies
list and a licence summary follow the tree. It is the fastest way to answer "why is this version on my class
path?" before you pin anything.
Version negotiation
When two paths through the graph ask for different versions of the same library, Jenesis picks one. The rule matches the repository:
- Maven coordinates use Maven's own nearest-wins conflict resolution, and understand version ranges
and the
LATEST/RELEASEselectors - the same behaviourmvngives you. - Module names use first-parent-wins: the first requirer reached in the resolution walk fixes the version, and a later, deeper requirer asking for a different version is ignored.
To override the negotiated result, declare the version you want directly - a <version> (or a
<dependencyManagement> entry) in Maven, or a pin in a modular project (below). A declared version always
beats what negotiation would have chosen.
Excluding a transitive (Maven only)
A Maven dependency can drag in a transitive you do not want. Prune it with an <exclusions> block, exactly as
in Maven - the excluded artifact never reaches the class path, tests included:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.12.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
There is no modular equivalent, and there is nothing to add. A module only ever sees what its
module-info.java requires, so an unwanted transitive cannot silently appear on the module path in the first
place. Exclusions are therefore a Maven-layout feature only.
Pinning: exact versions and checksums
By default a resolved version can still drift - a RELEASE selector or an unpinned range resolves to whatever
is newest today. Pinning freezes the entire transitive closure: every dependency records both an exact
version and the SHA-256 checksum of the jar, in your own committed sources. A later build that resolves a jar
whose bytes do not match the recorded checksum fails - so the build is resistant to a supply-chain swap at
the coordinate you already trusted.
Recording the pins
You do not write pins by hand. The pin selector resolves the closure, hashes each jar, and rewrites your
sources with the result:
java build/jenesis/Project.java pin
pin is opt-in - it is not part of the default build - and it writes back into your project tree rather than
under target/. In a modular project it adds a @jenesis.pin tag per dependency on the module
declaration; in a pom.xml project it fills a <dependencyManagement> block, tagging each entry with a
<!--Checksum/…--> comment. Commit the result and the pin set travels with the project.
A pin in module-info.java reads:
/**
* @jenesis.pin com.fasterxml.jackson.databind 2.18.2 SHA-256/8f2b...c41
*/
module demo.app {
requires com.fasterxml.jackson.databind;
}
The grammar is @jenesis.pin <group>/<repository>/<coordinate> <version> [<algorithm>/<hash>], with two
shorthands for a project's own dependencies (the main group):
| You write | Means |
|---|---|
com.fasterxml.jackson.databind |
a module name - main/module/… |
org.slf4j/slf4j-api |
a Maven groupId/artifactId - main/maven/… |
main/maven/org.foo/bar/jar/native |
a coordinate with a type or classifier, written in full |
A module project can therefore pin a plain Maven transitive it pulls in (say a non-modular library behind a
named module) with the groupId/artifactId form, even though its own dependencies resolve through the module
repository. The same @jenesis.pin grammar - including a :<classifier> qualifier and a trailing [<guard>]
platform guard - is covered in Core concepts; here it is enough that pin writes and refreshes these lines
for you.
pin whenever you change a dependency; it refreshes the versions and checksums from the
new closure and drops entries that no longer resolve. To record versions without checksums, pass
-Djenesis.pin.checksum=false. The digest defaults to SHA-256 and is set with
-Djenesis.project.digest=<algorithm>.
Enforcing the pins
How strictly the recorded pins are enforced is controlled by one property,
-Djenesis.dependency.pin:
-Djenesis.dependency.pin |
Versions | Checksums |
|---|---|---|
| (unset - the default) | honoured where pinned | verified where a pin carries one; a dependency with no checksum is allowed |
strict |
honoured | required - any third-party dependency without a pinned checksum fails the build |
versions |
honoured | not verified |
ignore |
float freely | not verified |
The default already validates every checksum you have recorded - a mismatch always fails the build. Strict
mode goes further and refuses to build at all until nothing is left unpinned, which is what you want in CI
once a project is fully pinned: run pin, commit, then build under -Djenesis.dependency.pin=strict so no new
un-vetted artifact can slip in unnoticed.