NixOS 声明式包管理实战:environment.systemPackages 选项与系统路径实现的深度解析
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
在 NixOS 中,管理"系统装了哪些软件"有两种风格:声明式(declarative)与临时式(ad hoc)。本篇聚焦声明式包管理——通过在configuration.nix中设置environment.systemPackages选项来声明软件清单,解释它在nixos-rebuild switch时如何生效,并结合 NixOS 模块源码剖析该选项的类型定义、默认值(corePackages/defaultPackages)以及最终生成/run/current-system/sw环境目录的底层调用链。读完后,你将能够正确地声明、升级和卸载系统级包,并理解 NixOS 与nix-env临时安装方式在可复现性上的本质区别。
什么是声明式包管理
NixOS 提供两种截然不同的包管理方式(见 package-mgmt.chapter.md):
- 声明式(Declarative):在
configuration.nix中声明你想要哪些包。每次执行nixos-rebuild时,NixOS 都会保证你得到与声明一致的一整套二进制。 - 临时式(Ad hoc):通过
nix-env命令安装、升级、卸载包。这种方式可以混用不同 Nixpkgs 版本的包,也是非 root 用户的唯一选择(详见 ad-hoc-packages.section.md)。
声明式的核心优势在于系统状态由单一配置文件完整定义:软件清单进入版本控制后,任何一台机器执行同一配置就能得到完全一致的软件集合,且每次重建时所有包都会被同步更新到当前 Nixpkgs 中的最新版本。
用 environment.systemPackages 声明软件
声明式包管理的全部入口就是environment.systemPackages这一个选项。例如,向configuration.nix中加入下面一行,即可启用 Mozilla Thunderbird 邮件客户端:
{ environment.systemPackages = [ pkgs.thunderbird ]; }这条声明的效果是:当执行nixos-rebuild switch时,Nixpkgs 中的 Thunderbird 包会作为系统构建/下载的一部分被拉取并链接进系统环境。
"卸载"则反过来操作:把包从environment.systemPackages列表中移除,再运行nixos-rebuild switch,该包就会从/run/current-system/sw中消失。由于声明是纯函数式的,安装与卸载完全对称,不存在残留。
一个常见误区:有 NixOS 模块的包不要只放列表里
原文档中有一个重要提示:某些包需要额外的全局配置(例如 D-Bus 或 systemd 服务注册),仅仅把包加进environment.systemPackages可能并不足够,建议先查选项列表,确认该包是否存在对应的 NixOS 模块。
仓库源码印证了这一点。以 Thunderbird 为例,仓库中不仅有裸包pkgs.thunderbird,还有专门的 NixOS 模块 thunderbird.nix。该模块支持programs.thunderbird.policies(可声明式安装扩展)、programs.thunderbird.preferences(对应about:config的设置,默认状态为locked)等选项。其激活逻辑最终只是把包装进系统环境:
# nixos/modules/programs/thunderbird.nix(config 部分,约 L70-L71) config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; ... };也就是说,programs.thunderbird.enable = true;与手动把pkgs.thunderbird写进environment.systemPackages殊途同归,但前者额外生成了/etc/thunderbird/policies/policies.json等配置文件。实践原则:能用模块声明的(带programs.*.enable的),优先用模块;没有模块的纯应用才直接进environment.systemPackages。
environment.systemPackages 的选项定义与默认值
这个选项并非孤立存在。在 NixOS 模块 system-path.nix 中,它被定义为一个包列表类型、默认为空列表的选项(选项定义):
environment.systemPackages = lib.mkOption { type = lib.types.listOf lib.types.package; default = [ ]; example = lib.literalExpression "[ pkgs.firefox pkgs.thunderbird ]"; description = '' The set of packages that appear in /run/current-system/sw. These packages are automatically available to all users, and are automatically updated every time you rebuild the system configuration. ... ''; };关键点在于描述中的两句话:这些包会出现在/run/current-system/sw中,自动对所有用户可见,并且每次重建系统配置时自动更新——这正是它与把包装进用户级默认 profile(/nix/var/nix/profiles/default)的最大区别。
corePackages 与 defaultPackages:系统的"底座"
同一个模块还定义了两个伴生选项,它们共同构成了environment.systemPackages的实际默认内容(system-path.nix 的 config 部分):
config = { environment.corePackages = corePackages; # 交互系统必需的底层工具集 environment.systemPackages = config.environment.corePackages ++ config.environment.defaultPackages; ... };environment.corePackages:普通交互系统的核心包集合,源码中列出的默认名单(corePackageNames)包括bashInteractive、coreutils-full、gnugrep、gnutar、findutils、curl、procps、util-linux等约 29 个基础工具,外加 C 标准库(pkgs.stdenv.cc.libc)。文档明确警告:"Only change this if you know what you're doing!"。environment.defaultPackages:并非严格必需、可在极简安装中移除的默认包,源码默认值为perl、rsync、strace三个(defaultPackageNames)。
这两组包在安装时都会通过lib.setPrio把meta.priority加 3(即降低其安装优先级),目的是让同名二进制在目录合并时优先来自你显式声明的包,而不是被底座包顶替:
corePackages = (map ( n: let pkg = pkgs.${n}; in lib.setPrio ((pkg.meta.priority or lib.meta.defaultPriority) + 3) pkg ) corePackageNames) ++ [ pkgs.stdenv.cc.libc ];此外该模块还提供三个辅助选项,用于精细控制/run/current-system/sw的内容:
| 选项 | 类型 | 作用 |
|---|---|---|
environment.pathsToLink | 字符串列表 | 列出要在sw下额外建立符号链接的目录,如"/bin"、"/etc/xdg"、"/lib"(NSS 模块依赖它)等,默认值见 system-path.nix |
environment.extraOutputsToInstall | 字符串列表 | 为systemPackages中每个包追加 derivation 输出(如"dev"、"info")并链接进sw |
environment.extraSetup | 多行文本 | 在系统环境构建完成后执行的 Shell 片段,只能用于修改环境内部(如生成 MIME 缓存),环境位于$out |
从配置到 /run/current-system/sw 的实现链路
environment.systemPackages最终如何变成"所有用户 PATH 里可用的命令"?仓库源码给出了清晰的三段式调用链。
第一步:构建合并环境。system-path.nix 中,选项值被交给pkgs.buildEnv生成名为system-path的派生环境:
system.path = pkgs.buildEnv { name = "system-path"; paths = config.environment.systemPackages; inherit (config.environment) pathsToLink extraOutputsToInstall; ignoreCollisions = true; postBuild = '' # 删除被包装的二进制,它们不应通过 PATH 直接访问 find $out/bin -maxdepth 1 -name ".*-wrapped" -type l -delete find $out/bin -maxdepth 1 -name ".*-wrapped_*" -type l -delete if [ -x $out/bin/glib-compile-schemas -a -w $out/share/glib-2.0/schemas ]; then $out/bin/glib-compile-schemas $out/share/glib-2.0/schemas fi ${config.environment.extraSetup} ''; };buildEnv会把列表中所有包的输出合并到一个目录树下(ignoreCollisions = true允许同名文件共存,靠优先级与去重规则决定可见版本);postBuild还会清理以.开头、不应暴露到 PATH 的 wrapped 二进制,并顺手执行glib-compile-schemas编译 GTK/GNOME 的 GSettings schema——这就是为什么桌面应用放进systemPackages后 schema 能正常工作。environment.extraSetup也在这一步被注入执行。
第二步:挂载为 $out/sw。系统顶层激活脚本将system.path符号链接为环境的sw目录(top-level.nix):
ln -s ${config.system.path} $out/sw第三步:指向 /run/current-system。每次nixos-rebuild switch激活新系统时,激活脚本把/run/current-system重新指向当前系统配置(activation-script.nix):
ln -sfn "$(readlink -f "$systemConfig")" /run/current-system把三步串起来:environment.systemPackages→buildEnv生成system-path→ 链接为$out/sw→/run/current-system/sw。系统PATH中指向/run/current-system/sw/bin的部分,因此始终反映的是当前激活配置里声明的完整包集合。这也解释了为什么"删除列表项 +nixos-rebuild switch"就是标准的卸载方式——旧环境仍留在 Nix store 中可回滚,但不再被/run/current-system引用。
NixOS 集成测试中也大量使用了这套机制来给测试机预装命令行工具,例如 accountsservice.nix 中的environment.systemPackages = with pkgs; [ jq ];,是查看该选项在真实配置中最小用法的一个样例。
如何查找可用包
在声明包之前,需要先知道 Nixpkgs 中包的属性名。原文档给出的命令是:
$ nix-env -qaP '*' --description nixos.firefox firefox-23.0 Mozilla Firefox - the browser, reloaded ...输出第一列是属性名(attribute name),例如nixos.thunderbird。这里有一个容易混淆的细节:nixos前缀表示"从nixos通道取包",这只在 CLI 工具(nix-env等)中有效;在声明式配置里必须使用pkgs变量前缀,即写pkgs.thunderbird而不是nixos.thunderbird。
结合本文的实现分析,可以给出完整的声明式工作流:
- 用
nix-env -qaP '*' --description搜索,拿到属性名(如thunderbird); - 查选项列表,确认是否存在
programs.thunderbird这类 NixOS 模块;有则enable = true,无则直接写environment.systemPackages = [ pkgs.thunderbird ];; - 运行
nixos-rebuild switch,包即被构建/下载并链接进/run/current-system/sw; - 需要移除时,从配置中删掉该行,再次
nixos-rebuild switch。
与 ad-hoc 方式的对比
理解两者的边界有助于避免"两边都装"的状态漂移。临时式安装(见 ad-hoc-packages.section.md)使用nix-env -iA nixos.thunderbird:
- 以 root 执行时包装入
/nix/var/nix/profiles/default(全系统可见);以普通用户执行则落入/nix/var/nix/profiles/per-user/username/profile(仅本人可见); - 升级依赖
nix-channel --update nixos后再装,且只影响显式操作的包,profile 中其余包不受影响;可用nix-env -u '*'升级全部有新版者,用nix-env -e卸载、nix-env --rollback回滚。
而声明式方式下,nixos-rebuild switch会把environment.systemPackages里的所有包一次性更新到当前 Nixpkgs 的版本——这正是"consistent set of binaries"承诺的来源。两者的本质差异在于:ad-hoc 状态记录在 profile 的生成历史里、机器相关;declarative 状态记录在版本控制的configuration.nix里、机器无关。
小结
environment.systemPackages是 NixOS 声明式包管理的唯一入口,类型是包列表,最终内容等于corePackages ++ defaultPackages再加上你显式声明的包(system-path.nix)。- 包经
pkgs.buildEnv合并为system-path环境,链接为$out/sw,再由激活脚本指向/run/current-system,从而对所有用户生效、随nixos-rebuild switch全量更新。 - 底座工具(corePackages)和可裁剪的默认包(
perl/rsync/strace)以meta.priority + 3降低优先级,保证你声明的包在二进制冲突时胜出。 - 有 NixOS 模块的包(如 Thunderbird)优先用
programs.*.enable,因为模块还会生成 D-Bus、systemd 单元、策略文件等全局配置;单纯塞进systemPackages可能功能不完整。 - 查找包用
nix-env -qaP '*' --description,CLI 中的nixos.前缀在配置文件中要换成pkgs.前缀;卸载 = 从列表移除 +nixos-rebuild switch。
【免费下载链接】nixpkgsNix Packages collection & NixOS项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考