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```