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