- 包管理器
- 操作系统
【免费下载链接】nixpkgs
Nix Packages collection & NixOS
本文围绕 nixpkgs 仓库中pkgs/tools/package-management/nix/modular/目录的说明文档展开,深入讲解 Nix 官方包的模块化(modular)打包方案:该目录如何复刻上游 Nix 仓库的目录结构、workDir属性如何决定子项目的源码路径、为什么对 fetch 型源码不能使用 filesets(IFD 约束),以及mkMesonExecutable/mkMesonLibrary分层机制与nix-everything组装包的完整原理。读完本文,你可以独立阅读 nixpkgs 中 Nix 的模块化表达式,理解每个组件派生(derivation)的生成过程,并能正确使用overrideSource、appendPatches、overrideAllMesonComponents等覆盖接口。
目录定位:复刻上游结构以方便对照
nixpkgs 对 Nix 官方包的模块化打包位于 pkgs/tools/package-management/nix/modular,其目录说明见 README.md。README 明确了两条设计原则:
- 目录结构与上游仓库保持一致。该目录刻意模仿上游 Nix 仓库的布局(
src/下每个子目录对应一个构建组件,packaging/存放打包逻辑,tests/、doc/分别存放功能测试与手册构建表达式),目的是让维护者在对照上游时“更容易比较”(to make comparisons easier)。 - 文件独立维护,差异在所难免。这些打包表达式与上游仓库分开维护,因此两边出现差异是预期内的。
从仓库实际结构看,modular 目录 下src/中每个子目录都只有一个package.nix,分别对应 Nix 的各个构建组件:libutil、libutil-c、libstore、libstore-c、libfetchers、libexpr、libflake、libmain、libcmd、nix(CLI 本身)、nswrapper、perl、internal-api-docs、external-api-docs等,以及tests/functional与doc/manual。而packaging/目录包含两个关键文件:components.nix(定义所有组件的构建逻辑与 scope 接口)和 everything.nix(把所有组件拼装成最终的多输出nix包)。
包入口:独立的 splice scope
组件集合的入口是 packages.nix。它的做法是调用lib.makeScopeWithSplicing'创建一个独立 scope,把 components.nix 作为f注入,并将splicePackages、nixDependencies.newScope、otherSplices等参数传入:
{ lib, splicePackages, nixDependencies, pkgs, teams, otherSplices, version, src, }: let officialRelease = true; # 新建一个 scope,用 callPackage 注入组件间内部依赖, # 而不"污染"顶层的 pkgs 属性集 nixComponents = lib.makeScopeWithSplicing' { inherit splicePackages; inherit (nixDependencies) newScope; } { inherit otherSplices; f = import ./packaging/components.nix { inherit lib teams officialRelease pkgs src version; }; }; in nixComponents.overrideSource src源码注释解释了两个动机:其一,独立 scope 可以用callPackage注入组件之间的内部依赖(例如nix-cli依赖nix-store、nix-expr),而不会污染顶层pkgs属性集;其二,得到一个可以整体迭代的独立包集合。入口最后一行nixComponents.overrideSource src说明:默认构建就使用外层传入的 fetch 型src,而不是 fileset 本地源码。这也是 README 中“无 filesets”讨论的落点。
workDir属性:子项目源码路径如何计算
README 的第二个比较点是workDir属性:Nixpkgs 版的 Nix 打包继承了workDir属性,它决定“要构建的子项目(subproject)位于何处”,并与modular根目录比较以生成正确的相对路径,这一点与上游一致。
在仓库中,每个组件的package.nix都用一行workDir = ./.;声明自己的位置,例如 src/libutil/package.nix 与 src/nix/package.nix。真正消费workDir的逻辑在 components.nix 的localSourceLayer中:
localSourceLayer = finalAttrs: prevAttrs: let workDirPath = prevAttrs.workDir; # 注意:取 prevAttrs 而非 finalAttrs workDirSubpath = lib.path.removePrefix root workDirPath; # 相对 modular 根目录的子路径 src = lib.fileset.toSource { fileset = prevAttrs.fileset; # 断言 fileset._type == "fileset" inherit root; }; in { sourceRoot = "${src.name}/" + workDirSubpath; # 关键:定位到子目录 inherit src; # 清理 mkDerivation 无法序列化的属性 fileset = null; workDir = null; };要点拆解:
root是modular目录本身(root = ../.;),workDirPath是组件package.nix所在的绝对路径;lib.path.removePrefix root workDirPath得到子路径(如src/libutil),再拼到src.name之后形成sourceRoot。这等效于“解压整棵源码树,但只把 stdenv 的工作目录指到子项目里”,与上游 meson 子项目构建的约定一致。- 源码用注释解释了一个微妙点:取的是
prevAttrs.workDir而不是finalAttrs.workDir。原因是mkDerivation要求除passthru和meta之外的一切属性都必须可被序列化,而workDir这类路径属性不能直接进 derivation 输入,所以最终层把它置为null清除。 - 对 fetch 型源码,存在对应的 makeFetchedSourceLayer,逻辑相同但源码来自
finalScope.patchedSrc(打过补丁的完整源码),并会按补丁数量给版本号追加+N后缀(如2.30+2),以区分打过补丁的构建。
为什么不对 fetch 源码使用 filesets:IFD 约束
README 的第一个比较点解释了“无 filesets”(No filesets):如果要在 fetch 型源码上使用 filesets,会引入 IFD(Importing Fetched Datas,导入获取的数据)问题——fetch 发生在 derivation 里(构建时),而 fileset 的过滤必须发生在之后,且只能由求值器(evaluator)完成。求值器无法感知一个构建期才产生的 store 路径的内容,因此无法为其定义 fileset。
仓库中的实现印证了这个约束,overrideSource的文档注释写明:“This allows the expressions to be vendored without copying the sources, but it does make the build non-granular; all components will use a complete source. Filesets in the packaging expressions will be ignored.”(见 components.nix)。也就是说,一旦切换到 fetch 源码(默认路径),所有组件都使用完整源码树,fileset 声明被忽略,只靠sourceRoot做子目录定位。
overrideSource的实现细节同样值得注意:
- 它把 scope 里的
sourceLayer换成makeFetchedSourceLayer,并清空patches; - 若调用了
appendPatches(appendPatches 会先执行overrideSource "${./..}"再追加补丁),则用stdenvNoCC.mkDerivation+srcOnly造出一个patchedSrc,供所有组件共享,避免每个组件重复打补丁; resolvePath与filesetToSource被改写:前者把路径解析为patchedSrc + 子路径,后者退化为{ root, fileset }: finalScope.resolvePath root,即 fileset 参数在 fetch 模式下完全不参与选源。
filesetToSource在 scope 里是一层“间接引用”(components.nix 注释:Indirection for Nixpkgs to override when package.nix files are vendored),供下游在 vendoring 打包表达式时覆盖选源行为。
分层装配:mkMesonExecutable / mkMesonLibrary 的构建层
每个组件派生并非直接写stdenv.mkDerivation,而是通过 components.nix 中的mkPackageBuilder把一组 overlay 形状的分层扩展函数组合到一个用户函数上:
mkPackageBuilder = exts: userFn: stdenv.mkDerivation (lib.extends (lib.composeManyExtensions exts) userFn);三种构建器按用途组合不同的层(components.nix):
| 构建器 | 层顺序 | 用途 |
|---|---|---|
mkMesonDerivation | nixDefaultsLayer→sourceLayer→setVersionLayer→mesonLayer→fixupStaticLayer→mesonComponentOverrides | 通用 meson 派生(如文档、测试) |
mkMesonExecutable | 上述 +bsdNoLinkAsNeeded、mesonBuildLayer | 可执行文件(nix-cli、nswrapper) |
mkMesonLibrary | 上述 +mesonLibraryLayer | 共享库(nix-util等) |
各层职责(均以源码为准):
nixDefaultsLayer:strictDeps默认开启、enableParallelBuilding、pos从pname位置注入;meta默认值(homepage、donationPage、按 majorMinor 版本生成的 changelog 链接、lgpl21Plus许可、unix ++ windows平台集)见 components.nix。setVersionLayer:Nix 上游用仓库根的.version文件(由符号链接./.version指向)传递版本,构建时只有workDir可写,因此该层在preConfigure中chmod u+w ./.version并echo ${finalAttrs.version} > ./.version(components.nix)。mesonLayer:显式设mesonBuildType = "release"(因为 meson setup-hook 在未指定时默认plain,而 Nix 构建的二进制应默认优化);非 Windows/Cygwin 且非静态目标下按mesonBuildType环境变量决定是否追加-Db_lto=true;注释特别指出故意读取环境变量而非finalAttrs.mesonBuildType,以便调试者按上游手册指引通过环境变量控制构建类型。mesonLibraryLayer:Nix 2.32+ 为 GCC 加-fno-semantic-interposition -Wl,-Bsymbolic-functions(源码注释解释:GCC 默认对位置无关代码禁用内联,放弃 LD_PRELOAD 能力换取优化;Clang 则默认就内联),并把dev加入outputs。fixupStaticLayer/bsdNoLinkAsNeeded:分别处理静态 stdenv 的propagated-build-inputs污染 hack,以及 BSD 上 meson 的--as-needed链接行为问题。mesonComponentOverrides:保留给用户的扩展位,与overrideAllMesonComponents对应。
以 src/libutil/package.nix 为例,nix-util组件用mkMesonLibrary声明依赖(brotli、libsodium、openssl,2.27+ 加 libblake3,2.35pre 加 zstd,x86_64 加 libcpuid),传播 boost、libarchive、nlohmann_json,并用lib.mesonEnable "cpuid"按平台开关。CLI 组件 src/nix/package.nix 则展示了条件依赖的典型写法:buildInputs固定包含nix-store、nix-expr、nix-main、nix-cmd;当版本 ≥ 2.35 且withPluginCApi开启时追加各-cC API 库(让插件能从可执行文件解析符号,无需直接链接 Nix 库);withMimalloc在非 Windows 平台默认开启,注释称其可显著提升分配密集工作负载的求值性能。
nix-everything:把组件拼成最终多输出包
nix-everything组件由 everything.nix 定义,是所有组件派生的“组装壳”:
- 多输出:
outputs = [ "out" "dev" "doc" "man" ]。源码注释解释为何不用各组件原生的多输出直接透传:包括 Nix 自身在内,不少工具尚不能处理由任意输出组合的包;devdoc则通过passthru提供而非成为输出。 - 不做实际构建:
dontUnpack = true(解压由组件完成)、dontBuild = true、dontFixup = true(该派生不编译不链接,且 fixupPhase 目前不认为符号链接输出不可写)。 - 测试门控:
doCheck = true时,checkInputs聚合各库的单元测试运行器(nix-util-tests.tests.run、nix-store-tests.tests.run、nix-expr-tests.tests.run、nix-fetchers-tests.tests.run、nix-flake-tests.tests.run)与nix-functional-tests;旧版本(< 2.35pre,且非静态、build 可执行 host)还会纳入nix-perl-bindings(everything.nix)。 - installPhase 用
lndir合并:把nix-cli合并进$out、把各库的dev输出合并进$dev,并把nix-manual、nix-manual.man以符号链接转发到$doc/$man;Linux 且 ≥ 2.34pre 时额外并入nix-nswrapper。 - passthru 接口:
passthru.libs暴露全部nix-*-c等库(注释推荐使用-cC API 库并建议用完整包跑一遍单元测试);passthru.tests.pkg-config用testers.hasPkgConfigModules校验元数据;meta.pkgConfigModules列出nix-cmd、nix-expr、nix-flake、nix-store、nix-util等十余个模块(2.29pre+ 追加nix-fetchers-c)。
覆盖与扩展接口:如何对整棵 Nix 源码打补丁或换源
scope 层面提供了四个对外接口(在 components.nix 定义,并在 nix-everything 包装 上重新暴露):
overrideAllMesonComponents f:对每个组件派生应用一个扩展函数(overlay 形状)。components.nix中有一个刻意的注释:nix-everything本身不携带passthru.overrideAllMesonComponents之类属性,因为那会传播进nix.overrideAttrs f却在调用.overrideAllMesonComponents时被丢弃——两种机制应是同一个不动点覆盖机制的两个视图,作者明确“有意不支持这个坏掉的双不动点方案”。overrideSource src:整体替换源码(vendoring 打包表达式而不拷贝源码的场景),副作用是所有组件改用完整源码、失去 fileset 粒度。appendPatches ps:对整棵 Nix 源码追加补丁,影响所有组件;注意此模式下“对打包表达式的修改会被忽略”。overrideScope f:直接覆盖包集合内部属性,文档示例为overrideScope (finalScope: prevScope: { aws-sdk-cpp = null; })。
版本条件在代码里通过whenAtLeast表达:如nix-fetchers-c仅在 2.29pre+ 提供、nix-nswrapper在 2.34pre+ 提供、nix-perl-bindings在 2.35pre+ 置为null(components.nix),这些条件与 Nix 上游各特性落地版本一一对应。
验证与延伸路径
理解本方案后可以沿以下路径在仓库中核对细节:组件层定义见 packaging/components.nix;组装包见 packaging/everything.nix;各组件声明见 src 目录(如 src/libutil/package.nix、src/nix/package.nix、src/libstore/package.nix);测试聚合见 tests/functional/package.nix。上层目录的 pkgs/tools/package-management/nix/README.md 则描述了 Nix 新版本测试流程(nixpkgs-review pr、nix-build nixVersions.nix_$version.tests等命令),与本模块化表达式共同构成 Nix 在 nixpkgs 中的完整维护链路。
- 包管理器
- 操作系统
【免费下载链接】nixpkgs
Nix Packages collection & NixOS
相关推荐
nixpkgs 中 Home Assistant 自定义组件打包指南:buildHomeAssistantComponent 原理与实践
nixpkgs 中 Home Assistant 自定义组件打包指南:buildHomeAssistantComponent 原理与实践 本文围绕 nixpkg
包管理器操作系统nixpkgs 中 Home Assistant 自定义 Lovelace 模块的打包规范与加载机制
nixpkgs 中 Home Assistant 自定义 Lovelace 模块的打包规范与加载机制 本文以 pkgs/servers/home assista
包管理器操作系统Nix构建系统与包管理机制详解
Nix构建系统与包管理机制详解 Nix构建系统通过强大的隔离机制和内容寻址存储实现了真正的可重现构建。本文详细解析了Nix的多层次构建隔离、Derivation
包管理器开发工具CLI构建工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考