3 Maven is a well-known build tool for the Java ecosystem however it has some challenges when integrating into the Nix build system.
5 The 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.
7 ## Building a package using `maven.buildMavenPackage` {#maven-buildmavenpackage}
9 Consider the following package:
12 { lib, fetchFromGitHub, jre, makeWrapper, maven }:
14 maven.buildMavenPackage rec {
18 src = fetchFromGitHub {
19 owner = "intoolswetrust";
21 rev = "${pname}-${version}";
22 hash = "sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q=";
25 mvnHash = "sha256-kLpjMj05uC94/5vGMwMlFzLKNFOKeyNvq/vmB6pHTAo=";
27 nativeBuildInputs = [ makeWrapper ];
30 mkdir -p $out/bin $out/share/jd-cli
31 install -Dm644 jd-cli/target/jd-cli.jar $out/share/jd-cli
33 makeWrapper ${jre}/bin/java $out/bin/jd-cli \
34 --add-flags "-jar $out/share/jd-cli/jd-cli.jar"
38 description = "Simple command line wrapper around JD Core Java Decompiler project";
39 homepage = "https://github.com/intoolswetrust/jd-cli";
40 license = lib.licenses.gpl3Plus;
41 maintainers = with lib.maintainers; [ majiir ];
46 This 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.
49 After 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.
52 ### Stable Maven plugins {#stable-maven-plugins}
54 Maven 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.
56 When `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.
58 This clearly prevents automatic upgrades of Maven: a manual effort must be made throughout nixpkgs by any maintainer wishing to push the upgrades.
60 To 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:
64 <groupId>org.apache.maven.plugins</groupId>
65 <artifactId>maven-enforcer-plugin</artifactId>
66 <version>3.3.0</version>
69 <id>enforce-plugin-versions</id>
75 <requirePluginVersions />
83 ## Manually using `mvn2nix` {#maven-mvn2nix}
85 This way is no longer recommended; see [](#maven-buildmavenpackage) for the simpler and preferred way.
88 For 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).
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>
103 <groupId>com.vdurmont</groupId>
104 <artifactId>emoji-java</artifactId>
105 <version>5.1.1</version>
111 Our main class file will be very simple:
114 import com.vdurmont.emoji.EmojiParser;
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);
125 You find this demo project at [https://github.com/fzakaria/nixos-maven-example](https://github.com/fzakaria/nixos-maven-example).
127 ### Solving for dependencies {#solving-for-dependencies}
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).
132 First you generate a `project-info.json` file using the maven plugin.
134 > This should be executed in the project's source repository or be told which `pom.xml` to execute with.
137 # run this step within the project's source repository
138 ❯ mvn org.nixos.mvn2nix:mvn2nix-maven-plugin:mvn2nix
140 ❯ cat project-info.json | jq | head
143 "artifactId": "maven-demo",
144 "groupId": "org.nixos",
150 "artifactId": "maven-resources-plugin",
153 This file is then given to the `buildMaven` function, and it returns 2 attributes.
156 A Maven repository that is a symlink farm of all the dependencies found in the `project-info.json`
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.
162 Here is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/build-maven-repository.nix) of building the Maven repository
165 { pkgs ? import <nixpkgs> { } }:
167 (buildMaven ./project-info.json).repo
170 The 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.
173 ❯ tree $(nix-build --no-out-link build-maven-repository.nix) | head
174 /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
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
181 │ └── avalon-framework
183 │ ├── avalon-framework-4.1.3.jar -> /nix/store/iv5fp3955w3nq28ff9xfz86wvxbiw6n9-avalon-framework-4.1.3.jar
186 #### Double Invocation {#double-invocation}
188 This pattern is the simplest but may cause unnecessary rebuilds due to the output hash changing.
191 The double invocation is a _simple_ way to get around the problem that `nix-build` may be sandboxed and have no Internet connectivity.
193 It 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.
195 The 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).
198 Traditionally the Maven repository is at `~/.m2/repository`. We will override this to be the `$out` directory.
202 { lib, stdenv, maven }:
203 stdenv.mkDerivation {
204 name = "maven-repository";
205 buildInputs = [ maven ];
206 src = ./.; # or fetchFromGitHub, cleanSourceWith, etc
208 mvn package -Dmaven.repo.local=$out
211 # keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside
214 -name \*.lastUpdated -or \
215 -name resolver-status.properties -or \
216 -name _remote.repositories \
222 outputHashAlgo = null;
223 outputHashMode = "recursive";
224 # replace this with the correct SHA256
225 outputHash = lib.fakeHash;
229 The 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.
232 Some additional files are deleted that would cause the output hash to change potentially on subsequent runs.
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
241 │ ├── backport-util-concurrent-3.1.pom
242 │ └── backport-util-concurrent-3.1.pom.sha1
246 │ │ ├── classworlds-1.1.jar
249 If 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`.
251 ### Building a JAR {#building-a-jar}
253 Regardless of which strategy is chosen above, the step to build the derivation is the same.
256 { stdenv, maven, callPackage }:
257 # pick a repository derivation, here we will use buildMaven
258 let repository = callPackage ./build-maven-repository.nix { };
259 in stdenv.mkDerivation rec {
260 pname = "maven-demo";
263 src = builtins.fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
264 buildInputs = [ maven ];
267 echo "Using repository ${repository}"
268 mvn --offline -Dmaven.repo.local=${repository} package;
272 install -Dm644 target/${pname}-${version}.jar $out/share/java
278 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.
282 ❯ tree $(nix-build --no-out-link build-jar.nix)
283 /nix/store/7jw3xdfagkc2vw8wrsdv68qpsnrxgvky-maven-demo-1.0
286 └── maven-demo-1.0.jar
288 2 directories, 1 file
291 ### Runnable JAR {#runnable-jar}
293 The previous example builds a `jar` file but that's not a file one can run.
295 You need to use it with `java -jar $out/share/java/output.jar` and make sure to provide the required dependencies on the classpath.
297 The following explains how to use `makeWrapper` in order to make the derivation produce an executable that will run the JAR file you created.
299 We will use the same repository we built above (either _double invocation_ or _buildMaven_) to setup a CLASSPATH for our JAR.
301 The following two methods are more suited to Nix then building an [UberJar](https://imagej.net/Uber-JAR) which may be the more traditional approach.
303 #### CLASSPATH {#classpath}
305 This method is ideal if you are providing a derivation for _nixpkgs_ and don't want to patch the project's `pom.xml`.
307 We 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.
309 We make sure to provide this classpath to the `makeWrapper`.
312 { stdenv, maven, callPackage, makeWrapper, jre }:
314 repository = callPackage ./build-maven-repository.nix { };
315 in stdenv.mkDerivation rec {
316 pname = "maven-demo";
319 src = builtins.fetchTarball
320 "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
321 nativeBuildInputs = [ makeWrapper ];
322 buildInputs = [ maven ];
325 echo "Using repository ${repository}"
326 mvn --offline -Dmaven.repo.local=${repository} package;
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#:}" \
343 #### MANIFEST file via Maven Plugin {#manifest-file-via-maven-plugin}
345 This method is ideal if you are the project owner and want to change your `pom.xml` to set the CLASSPATH within it.
347 Augment the `pom.xml` to create a JAR with the following manifest:
353 <artifactId>maven-jar-plugin</artifactId>
357 <addClasspath>true</addClasspath>
358 <classpathPrefix>../../repository/</classpathPrefix>
359 <classpathLayoutType>repository</classpathLayoutType>
360 <mainClass>Main</mainClass>
363 <Class-Path>.</Class-Path>
372 The 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.
375 ❯ unzip -q -c $(nix-build --no-out-link runnable-jar.nix)/share/java/maven-demo-1.0.jar META-INF/MANIFEST.MF
377 Manifest-Version: 1.0
378 Archiver-Version: Plexus Archiver
380 Class-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
382 Created-By: Apache Maven 3.6.3
387 We will modify the derivation above to add a symlink to our repository so that it's accessible to our JAR during the `installPhase`.
390 { stdenv, maven, callPackage, makeWrapper, jre }:
391 # pick a repository derivation, here we will use buildMaven
392 let repository = callPackage ./build-maven-repository.nix { };
393 in stdenv.mkDerivation rec {
394 pname = "maven-demo";
397 src = builtins.fetchTarball
398 "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
399 nativeBuildInputs = [ makeWrapper ];
400 buildInputs = [ maven ];
403 echo "Using repository ${repository}"
404 mvn --offline -Dmaven.repo.local=${repository} package;
410 # create a symbolic link for the repository directory
411 ln -s ${repository} $out/repository
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"
422 Our script produces a dependency on `jre` rather than `jdk` to restrict the runtime closure necessary to run the application.
425 This will give you an executable shell-script that launches your JAR with all the dependencies available.
428 ❯ tree $(nix-build --no-out-link runnable-jar.nix)
429 /nix/store/8d4c3ibw8ynsn01ibhyqmc1zhzz75s26-maven-demo-1.0
432 ├── repository -> /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
435 └── maven-demo-1.0.jar
437 ❯ $(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo
438 NixOS 😀 is super cool 😃!