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 which version it settles on when two paths disagree. It ends with the two tags that let you correct a closure you do not control: dropping a transitive you do not want, and naming a library that arrives without a module name of its own.

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.xml project 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 a requires, 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 HTTPS from Maven Central (https://repo1.maven.org/maven2/) into your local Maven repository (~/.m2/repository), exactly where mvn keeps them, and hard-linked from there into the build.
  • module - Java module names. Resolved through the Jenesis Module Index at repo.jenesis.build, which maps a name like com.fasterxml.jackson.databind to its artifact and 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 Module Index section documents that lookup in full.

Under the default 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. A module project therefore reaches automatic-module and plain class-path 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 a system property or an environment variable before the build. No project change is required, and a property beats the variable of the same name:

Property (environment variable) What it overrides
jenesis.maven.uri (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/,@).
jenesis.maven.token (MAVEN_REPOSITORY_TOKEN) Sent verbatim as the Authorization header on every Maven fetch (e.g. Bearer … or Basic …; a Jenesis Repository key can be given as is).
jenesis.maven.local (MAVEN_REPOSITORY_LOCAL) The local Maven repository directory (default ~/.m2/repository).
jenesis.module.uri (JENESIS_REPOSITORY_URI) The module index base URL (default https://repo.jenesis.build/), with the same list/filter/@ grammar.
jenesis.module.token (JENESIS_REPOSITORY_TOKEN) The Authorization header for module fetches, when jenesis.module.uri points at a server that needs one.
jenesis.module.local (JENESIS_REPOSITORY_LOCAL) The local module repository directory (default ~/.jenesis).
Fetches are refused over plaintext 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, the dependency's licence ({Apache-2.0}), and local for a module built inside this project rather than fetched. 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.

When the external closure is just noise, -Djenesis.tree.format=compact keeps only the local modules and folds everything external into a count per branch, so a large multi-module project shows its own shape at a glance.

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/RELEASE selectors - the same behaviour mvn gives 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 (the next chapter). A declared version always beats what negotiation would have chosen.

Choosing a different strategy

Each repository's rule is the sensible default, and each is selectable when you want another. On the Maven side, -Djenesis.resolver.maven takes four values:

Value Rule
maven (the default) Maven's own: declared versions, ranges and RELEASE/LATEST resolved from repository metadata, nearest-wins on a conflict, with ranges intersected when one competes.
closest The same minus the range arbitration - the nearest declaration simply stands, and no metadata is fetched to settle a conflict.
latest / release Ignore every declared version and take the <latest> or <release> entry of each coordinate's metadata.

On the module side, -Djenesis.resolver.module decides what happens when two compiled module-info files record different versions of the same requirement. first (the default) keeps the one nearest the roots, fail reports the disagreement instead of discarding one, and ignore keeps no compiled version at all.

latest and release are upgrade probes, not build modes. They override pinned versions too, so the checksum recorded beside a pin no longer describes the artifact that resolves and stops applying - under strict pinning the build then fails. Use them to find out what an upgrade would pull in, then record the result with pin.

Excluding a transitive

A dependency can drag in a transitive you do not want. Pruning it is a Maven mechanism (an exclusion tells the resolver to skip a subtree of a POM), so it is available wherever a POM is read: in the maven layout, and in the default modular_to_maven layout, whose requires resolve through Maven.

In a pom.xml it is 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>

A module-info.java states the same thing as a tag, since it has no <dependencies> block to hang it on:

/**
 * @jenesis.exclude org.apache.commons.text org.apache.commons/commons-lang3
 */
module demo.sample {
    requires org.apache.commons.text;
}

One line names the module to prune and any number of <groupId>/<artifactId> targets. Repeated lines for the same module add up, so a growing list of upstream mistakes stays readable. A target is an artifact, never one of its variants, so it carries no version, type, or classifier. Excluding from a module the declaration does not requires is an error rather than a silent no-op, because it is a typo in every case that matters.

Either way the artifact takes the whole subtree it pulled in with it. Because it never enters the resolved closure, there is nothing left to leak: it is off the compile and test paths, absent from the generated POM, and absent from the bill of materials and the compliance reports. The build genuinely never fetched it.

The strict modular layout is the one place this does not apply. Resolution there matches module descriptors and never reads a POM, so there is no transitive POM dependency to prune and the tag is rejected rather than ignored. Nothing is lost: a module only ever sees what it requires.

Naming a library that has no module name

Some libraries still ship as a plain jar: no module-info, and not even an Automatic-Module-Name. On the module path such a jar becomes an automatic module named after its file, which changes with the file and so cannot be requiresd reliably. An alias gives one a name your project chooses:

/**
 * @jenesis.alias org.kohsuke.args4j args4j/args4j
 */
module demo.cli {
    requires org.kohsuke.args4j;

    opens demo.cli to org.kohsuke.args4j;
}

The tag maps a module name onto a <groupId>/<artifactId> the resolved closure already contains, and the name is then a module name like any other; the opens above is what lets args4j set the annotated fields by reflection. Nothing is synthesised and no jar is rewritten. The artifact is placed under the aliased file name, which is exactly the name the JDK derives an automatic module from, so a pinned checksum keeps describing the bytes on the command line.

Two rules keep an alias predictable. It carries no version: the version comes from a pin, a bill of materials, or the closure the alias names, and is stated in one place only. And it only ever renames: a jar that already declares a module-info or an Automatic-Module-Name is rejected, because it is addressable under that name already. An alias also travels. A project that depends on a module which declared one inherits the name without redeclaring it.

An alias does not have to be something you requires yourself. Naming a transitive dependency the project never mentions is enough to make it a module every other module can require. That is how a closure of plain jars is brought onto the module path one deliberate name at a time.

Aliases are a modular_to_maven feature: they reach an artifact by its Maven coordinate, which the strict modular layout does not use.

Two runnable projects cover this chapter: demo-27 excludes Commons Lang from Commons Text and proves with a test that it is gone - in a POM, with the tag form beside it; and demo-31 gives args4j - a library with no module identity at all - a name of its own and opens a package to it. Each is a runnable project - see Demos.