python310Packages.pydeconz: 104 -> 105
[NixPkgs.git] / doc / languages-frameworks / maven.section.md
blobcc5b4e3ed799be5bd14be75e86ddf7e563377467
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 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).
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>
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 ```
30 Our main class file will be very simple:
32 ```java
33 import com.vdurmont.emoji.EmojiParser;
35 public 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   }
42 ```
44 You find this demo project at https://github.com/fzakaria/nixos-maven-example
46 ## Solving for dependencies {#solving-for-dependencies}
48 ### buildMaven with NixOS/mvn2nix-maven-plugin {#buildmaven-with-nixosmvn2nix-maven-plugin}
50 > ⚠️ Although `buildMaven` is the "blessed" way within nixpkgs, as of 2020, it hasn't seen much activity in quite a while.
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).
54 First you generate a `project-info.json` file using the maven plugin.
56 > This should be executed in the project's source repository or be told which `pom.xml` to execute with.
58 ```bash
59 # run this step within the project's source repository
60 ❯ mvn org.nixos.mvn2nix:mvn2nix-maven-plugin:mvn2nix
62 ❯ cat project-info.json | jq | head
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 ```
75 This file is then given to the `buildMaven` function, and it returns 2 attributes.
77 **`repo`**:
78     A Maven repository that is a symlink farm of all the dependencies found in the `project-info.json`
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.
84 Here is an [example](https://github.com/fzakaria/nixos-maven-example/blob/main/build-maven-repository.nix) of building the Maven repository
86 ```nix
87 { pkgs ? import <nixpkgs> { } }:
88 with pkgs;
89 (buildMaven ./project-info.json).repo
90 ```
92 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.
94 ```bash
95 ❯ tree $(nix-build --no-out-link build-maven-repository.nix) | head
96 /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
97 ├── antlr
98 │   └── antlr
99 │       └── 2.7.2
100 │           ├── antlr-2.7.2.jar -> /nix/store/d027c8f2cnmj5yrynpbq2s6wmc9cb559-antlr-2.7.2.jar
101 │           └── antlr-2.7.2.pom -> /nix/store/mv42fc5gizl8h5g5vpywz1nfiynmzgp2-antlr-2.7.2.pom
102 ├── avalon-framework
103 │   └── avalon-framework
104 │       └── 4.1.3
105 │           ├── avalon-framework-4.1.3.jar -> /nix/store/iv5fp3955w3nq28ff9xfz86wvxbiw6n9-avalon-framework-4.1.3.jar
108 ### Double Invocation {#double-invocation}
110 > ⚠️ This pattern is the simplest but may cause unnecessary rebuilds due to the output hash changing.
112 The double invocation is a _simple_ way to get around the problem that `nix-build` may be sandboxed and have no Internet connectivity.
114 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.
116 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).
118 > Traditionally the Maven repository is at `~/.m2/repository`. We will override this to be the `$out` directory.
120 ```nix
121 { lib, stdenv, maven }:
122 stdenv.mkDerivation {
123   name = "maven-repository";
124   buildInputs = [ maven ];
125   src = ./.; # or fetchFromGitHub, cleanSourceWith, etc
126   buildPhase = ''
127     mvn package -Dmaven.repo.local=$out
128   '';
130   # keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside
131   installPhase = ''
132     find $out -type f \
133       -name \*.lastUpdated -or \
134       -name resolver-status.properties -or \
135       -name _remote.repositories \
136       -delete
137   '';
139   # don't do any fixup
140   dontFixup = true;
141   outputHashAlgo = "sha256";
142   outputHashMode = "recursive";
143   # replace this with the correct SHA256
144   outputHash = lib.fakeSha256;
148 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.
150 > Some additional files are deleted that would cause the output hash to change potentially on subsequent runs.
152 ```bash
153 ❯ tree $(nix-build --no-out-link double-invocation-repository.nix) | head
154 /nix/store/8kicxzp98j68xyi9gl6jda67hp3c54fq-maven-repository
155 ├── backport-util-concurrent
156 │   └── backport-util-concurrent
157 │       └── 3.1
158 │           ├── backport-util-concurrent-3.1.pom
159 │           └── backport-util-concurrent-3.1.pom.sha1
160 ├── classworlds
161 │   └── classworlds
162 │       ├── 1.1
163 │       │   ├── classworlds-1.1.jar
166 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`.
168 ## Building a JAR {#building-a-jar}
170 Regardless of which strategy is chosen above, the step to build the derivation is the same.
172 ```nix
173 { stdenv, maven, callPackage }:
174 # pick a repository derivation, here we will use buildMaven
175 let repository = callPackage ./build-maven-repository.nix { };
176 in stdenv.mkDerivation rec {
177   pname = "maven-demo";
178   version = "1.0";
180   src = builtins.fetchTarball "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
181   buildInputs = [ maven ];
183   buildPhase = ''
184     echo "Using repository ${repository}"
185     mvn --offline -Dmaven.repo.local=${repository} package;
186   '';
188   installPhase = ''
189     install -Dm644 target/${pname}-${version}.jar $out/share/java
190   '';
194 > 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.
196 ```bash
197 ❯ tree $(nix-build --no-out-link build-jar.nix)
198 /nix/store/7jw3xdfagkc2vw8wrsdv68qpsnrxgvky-maven-demo-1.0
199 └── share
200     └── java
201         └── maven-demo-1.0.jar
203 2 directories, 1 file
206 ## Runnable JAR {#runnable-jar}
208 The previous example builds a `jar` file but that's not a file one can run.
210 You need to use it with `java -jar $out/share/java/output.jar` and make sure to provide the required dependencies on the classpath.
212 The following explains how to use `makeWrapper` in order to make the derivation produce an executable that will run the JAR file you created.
214 We will use the same repository we built above (either _double invocation_ or _buildMaven_) to setup a CLASSPATH for our JAR.
216 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.
218 ### CLASSPATH {#classpath}
220 > This is ideal if you are providing a derivation for _nixpkgs_ and don't want to patch the project's `pom.xml`.
222 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.
224 We make sure to provide this classpath to the `makeWrapper`.
226 ```nix
227 { stdenv, maven, callPackage, makeWrapper, jre }:
229   repository = callPackage ./build-maven-repository.nix { };
230 in stdenv.mkDerivation rec {
231   pname = "maven-demo";
232   version = "1.0";
234   src = builtins.fetchTarball
235     "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
236   nativeBuildInputs = [ makeWrapper ];
237   buildInputs = [ maven ];
239   buildPhase = ''
240     echo "Using repository ${repository}"
241     mvn --offline -Dmaven.repo.local=${repository} package;
242   '';
244   installPhase = ''
245     mkdir -p $out/bin
247     classpath=$(find ${repository} -name "*.jar" -printf ':%h/%f');
248     install -Dm644 target/${pname}-${version}.jar $out/share/java
249     # create a wrapper that will automatically set the classpath
250     # this should be the paths from the dependency derivation
251     makeWrapper ${jre}/bin/java $out/bin/${pname} \
252           --add-flags "-classpath $out/share/java/${pname}-${version}.jar:''${classpath#:}" \
253           --add-flags "Main"
254   '';
258 ### MANIFEST file via Maven Plugin {#manifest-file-via-maven-plugin}
260 > This is ideal if you are the project owner and want to change your `pom.xml` to set the CLASSPATH within it.
262 Augment the `pom.xml` to create a JAR with the following manifest:
264 ```xml
265 <build>
266   <plugins>
267     <plugin>
268         <artifactId>maven-jar-plugin</artifactId>
269         <configuration>
270             <archive>
271                 <manifest>
272                     <addClasspath>true</addClasspath>
273                     <classpathPrefix>../../repository/</classpathPrefix>
274                     <classpathLayoutType>repository</classpathLayoutType>
275                     <mainClass>Main</mainClass>
276                 </manifest>
277                 <manifestEntries>
278                     <Class-Path>.</Class-Path>
279                 </manifestEntries>
280             </archive>
281         </configuration>
282     </plugin>
283   </plugins>
284 </build>
287 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.
289 ```bash
290 ❯ unzip -q -c $(nix-build --no-out-link runnable-jar.nix)/share/java/maven-demo-1.0.jar META-INF/MANIFEST.MF
292 Manifest-Version: 1.0
293 Archiver-Version: Plexus Archiver
294 Built-By: nixbld
295 Class-Path: . ../../repository/com/vdurmont/emoji-java/5.1.1/emoji-jav
296  a-5.1.1.jar ../../repository/org/json/json/20170516/json-20170516.jar
297 Created-By: Apache Maven 3.6.3
298 Build-Jdk: 1.8.0_265
299 Main-Class: Main
302 We will modify the derivation above to add a symlink to our repository so that it's accessible to our JAR during the `installPhase`.
304 ```nix
305 { stdenv, maven, callPackage, makeWrapper, jre }:
306 # pick a repository derivation, here we will use buildMaven
307 let repository = callPackage ./build-maven-repository.nix { };
308 in stdenv.mkDerivation rec {
309   pname = "maven-demo";
310   version = "1.0";
312   src = builtins.fetchTarball
313     "https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz";
314   nativeBuildInputs = [ makeWrapper ];
315   buildInputs = [ maven ];
317   buildPhase = ''
318     echo "Using repository ${repository}"
319     mvn --offline -Dmaven.repo.local=${repository} package;
320   '';
322   installPhase = ''
323     mkdir -p $out/bin
325     # create a symbolic link for the repository directory
326     ln -s ${repository} $out/repository
328     install -Dm644 target/${pname}-${version}.jar $out/share/java
329     # create a wrapper that will automatically set the classpath
330     # this should be the paths from the dependency derivation
331     makeWrapper ${jre}/bin/java $out/bin/${pname} \
332           --add-flags "-jar $out/share/java/${pname}-${version}.jar"
333   '';
337 > Our script produces a dependency on `jre` rather than `jdk` to restrict the runtime closure necessary to run the application.
339 This will give you an executable shell-script that launches your JAR with all the dependencies available.
341 ```bash
342 ❯ tree $(nix-build --no-out-link runnable-jar.nix)
343 /nix/store/8d4c3ibw8ynsn01ibhyqmc1zhzz75s26-maven-demo-1.0
344 ├── bin
345 │   └── maven-demo
346 ├── repository -> /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository
347 └── share
348     └── java
349         └── maven-demo-1.0.jar
351 ❯ $(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo
352 NixOS 😀 is super cool 😃!