nixpkgs Java 打包指南:Ant 构建流程、CLASSPATH 约定与最小 JRE 定制
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
本文基于 nixpkgs 官方手册的 Java 语言章节(doc/languages-frameworks/java.section.md),系统讲解在 nixpkgs 中打包 Java 项目的完整套路:以 Ant 为例的从源码构建模板、share/java目录约定及其背后的CLASSPATH自动装配机制、makeWrapper可执行入口生成,以及 Java 9 模块系统时代如何用jre_minimal裁剪出只含所需模块的最小 JRE,并辅以仓库中 openjdk 默认版本映射 与 JRE 实现源码 作为佐证。
1. 用 Ant 从源码构建 Java 包
对于基于 Ant 的 Java 项目,nixpkgs 推荐的标准写法是用stdenv.mkDerivation描述完整的构建流程。文档给出的模板如下,nativeBuildInputs中放入构建工具链,buildPhase调用ant,installPhase把产物 JAR 安装到$out/share/java:
stdenv.mkDerivation { pname = "..."; version = "..."; src = fetchurl { # ... }; nativeBuildInputs = [ ant jdk stripJavaArchivesHook # removes timestamp metadata from jar files ]; buildPhase = '' runHook preBuild ant # build the project using ant runHook postBuild ''; installPhase = '' runHook preInstall # copy generated jar file(s) to an appropriate location in $out install -Dm644 build/foo.jar $out/share/java/foo.jar runHook postInstall ''; }几个要点值得展开:
- 各阶段的
runHook pre*/post*调用不可省略。Nix 的构建框架(stdenv)通过 setup hooks 在preBuild、postBuild、preInstall、postInstall等时机注入通用行为(日志记录、SOURCE_DATE_EPOCH处理等)。即使你的包“看起来”只需要执行ant和install,也必须保留这些 hook 调用,否则各类全局钩子(包括下面的stripJavaArchivesHook)将不会生效。 stripJavaArchivesHook与可复现构建:JAR 本质上是 ZIP 归档,压缩条目里会携带文件时间戳;如果不加该 hook,两次构建的.jar即使字节级内容相同,其 zip 元数据也可能不同,从而导致 Nix 哈希不稳定、构建不可复现。文档明确指出:不使用该 hook “很可能会让生成的.jar文件变成非确定性的,这不是最优的”;同时也要注意,使用了该 hook并不保证完全可复现(例如归档条目顺序、源文件 mtime 等其他因素仍可能破坏字节级确定性)。src的来源:模板使用fetchurl示意,实践中通常换成带sha256/hash校验的fetchurl { }、fetchFromGitHub { }等 fetcher,保证输入的可重复获取。
2.jdk别名与版本矩阵
文档说明:jdk是 OpenJDK 的别名——优先使用 nixpkgs 自编译的 OpenJDK,不可用时则回退到 Zulu 预编译版本。在 all-packages.nix 中可以看到当前仓库维护的版本别名全貌:
openjdk8/openjdk11/openjdk17/openjdk21/openjdk25及各自的_headless变体,并配有jdk8、jdk11、jdk17、jdk21等旧式别名;- 各版本还导出对应的
jre(如jre8 = openjdk8.jre); - 默认 JDK 的定义在 all-packages.nix 第 3763-3765 行:
# default JDK jdk = jdk21; jdk_headless = jdk21_headless;即当前仓库里jdk指向jdk21。此外还有openjdkN-bootstrap系列(自举编译用),以及openjdk直接别名到默认jdk。如果你的包只依赖某一个具体版本(例如 Java 8 生态的老项目),应显式写jdk8而不是笼统的jdk,避免默认版本升级带来的行为漂移。
3.share/java约定与 CLASSPATH 自动装配
JAR 文件按“用途”分两类安装,这是 nixpkgs 里 Java 打包最重要的约定:
- 供其他包作为依赖使用的公共库 JAR:安装到
$out/share/java。 - 包内部使用的私有 JAR:安装到
$out/share/<package-name>之类的私有位置,避免污染依赖方的 classpath。
公共库机制的原理是:JDK 自带一个 stdenv setup hook,它会把所有 build input 的share/java目录下的 JAR 追加进CLASSPATH环境变量。文档给的例子是:若包libfoo把foo.jar装到自己的share/java目录,而另一个包声明了
{ buildInputs = [ libfoo ]; nativeBuildInputs = [ jdk ]; }那么构建时CLASSPATH会被自动设置为/nix/store/...-libfoo/share/java/foo.jar,无需手写javac -cp ...。
在仓库源码中可以印证这条链路:
- openjdk/generic.nix 中 OpenJDK 派生项声明了
propagatedBuildInputs = [ setJavaClassPath ];,意味着任何把 JDK/JRE 放进buildInputs/nativeBuildInputs的包都会传递性地获得该 classpath 钩子; - generic.nix 第 575-581 行 还针对 Java 11 之前的版本显式把
setJavaClassPath写入 JRE 输出的propagated-build-inputs,注释写明目的就是“让任何依赖 JRE 的包都能正确设置$CLASSPATH”(Java 11+ 的 JRE 是 JDK 输出的一部分,不再需要单独传播); - 同文件 第 567-574 行 的
preFixup会在$out/nix-support/setup-hook中自动设置JAVA_HOME,这也是第 5 节--set JAVA_HOME技巧的来源。
这条机制的实用含义是:你的包只要把“对外暴露的 API JAR”放进share/java,下游包只需在buildInputs里列出你这个包名,classpath 就自动就位——依赖关系通过 Nix 的闭包精确表达,而不是通过系统级的全局 classpath。
4. 用 makeWrapper 为程序生成可执行入口
如果你的 Java 包对外提供的是命令行程序而不是库,需要生成一个包装脚本,用 JRE 来启动它。文档推荐makeWrapper:
{ nativeBuildInputs = [ makeWrapper ]; installPhase = '' runHook preInstall mkdir -p $out/bin makeWrapper ${jre}/bin/java $out/bin/foo \ --add-flags "-cp $out/share/java/foo.jar org.foo.Main" runHook postInstall ''; }- 包装脚本的真实内容是“
$out/bin/foo=jre/bin/java -cp <你的jar> <主类>的 wrapper”;makeWrapper会生成带 shebang 的可执行脚本并设置PATH等环境(makeWrapper实现见 pkgs/build-support/setup-hooks/make-wrapper.sh)。 -cp参数按你的入口 JAR 与主类修改;如果程序还依赖buildInputs中的库,class 搜索路径已由第 3 节的CLASSPATH机制自动补齐,wrapper 里只需写明本包自身的入口 JAR。- 生成的脚本会自动被 Nix 的
patchShebangs/包装机制处理,java可执行文件通过${jre}/bin/java的绝对路径引用,因此最终产物在目标系统上可以脱离构建环境运行。
5. Java 9 之后的模块系统与jre_minimal
自 Java 9 引入 JPMS(Java Platform Module System)以来,Java 发行版通常不再提供通用的 JRE:JRE 需要按应用所需的模块现场生成。文档对此的解释与仓库实现完全对应:
- 通用系统的默认
jre就是完整 JDK。因为无法预知通用系统上会运行哪些应用,nixpkgs 干脆让jre回退到完整 JDK。这一点在 all-packages.nix 第 3767-3775 行 有明确注释与定义:
# Since the introduction of the Java Platform Module System in Java 9, Java # no longer ships a separate JRE package. # # If you are building a 'minimal' system/image, you are encouraged to use # 'jre_minimal' to build a bespoke JRE containing only the modules you need. # # For a general-purpose system, 'jre' defaults to the full JDK: jre = jdk; jre_headless = jdk_headless;- 构建最小系统/镜像时,应 override
jre_minimal的modules参数,只保留应用真正用到的模块:
let my_jre = pkgs.jre_minimal.override { modules = [ # The modules used by 'something' and 'other' combined: "java.base" "java.logging" ]; }; something = (pkgs.something.override { jre = my_jre; }); other = (pkgs.other.override { jre = my_jre; }); in <...>jre_minimal的实现是 pkgs/development/compilers/openjdk/jre.nix,几个源码细节值得注意:
函数签名(第 1-8 行)接收
jdk、jdkOnBuild以及默认值为[ "java.base" ]的modules参数;核心构建步骤就是一行
jlink(第 24-30 行):buildPhase = '' runHook preBuild jlink --module-path ${jdk}/lib/openjdk/jmods --add-modules ${lib.concatStringsSep "," modules} --output $out runHook postBuild '';即从完整 JDK 的
jmods目录中只链接你列出的模块,输出就是一个独立的bin/lib布局的 JRE 目录;由于使用
jlink的 JDK 必须来自构建平台,参数jdkOnBuild在 all-packages.nix 第 3779-3797 行 中通过buildPackages.jdkN传入,天然支持交叉构建;passthru里带了两个自测用例jre_minimal-hello与jre_minimal-hello-logging(第 34-40 行),分别验证最简 JRE 与带java.logging模块的 JRE 能否正常运行 hello 程序——你可以在仓库中查看 tests 目录 了解具体断言。各主版本另有
jre11_minimal、jre17_minimal、jre21_minimal、jre25_minimal快捷包(all-packages.nix 第 3777-3804 行),默认jre_minimal跟随默认 JDK 版本。
选择 JRE 底层的 JDK 变体:你还可以指定jre_minimal基于哪个 JDK 构建,例如选 headless 版本以避免引入 GTK+ 链接:
{ my_jre = pkgs.jre_minimal.override { jdk = jdk11_headless; }; }对无图形环境的服务器或最小镜像,*_headless变体通常能显著减少 C 库依赖。
JAVA_HOME的通用设置:所有 JDK 包都在passthru中导出home属性(见 generic.nix 第 596-597 行:home = "${finalAttrs.finalPackage}/lib/openjdk";)。如果你的应用要求环境里设置JAVA_HOME,可以用makeWrapper的--set参数以通用方式注入,例如:
--set JAVA_HOME ${jdk.home}6. 使用非 OpenJDK 编译器(GCJ)
文档最后说明,也可以不用 OpenJDK 的javac而用其他 Java 编译器,例如 GNU 编译器集合中的gcj:
{ nativeBuildInputs = [ gcj ant ]; }此时 Ant 会自动使用gij(GNU Java Runtime)而不是 OpenJRE 来运行编译流程。这是一条兼容性逃生通道:当某个项目依赖 GNU 工具链特有的行为时可以考虑,但对绝大多数项目,直接使用jdk仍然是 nixpkgs 中的主流做法。
小结
把本文要点浓缩成一张打包清单:
| 场景 | 做法 |
|---|---|
| Ant 项目从源码构建 | stdenv.mkDerivation+nativeBuildInputs = [ ant jdk stripJavaArchivesHook ],buildPhase 调ant,installPhase 装 JAR |
| 对外暴露的库 JAR | 安装到$out/share/java,下游经buildInputs自动进入CLASSPATH |
| 包内部私有 JAR | 安装到$out/share/<package-name> |
| 提供可执行程序 | makeWrapper包装jre/bin/java -cp ... 主类 |
| 最小系统/镜像 | jre_minimal.override { modules = [ ... ]; jdk = ..._headless; } |
需要JAVA_HOME | makeWrapper --set JAVA_HOME ${jdk.home} |
| 需要 GNU 工具链 | nativeBuildInputs = [ gcj ant ] |
以上所有机制均有仓库源码支撑:版本别名与jre = jdk的默认值见 pkgs/top-level/all-packages.nix,JDK 的 classpath/JAVA_HOME 注入见 pkgs/development/compilers/openjdk/generic.nix,最小 JRE 的jlink实现与自测用例见 pkgs/development/compilers/openjdk/jre.nix,而本文所有约定的权威描述以官方手册章节 doc/languages-frameworks/java.section.md 为准。
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考