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{ lib, fetchFromGitHub, jre, makeWrapper, maven }: 13 14maven.buildMavenPackage rec { 15 pname = "jd-cli"; 16 version = "1.2.1"; 17 18 src = fetchFromGitHub { 19 owner = "intoolswetrust"; 20 repo = pname; 21 rev = "${pname}-${version}"; 22 hash = "sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q="; 23 }; 24 25 mvnHash = "sha256-kLpjMj05uC94/5vGMwMlFzLKNFOKeyNvq/vmB6pHTAo="; 26 27 nativeBuildInputs = [ makeWrapper ]; 28 29 installPhase = '' 30 mkdir -p $out/bin $out/share/jd-cli 31 install -Dm644 jd-cli/target/jd-cli.jar $out/share/jd-cli 32 33 makeWrapper ${jre}/bin/java $out/bin/jd-cli \ 34 --add-flags "-jar $out/share/jd-cli/jd-cli.jar" 35 ''; 36 37 meta = with lib; { 38 description = "Simple command line wrapper around JD Core Java Decompiler project"; 39 homepage = "https://github.com/intoolswetrust/jd-cli"; 40 license = licenses.gpl3Plus; 41 maintainers = with maintainers; [ majiir ]; 42 }; 43}: 44``` 45 46This 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. 47 48::: {.tip} 49After 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. 50::: 51 52### Stable Maven plugins {#stable-maven-plugins} 53 54Maven 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. 55 56When `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. 57 58This clearly prevents automatic upgrades of Maven: a manual effort must be made throughout nixpkgs by any maintainer wishing to push the upgrades. 59 60To 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: 61 62```xml 63<plugin> 64 <groupId>org.apache.maven.plugins</groupId> 65 <artifactId>maven-enforcer-plugin</artifactId> 66 <version>3.3.0</version> 67 <executions> 68 <execution> 69 <id>enforce-plugin-versions</id> 70 <goals> 71 <goal>enforce</goal> 72 </goals> 73 <configuration> 74 <rules> 75 <requirePluginVersions /> 76 </rules> 77 </configuration> 78 </execution> 79 </executions> 80</plugin> 81``` 82 83## Manually using `mvn2nix` {#maven-mvn2nix} 84::: {.warning} 85This way is no longer recommended; see [](#maven-buildmavenpackage) for the simpler and preferred way. 86::: 87 88For 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). 89 90```xml 91<?xml version="1.0" encoding="UTF-8"?> 92<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 93 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 94 <modelVersion>4.0.0</modelVersion> 95 <groupId>io.github.fzakaria</groupId> 96 <artifactId>maven-demo</artifactId> 97 <version>1.0</version> 98 <packaging>jar</packaging> 99 <name>NixOS Maven Demo</name> 100 101 <dependencies> 102 <dependency> 103 <groupId>com.vdurmont</groupId> 104 <artifactId>emoji-java</artifactId> 105 <version>5.1.1</version> 106 </dependency> 107 </dependencies> 108</project> 109``` 110 111Our main class file will be very simple: 112 113```java 114import com.vdurmont.emoji.EmojiParser; 115 116public class Main { 117 public static void main(String[] args) { 118 String str = "NixOS :grinning: is super cool :smiley:!"; 119 String result = EmojiParser.parseToUnicode(str); 120 System.out.println(result); 121 } 122} 123``` 124 125You find this demo project at [https://github.com/fzakaria/nixos-maven-example](https://github.com/fzakaria/nixos-maven-example). 126 127### Solving for dependencies {#solving-for-dependencies} 128 129#### buildMaven with NixOS/mvn2nix-maven-plugin {#buildmaven-with-nixosmvn2nix-maven-plugin} 130`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). 131 132First you generate a `project-info.json` file using the maven plugin. 133 134> This should be executed in the project's source repository or be told which `pom.xml` to execute with. 135 136```bash 137# run this step within the project's source repository 138❯ mvn org.nixos.mvn2nix:mvn2nix-maven-plugin:mvn2nix 139 140❯ cat project-info.json | jq | head 141{ 142 "project": { 143 "artifactId": "maven-demo", 144 "groupId": "org.nixos", 145 "version": "1.0", 146 "classifier": "", 147 "extension": "jar", 148 "dependencies": [ 149 { 150 "artifactId": "maven-resources-plugin", 151``` 152 153This file is then given to the `buildMaven` function, and it returns 2 attributes. 154 155**`repo`**: 156 A Maven repository that is a symlink farm of all the dependencies found in the `project-info.json` 157 158 159**`build`**: 160 A simple derivation that runs through `mvn compile` & `mvn package` to build the JAR. You may use this as inspiration for more complicated derivations. 161 162Here is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/build-maven-repository.nix) of building the Maven repository 163 164```nix 165{ pkgs ? import <nixpkgs> { } }: 166with pkgs; 167(buildMaven ./project-info.json).repo 168``` 169 170The 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. 171 172```bash 173❯ tree $(nix-build --no-out-link build-maven-repository.nix) | head 174/nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository 175├── antlr 176│   └── antlr 177│   └── 2.7.2 178│   ├── antlr-2.7.2.jar -> /nix/store/d027c8f2cnmj5yrynpbq2s6wmc9cb559-antlr-2.7.2.jar 179│   └── antlr-2.7.2.pom -> /nix/store/mv42fc5gizl8h5g5vpywz1nfiynmzgp2-antlr-2.7.2.pom 180├── avalon-framework 181│   └── avalon-framework 182│   └── 4.1.3 183│   ├── avalon-framework-4.1.3.jar -> /nix/store/iv5fp3955w3nq28ff9xfz86wvxbiw6n9-avalon-framework-4.1.3.jar 184``` 185 186#### Double Invocation {#double-invocation} 187::: {.note} 188This pattern is the simplest but may cause unnecessary rebuilds due to the output hash changing. 189::: 190 191The double invocation is a _simple_ way to get around the problem that `nix-build` may be sandboxed and have no Internet connectivity. 192 193It 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. 194 195The 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). 196 197::: {.note} 198Traditionally the Maven repository is at `~/.m2/repository`. We will override this to be the `$out` directory. 199::: 200 201```nix 202{ lib, stdenv, maven }: 203stdenv.mkDerivation { 204 name = "maven-repository"; 205 buildInputs = [ maven ]; 206 src = ./.; # or fetchFromGitHub, cleanSourceWith, etc 207 buildPhase = '' 208 mvn package -Dmaven.repo.local=$out 209 ''; 210 211 # keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside 212 installPhase = '' 213 find $out -type f \ 214 -name \*.lastUpdated -or \ 215 -name resolver-status.properties -or \ 216 -name _remote.repositories \ 217 -delete 218 ''; 219 220 # don't do any fixup 221 dontFixup = true; 222 outputHashAlgo = "sha256"; 223 outputHashMode = "recursive"; 224 # replace this with the correct SHA256 225 outputHash = lib.fakeSha256; 226} 227``` 228 229The 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. 230 231::: {.warning} 232Some additional files are deleted that would cause the output hash to change potentially on subsequent runs. 233::: 234 235```bash 236❯ tree $(nix-build --no-out-link double-invocation-repository.nix) | head 237/nix/store/8kicxzp98j68xyi9gl6jda67hp3c54fq-maven-repository 238├── backport-util-concurrent 239│   └── backport-util-concurrent 240│   └── 3.1 241│   ├── backport-util-concurrent-3.1.pom 242│   └── backport-util-concurrent-3.1.pom.sha1 243├── classworlds 244│   └── classworlds 245│   ├── 1.1 246│   │   ├── classworlds-1.1.jar 247``` 248 249If 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 then using `buildMaven`. 250 251### Building a JAR {#building-a-jar} 252 253Regardless of which strategy is chosen above, the step to build the derivation is the same. 254 255```nix 256{ stdenv, maven, callPackage }: 257# pick a repository derivation, here we will use buildMaven 258let repository = callPackage ./build-maven-repository.nix { }; 259in stdenv.mkDerivation rec { 260 pname = "maven-demo"; 261 version = "1.0"; 262 263 src = builtins.fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz"; 264 buildInputs = [ maven ]; 265 266 buildPhase = '' 267 echo "Using repository ${repository}" 268 mvn --offline -Dmaven.repo.local=${repository} package; 269 ''; 270 271 installPhase = '' 272 install -Dm644 target/${pname}-${version}.jar $out/share/java 273 ''; 274} 275``` 276 277::: {.tip} 278We 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. 279::: 280 281```bash 282❯ tree $(nix-build --no-out-link build-jar.nix) 283/nix/store/7jw3xdfagkc2vw8wrsdv68qpsnrxgvky-maven-demo-1.0 284└── share 285 └── java 286 └── maven-demo-1.0.jar 287 2882 directories, 1 file 289``` 290 291### Runnable JAR {#runnable-jar} 292 293The previous example builds a `jar` file but that's not a file one can run. 294 295You need to use it with `java -jar $out/share/java/output.jar` and make sure to provide the required dependencies on the classpath. 296 297The following explains how to use `makeWrapper` in order to make the derivation produce an executable that will run the JAR file you created. 298 299We will use the same repository we built above (either _double invocation_ or _buildMaven_) to setup a CLASSPATH for our JAR. 300 301The following two methods are more suited to Nix then building an [UberJar](https://imagej.net/Uber-JAR) which may be the more traditional approach. 302 303#### CLASSPATH {#classpath} 304 305This method is ideal if you are providing a derivation for _nixpkgs_ and don't want to patch the project's `pom.xml`. 306 307We 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. 308 309We make sure to provide this classpath to the `makeWrapper`. 310 311```nix 312{ stdenv, maven, callPackage, makeWrapper, jre }: 313let 314 repository = callPackage ./build-maven-repository.nix { }; 315in stdenv.mkDerivation rec { 316 pname = "maven-demo"; 317 version = "1.0"; 318 319 src = builtins.fetchTarball 320 "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz"; 321 nativeBuildInputs = [ makeWrapper ]; 322 buildInputs = [ maven ]; 323 324 buildPhase = '' 325 echo "Using repository ${repository}" 326 mvn --offline -Dmaven.repo.local=${repository} package; 327 ''; 328 329 installPhase = '' 330 mkdir -p $out/bin 331 332 classpath=$(find ${repository} -name "*.jar" -printf ':%h/%f'); 333 install -Dm644 target/${pname}-${version}.jar $out/share/java 334 # create a wrapper that will automatically set the classpath 335 # this should be the paths from the dependency derivation 336 makeWrapper ${jre}/bin/java $out/bin/${pname} \ 337 --add-flags "-classpath $out/share/java/${pname}-${version}.jar:''${classpath#:}" \ 338 --add-flags "Main" 339 ''; 340} 341``` 342 343#### MANIFEST file via Maven Plugin {#manifest-file-via-maven-plugin} 344 345This method is ideal if you are the project owner and want to change your `pom.xml` to set the CLASSPATH within it. 346 347Augment the `pom.xml` to create a JAR with the following manifest: 348 349```xml 350<build> 351 <plugins> 352 <plugin> 353 <artifactId>maven-jar-plugin</artifactId> 354 <configuration> 355 <archive> 356 <manifest> 357 <addClasspath>true</addClasspath> 358 <classpathPrefix>../../repository/</classpathPrefix> 359 <classpathLayoutType>repository</classpathLayoutType> 360 <mainClass>Main</mainClass> 361 </manifest> 362 <manifestEntries> 363 <Class-Path>.</Class-Path> 364 </manifestEntries> 365 </archive> 366 </configuration> 367 </plugin> 368 </plugins> 369</build> 370``` 371 372The 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. 373 374```bash 375❯ unzip -q -c $(nix-build --no-out-link runnable-jar.nix)/share/java/maven-demo-1.0.jar META-INF/MANIFEST.MF 376 377Manifest-Version: 1.0 378Archiver-Version: Plexus Archiver 379Built-By: nixbld 380Class-Path: . ../../repository/com/vdurmont/emoji-java/5.1.1/emoji-jav 381 a-5.1.1.jar ../../repository/org/json/json/20170516/json-20170516.jar 382Created-By: Apache Maven 3.6.3 383Build-Jdk: 1.8.0_265 384Main-Class: Main 385``` 386 387We will modify the derivation above to add a symlink to our repository so that it's accessible to our JAR during the `installPhase`. 388 389```nix 390{ stdenv, maven, callPackage, makeWrapper, jre }: 391# pick a repository derivation, here we will use buildMaven 392let repository = callPackage ./build-maven-repository.nix { }; 393in stdenv.mkDerivation rec { 394 pname = "maven-demo"; 395 version = "1.0"; 396 397 src = builtins.fetchTarball 398 "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz"; 399 nativeBuildInputs = [ makeWrapper ]; 400 buildInputs = [ maven ]; 401 402 buildPhase = '' 403 echo "Using repository ${repository}" 404 mvn --offline -Dmaven.repo.local=${repository} package; 405 ''; 406 407 installPhase = '' 408 mkdir -p $out/bin 409 410 # create a symbolic link for the repository directory 411 ln -s ${repository} $out/repository 412 413 install -Dm644 target/${pname}-${version}.jar $out/share/java 414 # create a wrapper that will automatically set the classpath 415 # this should be the paths from the dependency derivation 416 makeWrapper ${jre}/bin/java $out/bin/${pname} \ 417 --add-flags "-jar $out/share/java/${pname}-${version}.jar" 418 ''; 419} 420``` 421::: {.note} 422Our script produces a dependency on `jre` rather than `jdk` to restrict the runtime closure necessary to run the application. 423::: 424 425This will give you an executable shell-script that launches your JAR with all the dependencies available. 426 427```bash 428❯ tree $(nix-build --no-out-link runnable-jar.nix) 429/nix/store/8d4c3ibw8ynsn01ibhyqmc1zhzz75s26-maven-demo-1.0 430├── bin 431│   └── maven-demo 432├── repository -> /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository 433└── share 434 └── java 435 └── maven-demo-1.0.jar 436 437$(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo 438NixOS 😀 is super cool 😃! 439```