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