Migrating a Maven or Gradle build
A Maven or Gradle build moves to Jenesis one concern at a time. After each step you build, so a failure always has a single cause, and the old build keeps working beside the new one until what both produce compares equal.
Bringing the tool in
Vendor the engine as Getting started describes and commit
build/. The build runs on JDK 25 or newer; the release it compiles for is a setting of its own. While
both builds exist, keep them apart:
- Maven's
target/is the folder Jenesis writes, so run a Maven build in a second checkout, made withgit worktree add. gradle cleandeletesbuild/withbuild/in it. Move Gradle's output aside with one line in the rootjenesis build.orgradle. kts build., and listgradle gradle-build/in.in place ofgitignore build/:allprojects { layout..buildDirectory = layout. projectDirectory. dir("gradle-build") } - A
.linegitignore buildorbuild/also hidesbuild/, and git cannot include a file again below an ignored folder. Writejenesis /andbuild/ * !/instead.build/ jenesis/ - A header or licence check of the old build, such as Apache RAT, flags the vendored sources: exclude
build/from it.jenesis
Choosing the build declaration
| Declaration | Layout | What it means for the move |
|---|---|---|
pom. |
maven |
The quicker move. A Maven build keeps its pom. files as they stand; a Gradle build writes one per project. |
module-info. |
modular_ |
The Java Module System declares the build, and the published POM is generated from it. |
No build. is read. The pom. a Gradle project needs holds only the coordinates, the dependencies
with their scopes, and maven..
Whether the code can be modules yet
Code compiled for Java 8 or older cannot be a module, since a module-info. needs release 9, so it
migrates to pom. and stays there. A descriptor it ships for Java 9 and later lives in
src/, compiled as a
multi-release overlay, and a dependency without
a module name gets the name it requires from a <!--jenesis. comment.
Where the descriptor moves there, the old build follows it: ModiTect's moduleInfoFile, and the
maven-javadoc-plugin's sourcepath and excludePackageNames.
Otherwise, a module cannot be declared while a package is split: held by the tests and the main code of one
project, as white-box tests are, or by two projects of the build. This program prints each split package and
where it is held. Save it outside the sources as SplitPackages. and run java SplitPackages. from the
root, which the JDK runs as it stands on any operating system:
void main() throws IOException {
var source = Pattern.compile("(.*)src/(main|test)/java/(.+)/[^/]+[.]java");
var owners = new TreeMap<String, Set<String>>();
try (var files = Files.walk(Path.of("."))) {
files.map(file -> file.toString().replace(File.separatorChar, '/'))
.map(source::matcher)
.filter(Matcher::matches)
.forEach(match -> owners.computeIfAbsent(match.group(3), _ -> new TreeSet<>())
.add(match.group(2) + " " + match.group(1)));
}
owners.forEach((folder, held) -> {
if (held.stream().filter(owner -> owner.startsWith("main ")).count() > 1
|| held.stream().anyMatch(owner -> owner.startsWith("test ")
&& held.contains("main " + owner.substring(5)))) {
IO.println(folder.replace('/', '.') + " " + held);
}
});
}
A package it prints, or one that a dependency holds as well, is split.
Two phases
Where a package is split, the move takes two phases. Phase one migrates to pom., changing the build
and nothing of the code; its first passing build is the baseline later steps are compared against. Phase two
moves the result to module-info. as a change of its own, once the pom. build compares equal with the
old one. Code without a split package may go to module-info. directly; this chapter is the pom.
route.
What a pom.xml keeps
A pom. is read for what it declares, not for how Maven builds it:
- Read: coordinates; parents, local or fetched, a parent that cannot be fetched failing the build;
<modules>; properties; dependencies of every scope butsystem, with<optional>,<exclusions>,<type>and<classifier>, a type other thanjar,test-jar,ejb,ejb-client,maven-pluginandbundleplaced on no path, as Maven places it on none, and refused,pomaside, as a direct dependency of a module the build builds;<dependencyManagement>with imported BOMs;maven., or elsecompiler. release targetorsource, withtestReleaseandenablePreview; the metadata of the module and its parents; source and resource directories, a local parent's where the module names none, andsrc/where they exist for each name{main,test}/ <name> jenesis.lists - a project that keeps Kotlin or Groovy inmaven. sources src/ormain/ kotlin src/setsmain/ groovy jenesis.in itsmaven. sources= kotlin,groovy jenesis.; theproperties <!--jenesis.andplugin--> <!--jenesis.comments of the module and of a local parent, and its ownalias--> <!--jenesis.; and a profile of any POM that Maven activates bypin--> <jdk>or by<activeByDefault>. An unclosed<jdk>range such as[9,or[9reads as[9,); a range that is none leaves the profile of a fetched POM inactive and fails the build in the project's own. - Ignored:
<build><plugins>and<pluginManagement>, a profile activated by a property, the operating system, a file or-P,<repositories>andsettings., a resource's includes, excludes,xml targetPathand filtering, and every packaging butjarandbundle. Abundlebuilds a jar whose OSGi headers aMETA-INF/among the resources or a plugin supplies. AMANIFEST. MF jarmodule without sources or resources is built only where its configuration folder names a plugin, which may generate them; otherwise a[SKIPPED]line names it. Apomaggregator is followed for its modules, and apommodule with a<dependencyManagement>and no modules is published as a bill of materials. Any other packaging, awaramong them, is not built, and a[SKIPPED]line names its module.
A version a Maven extension supplies, as nisse or jgitver do, names a property no pom. defines and is
refused. Set jenesis. instead, which the dependencies between the project's own modules take
as well.
Nothing else ignored is reported, so list the old build's plugins, profiles and repositories before deleting
anything. A source directory gives the jar only what its compilers read, as Maven's does. A resource directory is copied whole; one that holds target/ or
., as . does, fails the build and names the remedy, -Djenesis.. Metadata is inherited as Publishing describes.
A test module-info. that names the main module itself, the --patch-module idiom for white-box tests,
is not supported: move it to a folder the old build alone compiles, and the tests run on the class path. One
that declares a module of its own is compiled as a module, but its tests run on the class path too, so tests of
Java Module System behaviour stay with the old build until phase two.
Where plugin configuration goes
Most plugins become a file that switches a tool on, a line of packaging., a tag or POM comment, or
a setting. A file sits in a configuration folder of the
module, the first one found being the whole configuration. A tool the build downloads is switched on by its
<name>., whose configuration= names the tool's own file and whose other lines are its
values, or by that file alone where no properties are found; jenesis. switches it off. What
lived inside a plugin's own configuration moves here:
| In Maven or Gradle | In Jenesis |
|---|---|
compilerArgs, options. |
process-javac. |
fork with meminitial, maxmem or -J options |
-J<option>= in process-javac., which runs javac in a JVM of its own |
A multi-release jar's sources in src/, a compiler execution with multiReleaseOutput, a Gradle java<N> source set |
The same sources moved below src/, compiled as a multi-release overlay |
annotationProcessorPaths |
A dependency of <type>processor</; one named without a version takes it from <dependencyManagement> |
The kotlin-maven-plugin, scala-maven-plugin or GMavenPlus, Gradle's kotlin("jvm"), scala or groovy plugin |
Nothing: a module's ., . or . sources are compiled wherever the build finds them; a compiler plugin is @jenesis. or @jenesis. |
| The dokka-maven-plugin or Gradle's Dokka plugin | A dokka. |
Gradle's com. plugin and its ksp(. dependencies |
A ksp. holding the processors' arguments, with each processor as @jenesis. |
| Surefire includes and groups | -Djenesis., -Djenesis. |
| Surefire excludes | -Djenesis., as . |
argLine, jvmArgs of the tests |
process-test. |
Surefire's rerunFailingTestsCount, Gradle's test-retry plugin |
A retry of the test framework, such as JUnit Pioneer's @RetryingTest |
forkCount with reuseForks=, forkEvery |
A test module of its own, since each test module runs in a JVM of its own |
| The environment of the tests | environment-test. |
| The Checkstyle, PMD, detekt, ktlint, Scalastyle, scalafmt or CodeNarc plugin | Its file in the configuration folder - checkstyle., pmd., detekt., ., scalastyle-config., . or codenarc. - or a <name>. naming it as configuration=; jenesis. fails the build on a finding |
| The jacoco-maven-plugin or pitest-maven | A jacoco. or a pitest. |
| The jaxb2, protobuf, avro, jaxws, openapi-generator or antlr4 Maven plugin | An xjc., protoc., avro., wsimport., openapi. or antlr. that binds the contracts |
| The spotbugs-maven-plugin, forbiddenapis, japicmp or revapi | A spotbugs., forbiddenapis., japicmp. or revapi. with its values |
| Spotless, the fmt-maven-plugin or the formatter-maven-plugin | A googlejavaformat., palantir. or eclipse. for the Java formatter, a ktfmt., or the . of ktlint and the . of scalafmt; -Djenesis. rewrites the sources |
Checkstyle's propertyExpansion, configProperties |
The other lines of checkstyle. |
PMD's minimumPriority, rulesMinimumPriority |
-Djenesis. |
The mainClass of the jar, shade or exec plugin |
A <mainClass> property |
manifestEntries, jar. |
A META-INF/ among the resources |
addDefaultImplementationEntries |
Written by default |
A resource with a targetPath |
-Djenesis. |
A <profile> chosen with -P, Gradle properties |
A jenesis-<profile>., selected with -Djenesis. |
<repositories>, settings. |
-Djenesis. or MAVEN_ |
| A toolchain | -Djenesis. |
Tests on another JDK, Surefire's jvm or a Test task's javaLauncher |
The whole build again with -Djenesis., as a CI matrix leg |
A -tests jar |
-Djenesis. |
maven-jar-plugin executions with a <classifier>, build-helper's attach-artifact |
A classifiers. naming each variant's folders |
os-maven-plugin's ${os. |
One profile per platform, each holding that platform's classifiers. |
A native build run by exec-maven-plugin or maven-antrun-plugin, Gradle's cpp-library |
A plugin at binary/ that writes the library into a folder classifiers. names |
javac -h, as maven-compiler-plugin's <headerOutputDirectory> or Gradle's options. |
The same plugin's header step, the build's javac step with headers(true) |
maven., maven. |
stage= in the module's packaging., which keeps it out of export and release alike |
The maven-plugin-plugin, <packaging>maven-plugin</ |
<packaging>jar</ and a mavenplugin. in the module's configuration folder, its goalPrefix as goalprefix= |
Gradle's java-gradle-plugin and its gradlePlugin. |
A META-INF/ among the resources naming the implementation-class, and a gradleplugin. in the project's configuration folder |
The asciidoctor-maven-plugin, Gradle's org. |
An asciidoctor. naming the documents as documents=, its attributes as attribute. lines |
The flatten-maven-plugin's flattenDependencyMode= |
None: a published POM names what the module declares, and a consumer's resolution decides the rest - see what the published POM names |
A tool resolves in a group named after its plugin, such as checkstyle:tool or jacoco:tool, once per module. A pom. pins
it with a <!--jenesis. comment, a child of <project> with one coordinate per line, which pin
completes with the tool's closure:
<!--jenesis.pin
checkstyle:tool/maven/<groupId>/<artifactId> <version>
-->
pin writes one into every module's POM, since a parent's is not inherited, unlike its
<!--jenesis. comments.
In a process file, a flag given more than once, as --add-opens is, takes one argument per line of its value,
and a --release is refused, since maven. declares it. A source and target without a
release compile as --release, which also checks the API, so code calling a newer API behind a version check
needs a --source= line. javac runs without -g, so a test that reads parameter names needs
-g= or -parameters= there. A plugin may pass flags its configuration never shows, as a convention plugin
adds -parameters, so compare the old build's effective javac arguments: mvn -X compile prints them after
"Command line options:", and gradle compileJava --debug on its "Compiler arguments:" line.
Checkstyle reads a copy of the sources below target/, so a suppression keyed on a source folder, as
src/, matches nothing. Key it on the package's folders instead, as [/.
The tests run a little differently, too. jenesis. replaces the default naming and its JUnit 4 check
rather than narrowing them, and the root module's entry is written /. Unlike Surefire, they do not
run in the module's folder, and no basedir property is set: a test that opens src/ by a relative path
is changed to read that file as a test resource. They run against the module's jar, so one that turns getResource
into a java. needs
-Djenesis.. A build prints no test totals; -Djenesis. streams the runner's summary.
Shading
Shading is not supported: nothing is relocated, and no class file is rewritten. A shaded library sits in another jar under another name, where neither licence checks nor the usage counts an open source project's funding relies on find it. A layer or packaging keeps each library a jar of its own under its own coordinate, pinned and reported like any other.
A dependency kept private, whose version must not meet the consumer's, goes into a
module layer, declared in module-info. alone, so a
pom. build keeps it plain until phase two. One runnable jar is launcher= in packaging.,
a repackaged Spring Boot application included; bundle= ships the jars whole, beside the argument file that
launches them.
Fewer dependencies for consumers means publishing the dependency as a dependency.
Pinning and comparing the two builds
Until it is pinned, a tool the build resolves itself - the test runner, Error Prone, Checkstyle - takes its
newest release without a pre-release qualifier, so the first pin settles those as well. Commit before
running it: pin writes the versions and checksums it resolved into each <dependencyManagement>, where every
entry it does not resolve, an imported BOM and every comment stay. A pinned entry outranks a BOM, so to move to a new BOM version, change it, delete the
entries pin wrote and pin again. Then build with -Djenesis., as CI should.
Before retiring the old build, compare what both produce: the contents of each jar, the dependency tree
(dependencies against mvn dependency:tree or gradle dependencies), the number of tests run, and the POM a
consumer receives, which is generated rather than copied from yours. A pom. resolves a
conflict as Maven does, the nearest version winning, where Gradle takes the highest; manage each version Gradle
resolved higher in <dependencyManagement>.
Retiring the old build
Remove what Jenesis replaces: the plugin configuration, the wrapper, and the CI steps that called them. A CI job
that built runs java -Djenesis.. One that published to Maven Central runs the
release selector with a jreleaser.
naming the jreleaser. and -Djenesis., and the version as -Djenesis., its credentials in
the JRELEASER_ variables; one that deployed to a
repository of its own names it in
MAVEN_ and its key in MAVEN_ instead. Then let the ide selector
write the IntelliJ IDEA, VS Code or Eclipse project. A project in the maven layout keeps its pom. files,
which are its build declaration from then on.
Migrating with a coding agent
The same guidance ships with the engine, written for a coding agent. To hand one the move, enter this in its prompt, or paste what the command prints:
! java build/jenesis/Make.java prompt/migrate
The task follows skill/, this chapter and phase two written for an agent, and skill/ names
the built-in that replaces each common plugin. The agent closes with a summary: the build declaration it chose,
what replaced each plugin, the split packages that kept the build on pom., each shaded dependency and what
replaced it, what still differs from the old build, and the command CI runs.