Nix:从独立工具环境到 Flake 项目与依赖回退
Nix 把软件及其依赖保存到 /nix/store 中的独立路径,再通过 Shell 环境或 Profile 引用它们。同一台机器可以保留多个版本,项目切换时选择不同依赖集合。
三个名字分别对应不同对象:Nix 是包管理器及配置语言,Nixpkgs 是软件包定义集合,NixOS 是使用 Nix 管理系统的 Linux 发行版。在 Ubuntu 或 macOS 上使用 Nix,无需先换成 NixOS。Flake 则为 Nix 项目提供输入和输出约定,开发 Shell 是其中一种输出。Nix 安装入口、Flake 模型
安装 Nix 并运行第一个工具
选择宿主安装或隔离容器
| 环境 | 常见方式 | 谁持有 Store,怎样运行构建 |
|---|---|---|
| Linux + systemd,符合官方安装器要求 | Multi-user | 管理员维护 daemon 与 build users;普通用户提交请求 |
| Linux 无 systemd | Single-user | 当前用户持有 /nix,承担安装和构建 |
| macOS | Multi-user | 安装器还处理 Store 卷和后台服务 |
| Windows | WSL2 中安装,或使用 Linux 容器 | WSL2 是否启用 systemd 决定可选安装方式 |
临时学习,不修改宿主 /nix | 普通用户 Single-user 容器 | Store 留在容器中,退出删除容器后不再保留 |
官方 Multi-user 安装器要求 Linux 使用 systemd 且没有启用 SELinux。受管机器不应为了安装工具直接关闭安全策略;先选取管理员支持的安装方式。Single-user 与 Multi-user 的安装、权限和卸载流程不同,已有安装不要直接再覆盖一套。二进制安装与平台条件
下面固定 Nix 2.35.2、Linux x86-64。ARM 或 macOS 要使用对应系统归档及摘要,不能复用这个 x86-64 文件。Linux Bash 需要 curl、tar、xz-utils;Ubuntu 缺少时由有权限的用户安装:
sudo apt-get update
sudo apt-get install -y ca-certificates curl xz-utils git普通用户新建安装材料目录,下载并核对官方归档。网络需要代理或企业 CA 时使用经过批准的配置,保留 HTTPS 校验。
INSTALL_DIR=$(mktemp -d /tmp/nix-install-lab.XXXXXX)
cd "$INSTALL_DIR"
(
set -eu
curl -q --fail --location --proto '=https' \
-o nix.tar.xz \
https://releases.nixos.org/nix/nix-2.35.2/nix-2.35.2-x86_64-linux.tar.xz
printf '%s %s\n' \
0c3960a9792331a22081c3c7a5d8465db9b17c50b3acdf18587fa4c6f2cb1158 \
nix.tar.xz | sha256sum -c -
)只有输出 nix.tar.xz: OK 后才进入安装。对应摘要来自官方归档校验文件;这可以发现传输损坏,来源信任仍依赖受信下载渠道。
宿主 Multi-user 路线会创建系统账号、服务和 /nix,需要管理员批准。确认条件后,在刚才目录执行:
(
set -eu
tar -xJf nix.tar.xz
./nix-2.35.2-x86_64-linux/install --daemon --no-channel-add
)安装器按需请求 sudo。完成后新开终端,执行 nix --version 和 systemctl status nix-daemon.socket nix-daemon.service;Socket 可触发服务启动,服务状态要结合连接请求判断。Linux Single-user 使用同一安装器的 --no-daemon --no-channel-add,由普通用户执行;不要把它当作共享机器的 Multi-user 替代品。
隔离容器路线无需执行上面的宿主安装。Docker 的安装与权限见 Docker Engine。将 Dockerfile保存到下载目录,其内容如下:
FROM ubuntu:24.04 AS bootstrap
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git nano xz-utils \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 10001 nixlab \
&& useradd --uid 10001 --gid 10001 --create-home --shell /bin/bash nixlab \
&& install -d -o 10001 -g 10001 /nix /work
FROM bootstrap
COPY --chown=10001:10001 nix.tar.xz /tmp/nix.tar.xz
USER 10001:10001
ENV USER=nixlab HOME=/home/nixlab
ENV NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
RUN echo '0c3960a9792331a22081c3c7a5d8465db9b17c50b3acdf18587fa4c6f2cb1158 /tmp/nix.tar.xz' | sha256sum -c - \
&& tar -xJf /tmp/nix.tar.xz -C /tmp \
&& /tmp/nix-2.35.2-x86_64-linux/install --no-daemon --no-channel-add --no-modify-profile \
&& rm /tmp/nix.tar.xz
ENV PATH=/home/nixlab/.nix-profile/bin:$PATH
RUN mkdir -p /home/nixlab/.config/nix \
&& printf '%s\n' 'experimental-features = nix-command flakes' 'sandbox = false' 'build-users-group =' > /home/nixlab/.config/nix/nix.conf
WORKDIR /work
CMD ["bash"]Ubuntu 软件安装阶段为容器 root;Nix 安装和后续命令使用 UID/GID 10001。这是容器内 Single-user 实验,未运行 Nix daemon。Docker 的隔离方式与 Nix 自身构建 Sandbox 不同;镜像中的 sandbox = false 只用于这份受限实验,不应复制到共享宿主配置。
docker build -t nix-lab-cli:2.35.2 "$INSTALL_DIR"
docker run --rm -it nix-lab-cli:2.35.2之后的命令在容器 Bash 中执行。容器没有挂载宿主源码、凭据、Docker Socket 或 Store;工作目录为 /work。镜像带有 nano,可用 nano 文件名 保存下面的配置,Ctrl+O 写入、Ctrl+X 退出。需要保存实验结果时,在退出容器前另行导出所需配置;这里的 --rm 会在退出后删除容器及其可写层。
临时获得一个工具,再回到原 Shell
在已安装 Nix 的宿主终端或上面的容器中,先核对身份和版本:
id -u
nix --version
nix --extra-experimental-features 'nix-command flakes' \
eval --impure --raw --expr builtins.currentSystem容器身份为 10001,CLI 输出包含 2.35.2,本次系统为 x86_64-linux。builtins.currentSystem 是读取当前平台的诊断,因此显式使用 --impure。
Nix 2.35 的 Flakes 和新 CLI 仍标为实验接口。为了不修改全局配置,在当前 Bash 定义一个传入功能开关的函数,后续 nx 均调用同一个 Nix:
nx() { nix --extra-experimental-features 'nix-command flakes' "$@"; }
NIXPKGS_REF=github:NixOS/nixpkgs/21a67dc470149f337cecafbe965d8d252a390518
command -v hello || true
nx shell "$NIXPKGS_REF#hello" --command hello
command -v hello || true首次运行可能下载 Nixpkgs 定义及工具闭包,成功后打印 Hello, world!。这条命令为子进程准备工具,不把 Hello 永久安装到用户 Profile;原 Shell 能否找到 Hello 仍取决于它原来的 PATH。固定 revision 选自 nixos-26.05,这里不依赖机器上的 <nixpkgs> Channel 或隐式 Registry 别名。Nix 2.35 的手册将这个入口归入 nix env shell,nix shell 仍可使用。命令说明
若报实验功能未开启,检查是否绕过了 nx 函数;若提示无法连接 Store,宿主 Multi-user 先检查 daemon 与 Socket,容器则检查当前用户对 /nix 的权限。下载失败与表达式求值失败分开判断,不通过重装或放开 Store 写权限来跳过错误。
从构建对象到 Flake 项目
表达式、derivation 和 Store 分别保存什么
Nix 表达式经过求值后可以产生字符串、列表、属性集,也可以描述一个构建任务。构建任务的底层记录称为 derivation,常见文件名以 .drv 结尾,其中包含目标平台、builder、参数、环境变量、输入及输出约定。Nix 根据依赖关系准备输入,再从可信二进制缓存取得输出,或调用 builder 构建。
flake.nix + flake.lock
└─ 求值:选择平台与输出
└─ derivation:描述构建任务及依赖
├─ 缓存可提供且满足信任要求 → 下载输出
└─ 缓存不可用或没有输出 → 准备依赖后构建
└─ /nix/store/<hash>-<name>
├─ result:项目构建结果链接
├─ devShell:准备命令与环境变量
└─ Profile:保留一组可切换的软件引用Store 路径前面的哈希用于区分对象身份。常见 input-addressed 构建的输出路径由构建描述等输入决定,不能把这段路径直接解释成输出文件的 SHA-256;fixed-output 和 content-addressed 对象另有寻址规则。Store 对象之间还会有引用,某个程序及它递归需要的 Store 对象组成 closure(闭包)。复制一个 bin/hello 文件通常无法代替复制它的运行闭包。Derivation、Store 路径
Nix 语言里,{ name = value; } 是属性集,[ a b ] 是列表;x: expression 定义接收 x 的函数;let ... in ... 为局部表达式绑定名字。下面的 inherit system; 等价于 system = system;。这些语法用来组合包定义,没有要求把应用业务代码也改成 Nix。Nix 语言
建立包含软件包、开发 Shell 和检查项的项目
在同一个终端创建独立目录:
PROJECT_DIR=$(mktemp -d /tmp/nix-project-lab.XXXXXX)
cd "$PROJECT_DIR"
git init -q保存 flake.nix:
{
description = "Nix local teaching environment";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/21a67dc470149f337cecafbe965d8d252a390518";
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
eachSystem = nixpkgs.lib.genAttrs systems;
packagesFor = system: import nixpkgs { inherit system; };
in {
packages = eachSystem (system:
let pkgs = packagesFor system;
in { default = pkgs.hello; });
devShells = eachSystem (system:
let pkgs = packagesFor system;
in {
default = pkgs.mkShellNoCC {
packages = [ pkgs.hello pkgs.jq ];
LAB_MARKER = "nix-lab";
};
});
checks = eachSystem (system:
let pkgs = packagesFor system;
in {
json = pkgs.runCommand "nix-lab-json-check" {
nativeBuildInputs = [ pkgs.jq ];
} ''
printf '{"status":"ok"}\n' | jq -e '.status == "ok"' >/dev/null
mkdir -p "$out"
printf 'ok\n' > "$out/result.txt"
'';
});
};
}packages.<system>.default 输出 Hello 软件包;devShells.<system>.default 准备 Hello、jq 和 LAB_MARKER;checks.<system>.json 在构建任务中执行 JSON 断言。$out 由 Nix 分配,检查成功才产生对应输出。四个平台分别生成属性,本次运行路径是 Linux x86-64;列出属性不表示四个平台均已完成构建测试。
再保存 check.sh。这份脚本检查开发 Shell 中实际可用的工具和变量:
#!/usr/bin/env bash
set -euo pipefail
test "${LAB_MARKER:-}" = nix-lab
hello --version | sed -n '1p'
printf '{"status":"ok"}\n' | jq -e '.status == "ok"' >/dev/null
printf 'project-check: ok\n'保存 .gitignore,防止把构建链接和实验 Profile 加入源码:
result
.lab-profile*
.dev-profile*
.cache/随后生成输入锁,也可保存与示例对应的 flake.lock 后执行同样的检查:
git add flake.nix check.sh .gitignore
if test -f flake.lock; then git add flake.lock; fi
nx flake lock
git add flake.lock
nx flake show --no-update-lock-file输出应包含本平台的 packages、devShells 和 checks。flake.lock 的 locked 部分记录具体 revision 与源树 narHash,original 部分记录原始引用;它锁住输入解析结果。项目自身源码还要由 Git 提交来定位,外部数据库、运行时凭据、内核和远程接口也不会自动装进这个锁文件。
在 Git 项目中,Nix 使用 Git 纳入跟踪的文件;刚创建但未 git add 的文件可能不在 Flake 的源树中。已跟踪文件的未提交修改会参与当前本地求值,同时出现 dirty tree 提示。准备共享或 CI 使用时,将经过检查的源码和锁文件一起提交,避免同一 commit 下还依赖未提交修改。
构建、查看闭包,再执行项目检查
nx build .#default --no-update-lock-file -L
./result/bin/hello
OUT=$(readlink -f result)
printf '%s\n' "$OUT"
nix-store -q --deriver "$OUT"
nix-store -q --references "$OUT"
nix-store -qR "$OUT"
nx path-info -Sh "$OUT"Hello 应打印问候语,result 指向以 -hello- 加版本结尾的 Store 路径。--references 展示直接引用,-qR 展示递归闭包,path-info -Sh 带出闭包大小。deriver 查询在本地记录不全时可以返回 unknown-deriver;不能仅据此判定程序已损坏。Store 查询
开发环境与构建产物分别运行:
nx develop --no-update-lock-file --command bash check.sh
nx flake check --no-update-lock-file -L第一条应打印 Hello 版本和 project-check: ok;第二条求值并构建本机适用的 Flake 检查项。nix develop 提供开发依赖,命令仍以当前用户身份运行,能访问用户有权限访问的文件与网络。shellHook 也会执行项目代码,进入他人项目之前应先审查。构建 Sandbox 的配置不能用来担保交互 Shell 安全。开发 Shell
用两个错误区分包选择和源码缺失
在独立实验目录中,将 flake.nix 的 pkgs.jq 暂时改成 pkgs.jqq,再执行前面的 nx develop。预期在求值阶段报告属性缺失,尚未进入 check.sh。先恢复包名,再处理其他问题;清缓存不会为错误属性补出软件包。
第二个实验是把 LAB_MARKER 改成:
LAB_MARKER = builtins.readFile ./marker.txt;然后在项目目录执行:
printf 'nix-lab' > marker.txt
nx develop --no-update-lock-file --command bash check.sh文件虽在工作目录中,未加入 Git 时仍可能报告 Flake 源树中缺少它。执行 git add marker.txt 后重试,应恢复通过。实验结束把配置改回 LAB_MARKER = "nix-lab";,再执行 git rm --cached marker.txt,最后删除本次创建的 marker.txt。
不要用真实密钥代替这个测试文件。Nix 2.35 对 Flake 源码采用延迟复制优化,但需要作为构建输入的源码仍可能进入 Store;构建记录、生成配置与可读 Store 均不适合保存 Secret。运行时密钥应从受限文件、凭据代理或部署系统注入。2.35 源码复制行为
更新依赖、切换 Profile 与共享环境
软件版本跟随哪个输入变化
升级 Nix CLI 和升级项目软件包是两项变更。CLI 版本决定可用接口与执行行为;inputs.nixpkgs 选中的包定义决定 pkgs.hello、pkgs.jq 等软件如何构建。项目已经固定 Nixpkgs commit 时,单纯更新机器上的 Nix CLI 不会自动把 Hello 换成新版本。
首次 flake lock 补齐缺失输入;有意更新既有输入使用:
nx flake update nixpkgs
git diff -- flake.nix flake.lock本例 URL 写死了 revision,所以这条 update 通常不会换到别的 commit。要升级,先在官方 Nixpkgs 分支中选定并审查目标 revision,修改 inputs.nixpkgs.url,再运行 update。使用 github:NixOS/nixpkgs/nixos-26.05 这类分支引用的项目,update 会重新解析该分支;平时构建则继续使用锁中的 revision。包的属性名、默认参数和平台支持可能随包定义变化,需要一起核对。输入更新
更新后先比较锁差异,再运行开发检查和构建:
nx develop --no-update-lock-file --command bash check.sh
nx flake check --no-update-lock-file -L
nx build .#default --no-update-lock-file
./result/bin/hello通过后将 flake.nix 与 flake.lock 一起提交。CI 使用同样的命令和 --no-update-lock-file,输入与锁不匹配时应失败,由依赖更新流程处理;不要让普通构建顺带更新锁。
项目回退时,从版本库取回经过验证的源码、Flake 与锁文件组合,再重新构建、检查。旧输入的下载地址和二进制缓存可能已经不可用,关键发布还需要保存源码归档及输出闭包。数据库迁移、用户文件和远端服务的回退另按其数据恢复流程执行。
只操作实验 Profile,观察删除与回退
Profile 用 generation 保存一组已安装软件的引用。下面将它放在当前实验目录,所有命令显式传入路径,不改用户默认 Profile:
PROFILE="$PROJECT_DIR/.lab-profile"
nx profile add --profile "$PROFILE" .#default
"$PROFILE/bin/hello"
nx profile list --profile "$PROFILE"
nx profile history --profile "$PROFILE"list 输出包含条目 Name、输入与 Store 路径。Name 由安装项决定,删除时要使用本机实际显示的名字。也可以读取 JSON 输出,从本例只有一个条目的 Profile 中取得它:
nx profile list --profile "$PROFILE" --json > profile-list.json
ENTRY=$(nx develop --no-update-lock-file --command \
jq -er '.elements | keys | if length == 1 then .[0] else error("expected one entry") end' profile-list.json)
printf 'remove entry: %s\n' "$ENTRY"
nx profile remove --profile "$PROFILE" "$ENTRY"
test ! -e "$PROFILE/bin/hello"
nx profile rollback --profile "$PROFILE"
"$PROFILE/bin/hello"
nx profile history --profile "$PROFILE"删除后当前 generation 不再提供 Hello;回退后应重新打印问候语。remove 改变 Profile 的软件集合,旧 generation 仍可能引用对应 Store 对象;软件是否实际释放磁盘空间,要看所有 GC Root。不要把 nix-env 的旧式 Profile 和新 CLI Profile 混用,格式与操作入口不同。Profile 与 generation
三种使用方式可以按任务选择:一次命令用 nix shell ... --command,项目研发与 CI 用声明好的 nix develop,长期保留个人命令集才用 Profile。需要自动进出目录环境时,由 direnv 承接 Shell Hook 和授权,不必在每个终端启动文件中重复写项目依赖。
缓存连接、签名与共享 daemon
二进制缓存保存 Store 输出,减少本地编译。先读取当前配置,确认客户端实际准备连接谁:
nx config show substituters
nx config show trusted-public-keys
nx config show require-sigs
nx config show sandboxsubstituters 指定缓存地址,trusted-public-keys 指定受信签名公钥,require-sigs 控制适用对象的签名要求。HTTPS 校验与缓存签名分别验证传输端点和对象来源;遇到错误签名,不应直接关闭二者中的任意一个。先核对缓存域名、公钥指纹、证书链及发布方的轮换说明。共享 daemon 还会限制普通用户提交的配置,用户 Shell 中显示的值需要结合 daemon 端配置判断。
引入企业缓存时,由管理员审核真实端点与公钥,在 daemon 配置中追加相应项并按服务方式重载;不要复制文章中的示意公钥。trusted-users 涉及很高的 Store 操作权限,不能为了让缓存参数生效就加入所有开发账号。完整参数和权限解释见 nix.conf。
为多个项目设置缓存时,至少同时保留输入 revision、构建平台、输出 Store 路径和构建日志。内容来自受信签名方,仍可能包含该构建方引入的软件缺陷;对外发布和高风险依赖需要独立审查源码与构建链。Nix 提供对象与依赖的可追踪关系,应用测试仍需执行。
按失败阶段排查,再决定回收范围
先定位是求值、取依赖还是执行失败
| 现象 | 检查位置 | 下一步 |
|---|---|---|
attribute ... missing | Flake 输出、包属性与平台 | nx flake show;核对锁对应的属性名,必要时加 --show-trace |
| 文件明明存在却报源路径缺失 | Git 跟踪范围 | git status --short、git ls-files;确认文件确实属于项目输入 |
does not provide attribute ... | 当前 system 与输出名称 | 核对 packages/devShells 的平台键,不用 --impure 掩盖缺失输出 |
| 下载超时、TLS 或签名错误 | 网络、CA、缓存与信任配置 | 保留完整报错,分别核对端点连通性与签名来源 |
| builder 返回非零 | 构建日志、输入和检查脚本 | 使用 -L 取得日志;修复实际失败步骤后重跑 |
| Shell 里版本不对 | 当前 PATH 与项目入口 | command -v、版本命令;确认通过 nx develop 调用而非宿主旧命令 |
| Profile 回退后数据没恢复 | 应用持久化位置 | 停止写入并按应用恢复流程核对数据,Profile 只切换软件引用 |
检查本机输出名称可以做一个无副作用反例:
nx develop .#missing --no-update-lock-file --command true这里应报告没有名为 missing 的适用输出,不能进入项目命令。若实际进入了 Shell,先检查当前目录和 flake.nix,不要继续按错误输出做判断。
热缓存、离线与可重复构建的区别
先完成前面的构建与检查,再执行:
nx develop --offline --no-update-lock-file --command bash check.sh成功说明这次开发 Shell 所需输入和对象已在本地满足。它没有测试空 Store 重建;换一台机器仍可能需要拉取源码、工具闭包和构建依赖。容器中的 sandbox = false 也没有隔绝构建对外部条件的访问。
需要验证跨机器重建时,在新的专用实例上安装同版 Nix、检出同一提交与锁,用相同命令重新构建并比较实际产物。若要求逐字节一致,还要针对输出做独立校验,排除构建时间、随机数、路径或其他非确定输入。Nix 的输入描述降低了隐式依赖,但是否产生相同字节仍要由所用包的构建过程验证。
GC Root 决定对象是否还能被保留
Profile generation、构建结果链接等可以保留 Store 对象及其依赖。先查看,不立即执行回收:
nix-store --gc --print-roots
nix-store --gc --print-dead第一条列出 root,第二条列出当前可回收对象;它们不执行删除。一个旧包仍占空间时,先从 root 查到引用它的 Profile 或结果链接,而不是直接进入 /nix/store 删除目录。Store GC
在实验项目确认无需保留构建链接后,只移除本次创建的 result:
(
set -eu
test "$PWD" = "$PROJECT_DIR"
test -L result
readlink result
rm -- result
)这里删除的是项目链接,Store 输出本身是否还被其他 root 保留,需要重新查询。清理 Profile 历史会缩短可回退范围;共享机器上全局 GC 影响其他用户,执行前应先约定保留窗口和重建来源。
容器路线结束时使用 exit,Docker 会按 --rm 删除这一个实验容器的可写层,已导出的项目文件另行保留。宿主安装的卸载还涉及 daemon、系统账号、Shell 初始化文件和平台卷,不要照容器方式删除 /nix。平台卸载步骤从前面的官方安装手册进入,按实际安装模式处理。
权威资料与规范地址
安装与接口
Nix 2.35.2 Linux x86-64 归档校验文件
