1# Maven {#maven}
2
3Maven is a well-known build tool for the Java ecosystem; however, it has some challenges when integrating into the Nix build system.
4
5The following provides a list of common patterns with how to package a Maven project (or any JVM language that can export to Maven) as a Nix package.
6
7## Building a package using `maven.buildMavenPackage` {#maven-buildmavenpackage}
8
9Consider the following package:
10
11```nix
12{
13 lib,
14 fetchFromGitHub,
15 jre,
16 makeWrapper,
17 maven,
18}:
19
20maven.buildMavenPackage rec {
21 pname = "jd-cli";
22 version = "1.2.1";
23
24 src = fetchFromGitHub {
25 owner = "intoolswetrust";
26 repo = "jd-cli";
27 tag = "jd-cli-${version}";
28 hash = "sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q=";
29 };
30
31 mvnHash = "sha256-kLpjMj05uC94/5vGMwMlFzLKNFOKeyNvq/vmB6pHTAo=";
32
33 nativeBuildInputs = [ makeWrapper ];
34
35 installPhase = ''
36 runHook preInstall
37
38 mkdir -p $out/bin $out/share/jd-cli
39 install -Dm644 jd-cli/target/jd-cli.jar $out/share/jd-cli
40
41 makeWrapper ${jre}/bin/java $out/bin/jd-cli \
42 --add-flags "-jar $out/share/jd-cli/jd-cli.jar"
43
44 runHook postInstall
45 '';
46
47 meta = {
48 description = "Simple command line wrapper around JD Core Java Decompiler project";
49 homepage = "https://github.com/intoolswetrust/jd-cli";
50 license = lib.licenses.gpl3Plus;
51 maintainers = with lib.maintainers; [ majiir ];
52 };
53}
54```
55
56This package calls `maven.buildMavenPackage` to do its work. The primary difference from `stdenv.mkDerivation` is the `mvnHash` variable, which is a hash of all of the Maven dependencies.
57
58::: {.tip}
59After setting `maven.buildMavenPackage`, we then do standard Java `.jar` installation by saving the `.jar` to `$out/share/java` and then making a wrapper which allows executing that file; see [](#sec-language-java) for additional generic information about packaging Java applications.
60:::
61
62### Overriding Maven package attributes {#maven-overriding-package-attributes}
63
64```
65overrideMavenAttrs :: (AttrSet -> Derivation) | ((AttrSet -> Attrset) -> Derivation) -> Derivation
66```
67
68The output of `buildMavenPackage` has an `overrideMavenAttrs` attribute, which is a function that takes either
69- any subset of the attributes that can be passed to `buildMavenPackage`
70
71 or
72- a function that takes the argument passed to the previous invocation of `buildMavenPackage` (conventionally called `old`) and returns an attribute set that can be passed to `buildMavenPackage`
73
74and returns a derivation that builds a Maven package based on the old and new arguments merged.
75
76This is similar to [](#sec-pkg-overrideAttrs), but notably does not allow accessing the final value of the argument to `buildMavenPackage`.
77
78:::{.example}
79### `overrideMavenAttrs` Example
80
81Use `overrideMavenAttrs` to build `jd-cli` version 1.2.0 and disable some flaky test:
82
83```nix
84jd-cli.overrideMavenAttrs (old: rec {
85 version = "1.2.0";
86 src = fetchFromGitHub {
87 owner = old.src.owner;
88 repo = old.src.repo;
89 rev = "${old.pname}-${version}";
90 # old source hash of 1.2.0 version
91 hash = "sha256-US7j6tQ6mh1libeHnQdFxPGoxHzbZHqehWSgCYynKx8=";
92 };
93
94 # tests can be disabled by prefixing it with `!`
95 # see Maven documentation for more details:
96 # https://maven.apache.org/surefire/maven-surefire-plugin/examples/single-test.html#Multiple_Formats_in_One
97 mvnParameters = lib.escapeShellArgs [
98 "-Dsurefire.failIfNoSpecifiedTests=false"
99 "-Dtest=!JavaDecompilerTest#basicTest,!JavaDecompilerTest#patternMatchingTest"
100 ];
101
102 # old mvnHash of 1.2.0 maven dependencies
103 mvnHash = "sha256-N9XC1pg6Y4sUiBWIQUf16QSXCuiAPpXEHGlgApviF4I=";
104})
105```
106:::
107
108### Offline build {#maven-offline-build}
109
110By default, `buildMavenPackage` does the following:
111
1121. Run `mvn package -Dmaven.repo.local=$out/.m2 ${mvnParameters}` in the
113 `fetchedMavenDeps` [fixed-output derivation](https://nixos.org/manual/nix/stable/glossary.html#gloss-fixed-output-derivation).
1142. Run `mvn package -o -nsu "-Dmaven.repo.local=$mvnDeps/.m2"
115 ${mvnParameters}` again in the main derivation.
116
117As a result, tests are run twice.
118This also means that a failing test will trigger a new attempt to realise the fixed-output derivation, which in turn downloads all dependencies again.
119For bigger Maven projects, this might lead to a long feedback cycle.
120
121Use `buildOffline = true` to change the behaviour of `buildMavenPackage to the following:
1221. Run `mvn de.qaware.maven:go-offline-maven-plugin:1.2.8:resolve-dependencies
123 -Dmaven.repo.local=$out/.m2 ${mvnDepsParameters}` in the fixed-output derivation.
1242. Run `mvn package -o -nsu "-Dmaven.repo.local=$mvnDeps/.m2"
125 ${mvnParameters}` in the main derivation.
126
127As a result, all dependencies are downloaded in step 1 and the tests are executed in step 2.
128A failing test only triggers a rebuild of step 2 as it can reuse the dependencies of step 1 because they have not changed.
129
130::: {.warning}
131Test dependencies are not downloaded in step 1 and are therefore missing in
132step 2 which will most probably fail the build. The `go-offline` plugin cannot
133handle these so-called [dynamic dependencies](https://github.com/qaware/go-offline-maven-plugin?tab=readme-ov-file#dynamic-dependencies).
134In that case you must add these dynamic dependencies manually with:
135```nix
136maven.buildMavenPackage rec {
137 manualMvnArtifacts = [
138 # add dynamic test dependencies here
139 "org.apache.maven.surefire:surefire-junit-platform:3.1.2"
140 "org.junit.platform:junit-platform-launcher:1.10.0"
141 ];
142}
143```
144:::
145
146### Stable Maven plugins {#stable-maven-plugins}
147
148Maven defines default versions for its core plugins, e.g. `maven-compiler-plugin`. If your project does not override these versions, an upgrade of Maven will change the version of the used plugins, and therefore the derivation and hash.
149
150When `maven` is upgraded, `mvnHash` for the derivation must be updated as well: otherwise, the project will be built on the derivation of old plugins, and fail because the requested plugins are missing.
151
152This clearly prevents automatic upgrades of Maven: a manual effort must be made throughout nixpkgs by any maintainer wishing to push the upgrades.
153
154To make sure that your package does not add extra manual effort when upgrading Maven, explicitly define versions for all plugins. You can check if this is the case by adding the following plugin to your (parent) POM:
155
156```xml
157<plugin>
158 <groupId>org.apache.maven.plugins</groupId>
159 <artifactId>maven-enforcer-plugin</artifactId>
160 <version>3.3.0</version>
161 <executions>
162 <execution>
163 <id>enforce-plugin-versions</id>
164 <goals>
165 <goal>enforce</goal>
166 </goals>
167 <configuration>
168 <rules>
169 <requirePluginVersions />
170 </rules>
171 </configuration>
172 </execution>
173 </executions>
174</plugin>
175```
176
177## Manually using `mvn2nix` {#maven-mvn2nix}
178::: {.warning}
179This way is no longer recommended; see [](#maven-buildmavenpackage) for the simpler and preferred way.
180:::
181
182For the purposes of this example let's consider a very basic Maven project with the following `pom.xml` with a single dependency on [emoji-java](https://github.com/vdurmont/emoji-java).
183
184```xml
185<?xml version="1.0" encoding="UTF-8"?>
186<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
187 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
188 <modelVersion>4.0.0</modelVersion>
189 <groupId>io.github.fzakaria</groupId>
190 <artifactId>maven-demo</artifactId>
191 <version>1.0</version>
192 <packaging>jar</packaging>
193 <name>NixOS Maven Demo</name>
194
195 <dependencies>
196 <dependency>
197 <groupId>com.vdurmont</groupId>
198 <artifactId>emoji-java</artifactId>
199 <version>5.1.1</version>
200 </dependency>
201 </dependencies>
202</project>
203```
204
205Our main class file will be very simple:
206
207```java
208import com.vdurmont.emoji.EmojiParser;
209
210public class Main {
211 public static void main(String[] args) {
212 String str = "NixOS :grinning: is super cool :smiley:!";
213 String result = EmojiParser.parseToUnicode(str);
214 System.out.println(result);
215 }
216}
217```
218
219You find this demo project at [https://github.com/fzakaria/nixos-maven-example](https://github.com/fzakaria/nixos-maven-example).
220
221### Solving for dependencies {#solving-for-dependencies}
222
223#### buildMaven with NixOS/mvn2nix-maven-plugin {#buildmaven-with-nixosmvn2nix-maven-plugin}
224`buildMaven` is an alternative method that tries to follow similar patterns of other programming languages by generating a lock file. It relies on the maven plugin [mvn2nix-maven-plugin](https://github.com/NixOS/mvn2nix-maven-plugin).
225
226First you generate a `project-info.json` file using the maven plugin.
227
228> This should be executed in the project's source repository or be told which `pom.xml` to execute with.
229
230```bash
231# run this step within the project's source repository
232❯ mvn org.nixos.mvn2nix:mvn2nix-maven-plugin:mvn2nix
233
234❯ cat project-info.json | jq | head
235{
236 "project": {
237 "artifactId": "maven-demo",
238 "groupId": "org.nixos",
239 "version": "1.0",
240 "classifier": "",
241 "extension": "jar",
242 "dependencies": [
243 {
244 "artifactId": "maven-resources-plugin",
245```
246
247This file is then given to the `buildMaven` function, and it returns 2 attributes.
248
249**`repo`**:
250 A Maven repository that is a symlink farm of all the dependencies found in the `project-info.json`
251
252
253**`build`**:
254 A simple derivation that runs through `mvn compile` & `mvn package` to build the JAR. You may use this as inspiration for more complicated derivations.
255
256Here is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/build-maven-repository.nix) of building the Maven repository
257
258```nix
259{
260 pkgs ? import <nixpkgs> { },
261}:
262with pkgs;
263(buildMaven ./project-info.json).repo
264```
265
266The benefit over the _double invocation_ as we will see below, is that the _/nix/store_ entry is a _linkFarm_ of every package, so that changes to your dependency set doesn't involve downloading everything from scratch.
267
268```bash
269❯ tree $(nix-build --no-out-link build-maven-repository.nix) | head
270/nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
271├── antlr
272│ └── antlr
273│ └── 2.7.2
274│ ├── antlr-2.7.2.jar -> /nix/store/d027c8f2cnmj5yrynpbq2s6wmc9cb559-antlr-2.7.2.jar
275│ └── antlr-2.7.2.pom -> /nix/store/mv42fc5gizl8h5g5vpywz1nfiynmzgp2-antlr-2.7.2.pom
276├── avalon-framework
277│ └── avalon-framework
278│ └── 4.1.3
279│ ├── avalon-framework-4.1.3.jar -> /nix/store/iv5fp3955w3nq28ff9xfz86wvxbiw6n9-avalon-framework-4.1.3.jar
280```
281
282#### Double Invocation {#double-invocation}
283::: {.note}
284This pattern is the simplest but may cause unnecessary rebuilds due to the output hash changing.
285:::
286
287The double invocation is a _simple_ way to get around the problem that `nix-build` may be sandboxed and have no Internet connectivity.
288
289It treats the entire Maven repository as a single source to be downloaded, relying on Maven's dependency resolution to satisfy the output hash. This is similar to fetchers like `fetchgit`, except it has to run a Maven build to determine what to download.
290
291The first step will be to build the Maven project as a fixed-output derivation in order to collect the Maven repository -- below is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/double-invocation-repository.nix).
292
293::: {.note}
294Traditionally the Maven repository is at `~/.m2/repository`. We will override this to be the `$out` directory.
295:::
296
297```nix
298{
299 lib,
300 stdenv,
301 maven,
302}:
303stdenv.mkDerivation {
304 name = "maven-repository";
305 buildInputs = [ maven ];
306 src = ./.; # or fetchFromGitHub, cleanSourceWith, etc
307 buildPhase = ''
308 runHook preBuild
309
310 mvn package -Dmaven.repo.local=$out
311
312 runHook postBuild
313 '';
314
315 # keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside
316 installPhase = ''
317 runHook preInstall
318
319 find $out -type f \
320 -name \*.lastUpdated -or \
321 -name resolver-status.properties -or \
322 -name _remote.repositories \
323 -delete
324
325 runHook postInstall
326 '';
327
328 # don't do any fixup
329 dontFixup = true;
330 outputHashAlgo = null;
331 outputHashMode = "recursive";
332 # replace this with the correct SHA256
333 outputHash = lib.fakeHash;
334}
335```
336
337The build will fail, and tell you the expected `outputHash` to place. When you've set the hash, the build will return with a `/nix/store` entry whose contents are the full Maven repository.
338
339::: {.warning}
340Some additional files are deleted that would cause the output hash to change potentially on subsequent runs.
341:::
342
343```bash
344❯ tree $(nix-build --no-out-link double-invocation-repository.nix) | head
345/nix/store/8kicxzp98j68xyi9gl6jda67hp3c54fq-maven-repository
346├── backport-util-concurrent
347│ └── backport-util-concurrent
348│ └── 3.1
349│ ├── backport-util-concurrent-3.1.pom
350│ └── backport-util-concurrent-3.1.pom.sha1
351├── classworlds
352│ └── classworlds
353│ ├── 1.1
354│ │ ├── classworlds-1.1.jar
355```
356
357If your package uses _SNAPSHOT_ dependencies or _version ranges_; there is a strong likelihood that over time, your output hash will change since the resolved dependencies may change. Hence this method is less recommended than using `buildMaven`.
358
359### Building a JAR {#building-a-jar}
360
361Regardless of which strategy is chosen above, the step to build the derivation is the same.
362
363```nix
364{
365 stdenv,
366 maven,
367 callPackage,
368}:
369let
370 # pick a repository derivation, here we will use buildMaven
371 repository = callPackage ./build-maven-repository.nix { };
372in
373stdenv.mkDerivation (finalAttrs: {
374 pname = "maven-demo";
375 version = "1.0";
376
377 src = fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
378 buildInputs = [ maven ];
379
380 buildPhase = ''
381 runHook preBuild
382
383 echo "Using repository ${repository}"
384 mvn --offline -Dmaven.repo.local=${repository} package;
385
386 runHook postBuild
387 '';
388
389 installPhase = ''
390 runHook preInstall
391
392 install -Dm644 target/${finalAttrs.pname}-${finalAttrs.version}.jar $out/share/java
393
394 runHook postInstall
395 '';
396})
397```
398
399::: {.tip}
400We place the library in `$out/share/java` since JDK package has a _stdenv setup hook_ that adds any JARs in the `share/java` directories of the build inputs to the CLASSPATH environment.
401:::
402
403```bash
404❯ tree $(nix-build --no-out-link build-jar.nix)
405/nix/store/7jw3xdfagkc2vw8wrsdv68qpsnrxgvky-maven-demo-1.0
406└── share
407 └── java
408 └── maven-demo-1.0.jar
409
4102 directories, 1 file
411```
412
413### Runnable JAR {#runnable-jar}
414
415The previous example builds a `jar` file but that's not a file one can run.
416
417You need to use it with `java -jar $out/share/java/output.jar` and make sure to provide the required dependencies on the classpath.
418
419The following explains how to use `makeWrapper` in order to make the derivation produce an executable that will run the JAR file you created.
420
421We will use the same repository we built above (either _double invocation_ or _buildMaven_) to setup a CLASSPATH for our JAR.
422
423The following two methods are more suited to Nix then building an [UberJar](https://imagej.net/Uber-JAR) which may be the more traditional approach.
424
425#### CLASSPATH {#classpath}
426
427This method is ideal if you are providing a derivation for _nixpkgs_ and don't want to patch the project's `pom.xml`.
428
429We will read the Maven repository and flatten it to a single list. This list will then be concatenated with the _CLASSPATH_ separator to create the full classpath.
430
431We make sure to provide this classpath to the `makeWrapper`.
432
433```nix
434{
435 stdenv,
436 maven,
437 callPackage,
438 makeWrapper,
439 jre,
440}:
441let
442 repository = callPackage ./build-maven-repository.nix { };
443in
444stdenv.mkDerivation (finalAttrs: {
445 pname = "maven-demo";
446 version = "1.0";
447
448 src = fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
449 nativeBuildInputs = [ makeWrapper ];
450 buildInputs = [ maven ];
451
452 buildPhase = ''
453 runHook preBuild
454
455 echo "Using repository ${repository}"
456 mvn --offline -Dmaven.repo.local=${repository} package;
457
458 runHook postBuild
459 '';
460
461 installPhase = ''
462 runHook preInstall
463
464 mkdir -p $out/bin
465
466 classpath=$(find ${repository} -name "*.jar" -printf ':%h/%f');
467 install -Dm644 target/maven-demo-${finalAttrs.version}.jar $out/share/java
468 # create a wrapper that will automatically set the classpath
469 # this should be the paths from the dependency derivation
470 makeWrapper ${jre}/bin/java $out/bin/maven-demo \
471 --add-flags "-classpath $out/share/java/maven-demo-${finalAttrs.version}.jar:''${classpath#:}" \
472 --add-flags "Main"
473
474 runHook postInstall
475 '';
476})
477```
478
479#### MANIFEST file via Maven Plugin {#manifest-file-via-maven-plugin}
480
481This method is ideal if you are the project owner and want to change your `pom.xml` to set the CLASSPATH within it.
482
483Augment the `pom.xml` to create a JAR with the following manifest:
484
485```xml
486<build>
487 <plugins>
488 <plugin>
489 <artifactId>maven-jar-plugin</artifactId>
490 <configuration>
491 <archive>
492 <manifest>
493 <addClasspath>true</addClasspath>
494 <classpathPrefix>../../repository/</classpathPrefix>
495 <classpathLayoutType>repository</classpathLayoutType>
496 <mainClass>Main</mainClass>
497 </manifest>
498 <manifestEntries>
499 <Class-Path>.</Class-Path>
500 </manifestEntries>
501 </archive>
502 </configuration>
503 </plugin>
504 </plugins>
505</build>
506```
507
508The above plugin instructs the JAR to look for the necessary dependencies in the `lib/` relative folder. The layout of the folder is also in the _maven repository_ style.
509
510```bash
511❯ unzip -q -c $(nix-build --no-out-link runnable-jar.nix)/share/java/maven-demo-1.0.jar META-INF/MANIFEST.MF
512
513Manifest-Version: 1.0
514Archiver-Version: Plexus Archiver
515Built-By: nixbld
516Class-Path: . ../../repository/com/vdurmont/emoji-java/5.1.1/emoji-jav
517 a-5.1.1.jar ../../repository/org/json/json/20170516/json-20170516.jar
518Created-By: Apache Maven 3.6.3
519Build-Jdk: 1.8.0_265
520Main-Class: Main
521```
522
523We will modify the derivation above to add a symlink to our repository so that it's accessible to our JAR during the `installPhase`.
524
525```nix
526{
527 stdenv,
528 maven,
529 callPackage,
530 makeWrapper,
531 jre,
532}:
533let
534 # pick a repository derivation, here we will use buildMaven
535 repository = callPackage ./build-maven-repository.nix { };
536in
537stdenv.mkDerivation (finalAttrs: {
538 pname = "maven-demo";
539 version = "1.0";
540
541 src = fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
542 nativeBuildInputs = [ makeWrapper ];
543 buildInputs = [ maven ];
544
545 buildPhase = ''
546 runHook preBuild
547
548 echo "Using repository ${repository}"
549 mvn --offline -Dmaven.repo.local=${repository} package;
550
551 runHook postBuild
552 '';
553
554 installPhase = ''
555 runHook preInstall
556
557 mkdir -p $out/bin
558
559 # create a symbolic link for the repository directory
560 ln -s ${repository} $out/repository
561
562 install -Dm644 target/maven-demo-${finalAttrs.version}.jar $out/share/java
563 # create a wrapper that will automatically set the classpath
564 # this should be the paths from the dependency derivation
565 makeWrapper ${jre}/bin/java $out/bin/maven-demo \
566 --add-flags "-jar $out/share/java/maven-demo-${finalAttrs.version}.jar"
567
568 runHook postInstall
569 '';
570})
571```
572::: {.note}
573Our script produces a dependency on `jre` rather than `jdk` to restrict the runtime closure necessary to run the application.
574:::
575
576This will give you an executable shell-script that launches your JAR with all the dependencies available.
577
578```bash
579❯ tree $(nix-build --no-out-link runnable-jar.nix)
580/nix/store/8d4c3ibw8ynsn01ibhyqmc1zhzz75s26-maven-demo-1.0
581├── bin
582│ └── maven-demo
583├── repository -> /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
584└── share
585 └── java
586 └── maven-demo-1.0.jar
587
588❯ $(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo
589NixOS 😀 is super cool 😃!
590```