vuls: init at 0.27.0
[NixPkgs.git] / doc / languages-frameworks / maven.section.md
blobd6c8357785e35a1e3e019d9148abe43e1569e08d
1 # Maven {#maven}
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:
11 ```nix
12 { lib, fetchFromGitHub, jre, makeWrapper, maven }:
14 maven.buildMavenPackage rec {
15   pname = "jd-cli";
16   version = "1.2.1";
18   src = fetchFromGitHub {
19     owner = "intoolswetrust";
20     repo = pname;
21     rev = "${pname}-${version}";
22     hash = "sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q=";
23   };
25   mvnHash = "sha256-kLpjMj05uC94/5vGMwMlFzLKNFOKeyNvq/vmB6pHTAo=";
27   nativeBuildInputs = [ makeWrapper ];
29   installPhase = ''
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"
35   '';
37   meta = {
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 ];
42   };
44 ```
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.
48 ::: {.tip}
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.
50 :::
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:
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 ```
83 ## Manually using `mvn2nix` {#maven-mvn2nix}
84 ::: {.warning}
85 This way is no longer recommended; see [](#maven-buildmavenpackage) for the simpler and preferred way.
86 :::
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).
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>
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>
111 Our main class file will be very simple:
113 ```java
114 import com.vdurmont.emoji.EmojiParser;
116 public 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   }
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.
136 ```bash
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
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",
153 This file is then given to the `buildMaven` function, and it returns 2 attributes.
155 **`repo`**:
156     A Maven repository that is a symlink farm of all the dependencies found in the `project-info.json`
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.
162 Here is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/build-maven-repository.nix) of building the Maven repository
164 ```nix
165 { pkgs ? import <nixpkgs> { } }:
166 with pkgs;
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.
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
186 #### Double Invocation {#double-invocation}
187 ::: {.note}
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).
197 ::: {.note}
198 Traditionally the Maven repository is at `~/.m2/repository`. We will override this to be the `$out` directory.
201 ```nix
202 { lib, stdenv, maven }:
203 stdenv.mkDerivation {
204   name = "maven-repository";
205   buildInputs = [ maven ];
206   src = ./.; # or fetchFromGitHub, cleanSourceWith, etc
207   buildPhase = ''
208     mvn package -Dmaven.repo.local=$out
209   '';
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   '';
220   # don't do any fixup
221   dontFixup = true;
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.
231 ::: {.warning}
232 Some additional files are deleted that would cause the output hash to change potentially on subsequent runs.
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
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.
255 ```nix
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";
261   version = "1.0";
263   src = builtins.fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
264   buildInputs = [ maven ];
266   buildPhase = ''
267     echo "Using repository ${repository}"
268     mvn --offline -Dmaven.repo.local=${repository} package;
269   '';
271   installPhase = ''
272     install -Dm644 target/${pname}-${version}.jar $out/share/java
273   '';
277 ::: {.tip}
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.
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
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`.
311 ```nix
312 { stdenv, maven, callPackage, makeWrapper, jre }:
314   repository = callPackage ./build-maven-repository.nix { };
315 in stdenv.mkDerivation rec {
316   pname = "maven-demo";
317   version = "1.0";
319   src = builtins.fetchTarball
320     "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
321   nativeBuildInputs = [ makeWrapper ];
322   buildInputs = [ maven ];
324   buildPhase = ''
325     echo "Using repository ${repository}"
326     mvn --offline -Dmaven.repo.local=${repository} package;
327   '';
329   installPhase = ''
330     mkdir -p $out/bin
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   '';
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:
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>
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.
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
377 Manifest-Version: 1.0
378 Archiver-Version: Plexus Archiver
379 Built-By: nixbld
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
383 Build-Jdk: 1.8.0_265
384 Main-Class: Main
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`.
389 ```nix
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";
395   version = "1.0";
397   src = builtins.fetchTarball
398     "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
399   nativeBuildInputs = [ makeWrapper ];
400   buildInputs = [ maven ];
402   buildPhase = ''
403     echo "Using repository ${repository}"
404     mvn --offline -Dmaven.repo.local=${repository} package;
405   '';
407   installPhase = ''
408     mkdir -p $out/bin
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"
418   '';
421 ::: {.note}
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.
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
437 ❯ $(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo
438 NixOS 😀 is super cool 😃!