十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

nixpkgs 中的 Gradle 构建支持:mitm-cache、fetchDeps 与 deps.json 锁定文件的完整指南

nixpkgs 中的 Gradle 构建支持:mitm-cache、fetchDeps 与 deps.json 锁定文件的完整指南 nixpkgs 中的 Gradle 构建支持mitm-cache、fetchDeps 与 deps.json 锁定文件的完整指南【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgsGradle 是 Java/Kotlin 世界最主流的构建工具之一但它本身并不提供可复现的依赖解析能力。nixpkgs 为此实现了一套独特的方案通过 Gradle setup hook 拦截 Gradle 发出的所有网络请求借助 mitm-cache 中间人代理把构建过程中实际拉取的依赖记录下来存为deps.json锁定文件从而让每次构建都从离线缓存中取回完全相同的依赖。读完本文你将掌握在 nixpkgs 中编写 Gradle 派生包的完整写法、gradle.fetchDeps各参数的含义与两种pkg传参方式的区别、更新脚本update script的内部工作流程以及 setup hook 暴露的全部环境变量和底层 Maven 仓库/SNAPSHOT/锁定文件格式的原理。为什么 Gradle 需要专门的锁定机制Gradle 的构建脚本本质上是一门图灵完备的 DSL计算最终会拉取哪些依赖这件事不仅理论上、实践中也往往无法静态完成。获取依赖的过程可能需要编译原生代码、执行命令探测主机平台甚至直接用 JVM 代码或curl/wget去下载文件。这些写法在 Java 生态中很常见也被视为正常实践见 Gradle 构建管理器 README。更棘手的是 Maven 仓库的快照snapshot机制——依赖可以永远指向最新不稳定版本这对可复现构建是灾难。Gradle 也不提供导出完整依赖 URL 哈希清单的可用接口。因此 nixpkgs 的做法是真正运行一次 Gradle用 mitm-cache 拦截它的全部 HTTP 请求并记录每个被访问文件的哈希生成一个 Nix 派生包之后构建时Gradle 通过本地代理从该缓存中取文件完全不需要联网。在源码层面这套机制由 default.nix 中的wrapGradle函数组装它把 mitm-cache 的 setup hook 与 setup-hook.sh 合并为一个gradle-setup-hook与 Gradle 本体、mitm-cache 一起通过symlinkJoin打包成最终的gradle包。构建一个 Gradle 包典型派生包写法一个典型的 Gradle 派生包如下取自官方文档中的 pdftk 示例stdenv.mkDerivation (finalAttrs: { pname pdftk; version 3.3.3; src fetchFromGitLab { owner pdftk-java; repo pdftk; tag v${finalAttrs.version}; hash sha256-ciKotTHSEcITfQYKFZ6sY2LZnXGChBJy0eno8B3YHY; }; nativeBuildInputs [ gradle makeWrapper ]; # 如果包有依赖必须设置 mitmCache mitmCache gradle.fetchDeps { inherit (finalAttrs) pname; data ./deps.json; }; # 在 Darwin 上使用 mitm-cache 所必需 __darwinAllowLocalNetworking true; gradleFlags [ -Dfile.encodingutf-8 ]; # 默认值为 assemble gradleBuildTask shadowJar; # 会执行 gradleCheckTask默认 test doCheck true; installPhase mkdir -p $out/{bin,share/pdftk} cp build/libs/pdftk-all.jar $out/share/pdftk makeWrapper ${lib.getExe jre} $out/bin/pdftk \ --add-flags -jar $out/share/pdftk/pdftk-all.jar cp ${finalAttrs.src}/pdftk.1 $out/share/man/man1 ; meta.sourceProvenance with lib.sourceTypes; [ fromSource binaryBytecode # mitm cache ]; })关键点说明mitmCache这是整个可复现机制的核心。它必须是一个通过gradle.fetchDeps生成的依赖缓存派生包。setup hook 检测到mitmCache后会在构建环境内启动 mitm-cache 代理并把 Gradle 的 HTTP(S) 流量全部导向它见 setup-hook.sh 中gradleConfigureHook设置http.proxyHost/https.proxyHost指向$MITM_CACHE_HOST/$MITM_CACHE_PORT并用 JDK 的keytool把 mitm-cache 的 CA 证书导入一个动态生成的 Java keystore作为javax.net.ssl.trustStore。如果没有 mitmCachehook 则直接追加--offline标志。__darwinAllowLocalNetworking trueDarwin 构建沙箱默认禁止本地网络通信而 mitm-cache 需要通过回环地址转发请求因此必须放开。meta.sourceProvenance由于依赖缓存中包含二进制的 jar 字节码需要如实声明binaryBytecode。gradleFlags、gradleBuildTask、gradleCheckTask、doCheck分别控制每次 Gradle 调用附加的命令行参数、构建任务默认assemble与检查任务默认test。当前 nixpkgs 中可用的 Gradle 版本可以从 default.nix 确认gradle_88.14.4默认 JDK 21与gradle_99.7.1默认 JDK 25其中gradle属性指向gradle_8。两个大版本都内置了对应的版本更新脚本updateScriptMajorVersion且默认 JDK 均为 LTS遵循 Gradle 官方兼容矩阵。更新或初始化依赖锁定文件依赖锁定文件的更新/初始化通过运行更新脚本完成$(nix-build -A pname.mitmCache.updateScript)nix-build构建出updateScript派生包并在 stdout 打印其路径$(...)则在该路径处执行这个脚本。从源码看updateScript由 update-deps.nix 生成它的工作流程是用openssl现场生成一个自签 CAca.key/ca.cer并通过ephemeral-port-reserve预留一个临时端口在本地启动mitm-cache record进程其启动参数值得注意--reject \.(md5|sha(1|256|512:?):?)$—— 拒绝记录.md5/.sha1/.sha256/.sha512校验和文件。原因在 README 中有精彩的反例若两个 jar 内容相同而 Gradle 按不同顺序先取.sha1第二个 jar 可能因缓存命中校验和而跳过下载导致缓存中缺失该 jar下次换个执行顺序就会构建失败--forget-redirects-from .*—— 丢弃所有重定向使锁定文件不包含 CDN 跳转地址既保持可预测又防止恶意篡改重定向目标--record-text /maven-metadata\.xml$—— 把 Maven 元数据 XML 以文本形式记录而不是仅记录哈希供后续在 Nix 端重建元数据通过srcOnly构建包的源码进入该源码的nix-shell在 Linux 上使用bwrap沙箱--unshare-all --share-net挂载/nix、源码路径等沙箱只用于防止混乱的构建脚本污染$HOME非 Linux 平台直接nix-shell --pure设置IN_GRADLE_UPDATE_DEPS1依次执行派生包的unpackPhase、patchPhase、configurePhase然后执行gradleUpdateScriptsetup hook 为其提供默认实现依次触发preBuild、preGradleUpdatehook运行gradleUpdateTask任务再触发postGradleUpdatehook默认gradleUpdateTask为nixDownloadDeps该任务由 init-deps.gradle 这个 init script 注入它遍历项目中所有可解析的configurations及buildscript.configurations并调用resolve()迫使 Gradle 把全部依赖真正下载一遍——于是 mitm-cache 就完成了记录最后用jq校验 mitm-cache 输出的 JSON再由 compress-deps-json.py 把原始 mitm-cache 格式压缩为 nixpkgs Gradle 锁定文件格式!version 1写入deps.json。gradle.fetchDeps的参数fetchDeps的实现在 fetch-deps.nix其参数与文档描述一致attrPath— 包在 nixpkgs 中的属性路径例如javaPackages.openjfx25用于更新脚本的元数据attrPath会被写入passthru供nixpkgs-update/nix-update识别pname—attrPath的便捷别名通常用它代替pkg或attrPathpkg— 实际用于抓取依赖的包。默认为getAttrFromPath (splitString . attrPath) pkgs即按属性路径从pkgs中取值bwrapFlags— 覆盖 bwrap 沙箱参数默认--ro-bind $PWD $PWD主要面向下游非 nixpkgs 项目data— 依赖锁定文件的路径可以相对于包目录也可以是绝对路径。nixpkgs 中约定锁定文件命名为deps.json如果同一个包需要多个锁定文件建议创建子目录区分。另有源码中可见的silent默认 true把 stdout 重定向到 stderr 以便配合 update script 组合子与useBwrap默认仅在 Linux 上启用两个参数。当包不能用pkgs.pname简单求值时如果包不在 nixpkgs 中、或你想覆盖它的某些属性就需要向gradle.fetchDeps传pkg而不是pname。有两种方式方式一把获取包所需的派生参数加入调用作用域例如{ lib, stdenv, gradle, # ... pdftk, }: stdenv.mkDerivation (finalAttrs: { # ... mitmCache gradle.fetchDeps { pkg pdftk; data ./deps.json; }; })这种写法的优点是允许你override更新脚本所用pkg的任何参数例如pkg pdftk.override { enableSomeFlag true; }。方式二使用finalAttrs.finalPackagestdenv.mkDerivation (finalAttrs: { # ... mitmCache gradle.fetchDeps { pkg finalAttrs.finalPackage; data ./deps.json; }; })二者的行为差异值得记住方式一中即使派生包以不同参数被调用更新脚本本身保持不变方式二中更新脚本会随派生包参数变化而变化但代价是无法overridepkg的参数。按你的场景选择。锁定文件格式deps.jsondeps.json采用 nixpkgs 自定义的压缩格式与 mitm-cache 原始格式的区别在于每个 URL 被拆成三部分拼接为part1/part2.part3含#的部分会被解析为#artifact-id/version[/SNAPSHOT][/classifier].ext并展开为 Maven 标准路径。值可以是 SRI 哈希字符串或针对maven-metadata.xml包含少量无法从 URL 推出的元数据片段如groupId的 attrset。一个真实形态的示例见 Gradle README{ !comment: This is a Nixpkgs Gradle dependency lockfile. ..., !version: 1, https://repo.maven.apache.org/maven2: { com/badlogicgames/gdx#gdx-backend-lwjgl3/1.12.1: { jar: sha256-B3OwjHfBoHcJPFlyy4u2WJuRe4ZF/tKh7gKsDg41o0, module: sha256-9O7d2ip5E6OiwN47WWxC8XqSX/mTb0iDioCRTTyqc, pom: sha256-IRSihaCUPC2d0QzB0MVDoOWM1DXjcisTYtnaaxR9SRo } } }fetch-deps.nix中的visit/visitAttrs函数负责把这种压缩格式反向展开为 mitm-cache 所需的完整 URL → 哈希映射并对 SNAPSHOT 依赖动态重建maven-metadata.xml的文本内容。出于安全考虑锁定文件只包含 URL、哈希和极少量经校验assert断言无 XML 特殊字符的元数据任何人无法借由依赖构建过程向锁定文件注入任意内容。Gradle setup hook 的环境变量全集setup hooksetup-hook.sh接受以下环境变量mitmCache— 通过gradle.fetchDeps导入的 MITM 代理缓存。设置后 hook 会注入代理与 trustStore 参数未设置时追加--offline。gradleFlags— 附加到每次 Gradle 调用的命令行参数。hook 通过gradle函数实现把gradleFlags与gradleFlagsArray拼接后调用真正的gradle可执行文件。含空格的参数不能用gradleFlags会因分词失败必须改为在派生包的 bash 代码中写gradleFlagsArray(-flag with spaces)若要指定特定 Java 版本构建可传入-Dorg.gradle.java.home${jdk}。gradleBuildTask— 构建任务默认assemble。对应gradleBuildPhase中的gradle ${enableParallelBuilding:--parallel} ${gradleBuildTask:-assemble}。gradleCheckTask— 当doCheck true时运行的检查任务默认test。gradleUpdateTask— 更新脚本中拉取全部依赖的任务默认nixDownloadDeps即 init-deps.gradle 注入的任务。gradleUpdateScript— 更新脚本中执行的代码默认为运行preBuild与preGradleUpdatehook、执行gradleUpdateTask、最后运行postGradleUpdatehook。当依赖获取逻辑超出默认resolve()能力如依赖由构建脚本以curl下载时你可以覆盖它。gradleInitScript— 传给 Gradle 的--init-script路径。默认使用 init-build.gradle其内容是对所有AbstractArchiveTask设置preserveFileTimestamps false与reproducibleFileOrder true以生成可复现的归档。注意可复现归档可能破坏某些构建典型报错是Could not create task :jar. Replacing an existing task that may have already been used by other plugins is not supported。遇到此类错误最简单的办法是把gradleInitScript设为空脚本例如writeText empty-init-script.gradle 。enableParallelBuilding/enableParallelChecking/enableParallelUpdating— 在构建/检查阶段或更新脚本中向 Gradle 传--parallel三者默认均为 1true。如果构建以难以理解的原因失败可以尝试设为 false。dontUseGradleConfigure/dontUseGradleBuild/dontUseGradleCheck— 强制禁用对应阶段的 Gradle hook。注意禁用 configure hook 后你可能遇到诸如Failed to load native library libnative-platform.so的问题因为 configure hook 负责初始化 Gradle设置GRADLE_USER_HOME、TERMdumb、--no-daemon、--init-script等。hook 的注册逻辑也可以从源码直接读出gradleConfigureHook追加到preConfigureHooks除非设置了dontUseGradleConfigure若buildPhase/checkPhase尚未定义且未禁用则分别替换为gradleBuildPhase/gradleCheckPhase。深入原理Maven 仓库布局与 SNAPSHOT 的处理理解这套机制为何如此设计需要一点 Maven 背景详见 Gradle READMEGradle 依赖大多来自 Maven 仓库URL 格式为repo/group-id/artifact-id/base-version/artifact-id-version[-classifier].ext其中 group-id 的圆点被替换为斜杠org.slf4j→org/slf4j。由于 artifact-id 本身可含连字符无法仅凭文件名区分 artifact-id 与 version——这正是锁定文件中#压缩语法存在的意义。不同仓库对同一 artifact 可能返回不同文件例如 pom 归一化差异甚至同一个包内两个 build script 声明仓库顺序不同就会命中不同仓库。mitm-cache 以实际请求的完整 URL为键记录天然处理了这种多仓库情况。仓库中的.asc签名文件无害而.md5/.sha1校验和文件会导致缓存命中歧义如前所述因此更新脚本用--reject显式拒绝后者代理还会硬编码剥离响应中的 checksum/etag 头保证重放一致性。SNAPSHOT 依赖的解析依赖maven-metadata.xml分 G/A/V 三级npxkgs 支持 A 级与 V 级。fetch-deps.nix的visit函数在展开锁定时会为 metadata 重建完整 XML 文本包括latest/release/versions列表与 snapshot 的timestamp/buildNumber并对最新版本已知但未被 Gradle 抓取的情况主动补入versions让 Gradle 在离线环境下仍能正确解析版本。测试与验证这套构建支持自带测试位于 gradle 包目录 下tests/version验证gradle --version能正确运行设置GRADLE_USER_HOME与GRADLE_OPTS-Dorg.gradle.native.dir到临时目录避免写只读 storetests/java-application构建并运行一个最简 Java 应用tests/java-application/src/main/java/Main.java断言输出hello同时顺带验证GRADLE_OPTS生效包装层的tests/toolchains通过gradle.override { javaToolchains [ jdk11 ]; }注册额外工具链验证 hook 能通过org.gradle.java.installations.paths写入gradle.properties见 default.nix 的installPhase让 Gradle 探测到 nix-env 中提供的 JDK 版本。这些测试也展示了在 Nix 沙箱中使用 Gradle 的两个实用技巧把GRADLE_USER_HOME与 native 目录指到$TMPDIR以及使用--no-daemon避免守护进程残留。小结nixpkgs 的 Gradle 支持用一条清晰的链路解决了 Java 生态可复现构建的难题gradle.fetchDepsdeps.json锁定 Maven 依赖与元数据mitm-cache 代理在构建期离线回放setup hook 通过gradleFlags/gradleBuildTask/gradleCheckTask/gradleInitScript等环境变量暴露完整的定制面update script 则把重新运行一次 Gradle 来发现依赖这一半自动流程标准化。对维护者而言记住三条经验即可锁定文件只应包含 URL、哈希与经校验的元数据片段遇到神秘的并行构建失败先关掉--parallel遇到归档任务替换报错就换掉gradleInitScript。所有实现细节都可以在 pkgs/development/tools/build-managers/gradle/ 目录中找到对应源码。【免费下载链接】nixpkgsNix Packages collection NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表