nixpkgs 中的 Maven 打包实战:buildMavenPackage、离线构建、可执行 JAR 与 Maven 4 实践 📅 发布时间:2026/9/17 15:30:07 👁 浏览次数: nixpkgs 中的 Maven 打包实战buildMavenPackage、离线构建、可执行 JAR 与 Maven 4 实践【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgsMaven 是 Java 生态中事实标准的构建工具但它依赖远程仓库动态解析依赖的特性与 Nix 构建系统的沙盒化、可复现构建存在天然冲突。本文基于 nixpkgs 官方手册中的 Maven 章节系统讲解在 nixpkgs 中将 Maven 项目或任何可导出为 Maven 坐标的 JVM 语言项目打包为 Nix 包的推荐方案与备选方案包括核心的maven.buildMavenPackage助手、overrideMavenAttrs属性覆写、buildOffline离线构建优化、Maven 4 的maven_4包以及已不推荐但仍广泛存量的mvn2nix/buildMaven、双调用Double Invocation仓库固定模式最后给出将构建产物 JAR 变为可直接执行的可执行文件的 CLASSPATH 与 MANIFEST 两种做法。为什么 Maven 项目难以直接融入 Nix 构建Maven 的标准工作流是构建时在线访问中央仓库按pom.xml声明的依赖坐标groupId/artifactId/version动态解析并下载 jar 与 pom 文件填充本地仓库传统位置为~/.m2/repository。而 Nix 的nix-build默认在沙盒中运行、没有互联网连接且要求构建输出可复现output hash 稳定。因此在 nixpkgs 中打包 Maven 项目核心问题只有一个如何把“Maven 仓库里到底有哪些依赖、哪些文件”变成构建的一个确定性输入。官方文档给出的整体答案分两代现代推荐路径maven.buildMavenPackage把整个 Maven 依赖仓库作为一个固定输出派生fixed-output derivation来下载并用一个mvnHash锁定依赖集合历史路径buildMaven基于mvn2nix-maven-plugin生成 lock 文件与 Double Invocation整个仓库当单一源下载。从顶层属性定义看maven属性集是这些功能的统一入口pkgs/top-level/all-packages.nix 中可见maven3 maven;以及inherit (maven) buildMaven;即buildMaven同样由maven属性集导出同时 pkgs/top-level/java-packages.nix 中保留了mavenfod的弃用声明throw mavenfod is renamed to/replaced by maven.buildMavenPackage印证了buildMavenPackage取代旧入口的演进方向。首选方案maven.buildMavenPackage以jd-cli一个 JD Core Java 反编译器的命令行封装为例完整的推荐打包写法如下{ lib, fetchFromGitHub, jre, makeWrapper, maven, }: maven.buildMavenPackage (finalAttrs: { pname jd-cli; version 1.2.1; src fetchFromGitHub { owner intoolswetrust; repo jd-cli; tag jd-cli-${finalAttrs.version}; hash sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q; }; mvnHash sha256-kLpjMj05uC94/5vGMwMlFzLKNFOKeyNvq/vmB6pHTAo; nativeBuildInputs [ makeWrapper ]; installPhase runHook preInstall mkdir -p $out/bin $out/share/jd-cli install -Dm644 jd-cli/target/jd-cli.jar $out/share/jd-cli makeWrapper ${jre}/bin/java $out/bin/jd-cli \ --add-flags -jar $out/share/jd-cli/jd-cli.jar runHook postInstall ; meta { description Simple command line wrapper around JD Core Java Decompiler project; homepage https://github.com/intoolswetrust/jd-cli; license lib.licenses.gpl3Plus; maintainers with lib.maintainers; [ majiir ]; }; })这个包通过maven.buildMavenPackage完成大部分工作。与stdenv.mkDerivation相比它的关键差异在于mvnHash属性它是整个 Maven 依赖集合即固定输出派生下载下来的$out/.m2仓库的哈希。Nix 用mvnHash保证只要项目依赖树不变依赖下载结果就不变依赖一旦变化升级版本、换插件mvnHash失配会立刻暴露迫使维护者显式更新这正是 Maven 动态依赖问题被收敛为静态输入的方式。文档中同时给出了一条通用建议设置好buildMavenPackage之后安装环节遵循标准的 Java.jar安装惯例——把.jar放到$out/share/java示例中按项目需要放到了$out/share/jd-cli再用makeWrapper包装一个可执行入口。关于 Java 应用的通用打包信息可参考同目录的 Java 语言章节。用overrideMavenAttrs覆写包属性buildMavenPackage的返回对象携带一个overrideMavenAttrs属性其签名为overrideMavenAttrs :: (AttrSet - Derivation) | ((AttrSet - Attrset) - Derivation) - Derivation它接受以下两种形式的参数之一并基于“旧参数 新参数合并”的结果返回新的 derivation任意一个buildMavenPackage允许传入属性的子集AttrSet一个函数形如(old: ...)old是上一次buildMavenPackage调用所用的参数按惯例命名函数返回一个可传给buildMavenPackage的属性集。它的语义类似于普通的overrideAttrs但有一个重要区别不允许访问传给buildMavenPackage的参数的最终值即拿不到finalAttrs形式的全量结果。overrideMavenAttrs实战示例官方示例演示了如何构建jd-cli的 1.2.0 旧版本并禁用若干不稳定的测试jd-cli.overrideMavenAttrs (old: rec { version 1.2.0; src fetchFromGitHub { owner old.src.owner; repo old.src.repo; tag ${old.pname}-${version}; # old source hash of 1.2.0 version hash sha256-US7j6tQ6mh1libeHnQdFxPGoxHzbZHqehWSgCYynKx8; }; # tests can be disabled by prefixing it with ! # see Maven documentation for more details: # https://maven.apache.org/surefire/maven-surefire-plugin/examples/single-test.html#Multiple_Formats_in_One mvnParameters lib.escapeShellArgs [ -Dsurefire.failIfNoSpecifiedTestsfalse -Dtest!JavaDecompilerTest#basicTest,!JavaDecompilerTest#patternMatchingTest ]; # old mvnHash of 1.2.0 maven dependencies mvnHash sha256-N9XC1pg6Y4sUiBWIQUf16QSXCuiAPpXEHGlgApviF4I; })这个示例有三个可复用的技巧old.src.owner/old.src.repo让覆写尽量小只改必须改的字段surefire 插件支持用-Dtest!TestClass#method,!...语法按类/方法粒度禁用 flaky 测试-Dsurefire.failIfNoSpecifiedTestsfalse则避免“指定了测试但某模块没有该测试”时构建直接失败换版本意味着依赖树变化mvnHash必须一并换成对应旧版本的值否则固定输出派生会哈希失配。离线构建buildOffline true默认情况下buildMavenPackage的执行流程是两步在依赖的固定输出派生中运行mvn package -Dmaven.repo.local$out/.m2 ${mvnParameters}在主 derivation 中再次运行mvn package -o -nsu -Dmaven.repo.local$mvnDeps/.m2 ${mvnParameters}。也就是说测试会被执行两次第一次在下载依赖的固定输出派生里因为mvn package本身就包含测试阶段第二次在主构建里。这带来两个代价一是双倍测试耗时二是任何一次测试失败都会触发固定输出派生的重新实化re-realise进而把全部依赖重新下载一遍。对于大型 Maven 项目这会造成很长的调试反馈循环。设置buildOffline true后行为变为在固定输出派生中运行mvn de.qaware.maven:go-offline-maven-plugin:1.2.8:resolve-dependencies -Dmaven.repo.local$out/.m2 ${mvnDepsParameters}在主 derivation 中运行mvn package -o -nsu -Dmaven.repo.local$mvnDeps/.m2 ${mvnParameters}。此时所有依赖在步骤 1 一次性下载测试只在步骤 2 执行一次。测试失败只会触发步骤 2 的重建因为步骤 1 的依赖产物未变化、可以直接复用——反馈循环显著缩短。注意动态测试依赖需要手动补充go-offline 插件无法处理所谓“动态依赖”dynamic dependencies测试依赖不会在步骤 1 被下载因此在步骤 2 中很可能因缺失这些依赖而构建失败。此时必须用manualMvnArtifacts手动列出这些坐标maven.buildMavenPackage { manualMvnArtifacts [ # add dynamic test dependencies here org.apache.maven.surefire:surefire-junit-platform:3.1.2 org.junit.platform:junit-platform-launcher:1.10.0 ]; }稳定 Maven 插件版本避免升级 Maven 时的连锁手工劳动Maven 为其核心插件定义了默认版本例如maven-compiler-plugin。如果你的项目没有显式声明这些插件版本那么升级 Maven 就会改变实际使用的插件版本进而改变构建产物与mvnHash。这带来一个维护上的连锁反应每当maven包升级所有受影响包的mvnHash都要人工更新否则构建会基于旧插件的依赖派生进行、因找不到请求的插件而失败。这实际上阻止了 Maven 的自动升级——任何想要推进升级的维护者都要在整个 nixpkgs 中做一遍手工劳动。文档给出的建议是让包显式声明所有插件版本。可以在父POM 中加入maven-enforcer-plugin的requirePluginVersions规则来强制校验plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId version3.3.0/version executions execution idenforce-plugin-versions/id goals goalenforce/goal /goals configuration rules requirePluginVersions / /rules /configuration /execution /executions /plugin一旦有插件漏声明版本enforcer 会在构建期直接报错把问题提前到上游暴露。Maven 4maven_4包除了默认的maven包最新的 Maven 3 发行版nixpkgs 还提供了一个maven_4包封装 Maven 4 发行线。maven_4是一个独立的 derivation可以在任何使用maven的地方作为直接替代品drop-in replacement。例如用 Maven 4 构建同一个jd-cli{ lib, fetchFromGitHub, jre, makeWrapper, maven_4, }: maven_4.buildMavenPackage (finalAttrs: { pname jd-cli; version 1.2.1; src fetchFromGitHub { owner intoolswetrust; repo jd-cli; tag jd-cli-${finalAttrs.version}; hash sha256-rRttA5H0A0c44loBzbKH7Waoted3IsOgxGCD2VM0U/Q; }; mvnHash ; nativeBuildInputs [ makeWrapper ]; installPhase runHook preInstall mkdir -p $out/bin $out/share/jd-cli install -Dm644 jd-cli/target/jd-cli.jar $out/share/jd-cli makeWrapper ${jre}/bin/java $out/bin/jd-cli \ --add-flags -jar $out/share/jd-cli/jd-cli.jar runHook postInstall ; meta { description Simple command line wrapper around JD Core Java Decompiler project; homepage https://github.com/intoolswetrust/jd-cli; license lib.licenses.gpl3Plus; maintainers with lib.maintainers; [ majiir ]; }; })maven_4暴露了与maven相同的buildMavenPackage助手因此本文前面所有模式同等适用。需要特别注意一点Maven 4 解析出的依赖集合与 Maven 3 不同Maven 4 在依赖解析语义上有变化因此在两者之间切换时必须重新计算mvnHash。历史方案一mvn2nix/buildMaven已不推荐官方文档明确标注这条路线已不再推荐应优先使用上面的buildMavenPackage。本节内容用于理解存量包与 lock-file 思路。为了完整演示文档使用一个极简 Maven 项目。其pom.xml只声明一个依赖emoji-java?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdio.github.fzakaria/groupId artifactIdmaven-demo/artifactId version1.0/version packagingjar/packaging nameNixOS Maven Demo/name dependencies dependency groupIdcom.vdurmont/groupId artifactIdemoji-java/artifactId version5.1.1/version /dependency /dependencies /project主类Main.java使用emoji-java解析表情符号import com.vdurmont.emoji.EmojiParser; public class Main { public static void main(String[] args) { String str NixOS :grinning: is super cool :smiley:!; String result EmojiParser.parseToUnicode(str); System.out.println(result); } }文档以emoji-java5.1.1 为唯一直接依赖构造该 demo 项目其完整源码仓库为nixos-maven-example文中示例文件如build-maven-repository.nix、double-invocation-repository.nix、build-jar.nix、runnable-jar.nix均来自该项目。生成依赖锁定文件buildMaven是一条模仿其他语言 lock-file 思路的路线它依赖mvn2nix-maven-plugin这个 Maven 插件。首先在项目源码仓库内部或指定pom.xml位置运行插件生成project-info.json# run this step within the projects source repository ❯ mvn org.nixos.mvn2nix:mvn2nix-maven-plugin:mvn2nix ❯ cat project-info.json | jq | head { project: { artifactId: maven-demo, groupId: org.nixos, version: 1.0, classifier: , extension: jar, dependencies: [ { artifactId: maven-resources-plugin,然后把该文件交给buildMaven函数它返回两个属性repo一个 Maven 仓库 derivation实质是project-info.json中所有依赖的 symlink farm链接农场build一个依次执行mvn compile与mvn package的简单 derivation用于构建 JAR可以作为更复杂 derivation 的参考骨架。构建 Maven 仓库的最小调用{ pkgs ? import nixpkgs { }, }: with pkgs; (buildMaven ./project-info.json).repo它与下面要讲的“双调用”相比的优势在于/nix/store条目是每个构件一个链接的 linkFarm 结构依赖集合的小幅变化不会导致从头重新下载全部构件。实际产物结构如下❯ tree $(nix-build --no-out-link build-maven-repository.nix) | head /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository ├── antlr │ └── antlr │ └── 2.7.2 │ ├── antlr-2.7.2.jar - /nix/store/d027c8f2cnmj5yrynpbq2s6wmc9cb559-antlr-2.7.2.jar │ └── antlr-2.7.2.pom - /nix/store/mv42fc5gizl8h5g5vpywz1nfiynmzgp2-antlr-2.7.2.pom ├── avalon-framework │ └── avalon-framework │ └── 4.1.3 │ ├── avalon-framework-4.1.3.jar - /nix/store/iv5fp3955w3nq28ff9xfz86wvxbiw6n9-avalon-framework-4.1.3.jar双调用Double Invocation模式官方注记这是最简单的方式但可能因为 output hash 变化引发不必要的重建。双调用绕过了“nix-build沙盒内无网络”的问题把整个 Maven 仓库当成一个整体源来下载依赖 Maven 自身的依赖解析满足固定输出哈希。它和fetchgit等 fetcher 的思路类似——只是要确定下载内容必须先实际跑一次 Maven 构建。第一步是把 Maven 项目作为固定输出派生来构建以此收集 Maven 仓库。传统上 Maven 仓库位于~/.m2/repository这里被覆写为$out目录{ lib, stdenv, maven, }: stdenv.mkDerivation { name maven-repository; buildInputs [ maven ]; src ./.; # or fetchFromGitHub, cleanSourceWith, etc buildPhase runHook preBuild mvn package -Dmaven.repo.local$out runHook postBuild ; # keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside installPhase runHook preInstall find $out -type f \ -name \*.lastUpdated -or \ -name resolver-status.properties -or \ -name _remote.repositories \ -delete runHook postInstall ; # dont do any fixup dontFixup true; outputHashAlgo null; outputHashMode recursive; # replace this with the correct SHA256 outputHash lib.fakeHash; }首次构建会失败并打印出应填入的outputHash填入正确哈希后构建产出的/nix/store条目即为完整的 Maven 仓库❯ tree $(nix-build --no-out-link double-invocation-repository.nix) | head /nix/store/8kicxzp98j68xyi9gl6jda67hp3c54fq-maven-repository ├── backport-util-concurrent │ └── backport-util-concurrent │ └── 3.1 │ ├── backport-util-concurrent-3.1.pom │ └── backport-util-concurrent-3.1.pom.sha1 ├── classworlds │ └── classworlds │ ├── 1.1 │ │ ├── classworlds-1.1.jar两个工程细节值得注意installPhase中删除*.lastUpdated、resolver-status.properties、_remote.repositories这些临时文件是为了避免后续运行中它们携带的时间戳类内容让 output hash 漂移若项目使用SNAPSHOT 依赖或版本区间version ranges随时间推移解析结果可能变化output hash 会随之改变——这是该方式不如buildMaven推荐的根本原因。构建 JAR三种仓库策略共用的最终步骤无论上文选择哪种仓库固定策略构建 JAR 这一步都是相同的{ stdenv, maven, callPackage, }: let # pick a repository derivation, here we will use buildMaven repository callPackage ./build-maven-repository.nix { }; in stdenv.mkDerivation (finalAttrs: { pname maven-demo; version 1.0; src fetchTarball https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz; buildInputs [ maven ]; buildPhase runHook preBuild echo Using repository ${repository} mvn --offline -Dmaven.repo.local${repository} package; runHook postBuild ; installPhase runHook preInstall install -Dm644 target/${finalAttrs.pname}-${finalAttrs.version}.jar $out/share/java runHook postInstall ; })这里的关键点是mvn --offline -Dmaven.repo.local${repository}完全离线、指向已固定的仓库 derivation。产物结构为❯ tree $(nix-build --no-out-link build-jar.nix) /nix/store/7jw3xdfagkc2vw8wrsdv68qpsnrxgvky-maven-demo-1.0 └── share └── java └── maven-demo-1.0.jar 2 directories, 1 file文档中有一条与 nixpkgs Java 工具链强相关的设计注记把库放进$out/share/java是因为 JDK 包携带一个stdenv setup hook它会自动把所有 build input 的share/java目录下的 JAR 追加到 CLASSPATH 环境变量——这就是 nixpkgs 中 Java 包之间“声明buildInputs即自动获得类路径依赖”的底层机制。可执行 JAR两种比 UberJar 更 Nix 化的做法上面构建出的jar文件本身不可直接执行必须用java -jar $out/share/java/output.jar并自行提供 classpath 依赖。传统的解决方式是打一个 UberJarfat jar但文档指出下面两种方法比 UberJar 更贴合 Nix 的依赖模型并都通过makeWrapper让 derivation 产出一个可执行入口。两种方式都复用前面构建的仓库无论buildMaven还是双调用产物。方式一CLASSPATH适合为 nixpkgs 提供包、不想改动pom.xml思路是读取 Maven 仓库、展平为 jar 文件列表用 CLASSPATH 分隔符拼接成完整 classpath再交给makeWrapper{ stdenv, maven, callPackage, makeWrapper, jre, }: let repository callPackage ./build-maven-repository.nix { }; in stdenv.mkDerivation (finalAttrs: { pname maven-demo; version 1.0; src fetchTarball https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz; nativeBuildInputs [ makeWrapper ]; buildInputs [ maven ]; buildPhase runHook preBuild echo Using repository ${repository} mvn --offline -Dmaven.repo.local${repository} package; runHook postBuild ; installPhase runHook preInstall mkdir -p $out/bin classpath$(find ${repository} -name *.jar -printf :%h/%f); install -Dm644 target/maven-demo-${finalAttrs.version}.jar $out/share/java # create a wrapper that will automatically set the classpath # this should be the paths from the dependency derivation makeWrapper ${jre}/bin/java $out/bin/maven-demo \ --add-flags -classpath $out/share/java/maven-demo-${finalAttrs.version}.jar:${classpath#:} \ --add-flags Main runHook postInstall ; })细节解析find ... -printf :%h/%f生成:目录/文件名形式的列表${classpath#:}在 shell 中剥掉首个多余的冒号-classpath参数把主 JAR 与全部依赖 JAR 合并为一条类路径Main是主类名。这样产出的$out/bin/maven-demo是一个已注入类路径的 wrapper 脚本。方式二MANIFEST 文件适合项目所有者、可以修改pom.xml作为上游项目维护者可以直接在pom.xml里通过maven-jar-plugin生成带 classpath 信息的 MANIFESTbuild plugins plugin artifactIdmaven-jar-plugin/artifactId configuration archive manifest addClasspathtrue/addClasspath classpathPrefix../../repository//classpathPrefix classpathLayoutTyperepository/classpathLayoutType mainClassMain/mainClass /manifest manifestEntries Class-Path./Class-Path /manifestEntries /archive /configuration /plugin /plugins /build该配置让 JAR 按 Maven 仓库的目录布局到相对路径lib/风格的repository/文件夹中查找依赖。生成的 MANIFEST 内容形如❯ unzip -q -c $(nix-build --no-out-link runnable-jar.nix)/share/java/maven-demo-1.0.jar META-INF/MANIFEST.MF Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: nixbld Class-Path: . ../../repository/com/vdurmont/emoji-java/5.1.1/emoji-java-5.1.1.jar ../../repository/org/json/json/20170516/json-20170516.jar Created-By: Apache Maven 3.6.3 Build-Jdk: 1.8.0_265 Main-Class: Main注意classpathPrefix为../../repository/因此 Nix 侧的 derivation 需要在installPhase中把仓库软链进产物使 JAR 在运行时能沿相对路径找到依赖{ stdenv, maven, callPackage, makeWrapper, jre, }: let # pick a repository derivation, here we will use buildMaven repository callPackage ./build-maven-repository.nix { }; in stdenv.mkDerivation (finalAttrs: { pname maven-demo; version 1.0; src fetchTarball https://github.com/fzakaria/nixos-maven-example/archive/main.tar.gz; nativeBuildInputs [ makeWrapper ]; buildInputs [ maven ]; buildPhase runHook preBuild echo Using repository ${repository} mvn --offline -Dmaven.repo.local${repository} package; runHook postBuild ; installPhase runHook preInstall mkdir -p $out/bin # create a symbolic link for the repository directory ln -s ${repository} $out/repository install -Dm644 target/maven-demo-${finalAttrs.version}.jar $out/share/java # create a wrapper that will automatically set the classpath # this should be the paths from the dependency derivation makeWrapper ${jre}/bin/java $out/bin/maven-demo \ --add-flags -jar $out/share/java/maven-demo-${finalAttrs.version}.jar runHook postInstall ; })文档特别指出一个 closure 控制细节脚本刻意依赖jre而非jdk以限制运行应用所必需的运行时闭包runtime closure体积。最终产物与运行结果❯ tree $(nix-build --no-out-link runnable-jar.nix) /nix/store/8d4c3ibw8ynsn01ibhyqmc1zhzz75s26-maven-demo-1.0 ├── bin │ └── maven-demo ├── repository - /nix/store/g87va52nkc8jzbmi1aqdcf2f109r4dvn-maven-repository └── share └── java └── maven-demo-1.0.jar ❯ $(nix-build --no-out-link --option tarball-ttl 1 runnable-jar.nix)/bin/maven-demo NixOS is super cool !相关实现文件与模式选择小结围绕本文内容仓库内可直接查证的相关文件包括Maven 手册章节本文主体来源Java 手册章节Java 应用通用打包信息.jar安装与 wrapper 惯例pkgs/top-level/all-packages.nixmaven3 maven别名与buildMaven属性导出pkgs/top-level/java-packages.nixmavenfod已弃用并指向maven.buildMavenPackagepkgs/top-level/all-packages.nixfetchMavenArtifact导出用于按需获取单个 Maven 构件pkgs/build-support/release/maven-build.nix从源码结构看n【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考