引言
Flake 的可复现性并不来自 flake.nix,而来自 inputs 与 flake.lock 的组合:inputs 声明「我依赖谁」,锁文件记录「我依赖的那个精确版本」。前者是可读的意图,后者是可复现的事实——两者分离,才让 nix build 在任何机器上产出同一结果。
本文把 inputs 讲到工程可用:六种 input 类型各自适合什么场景、flake.lock 的每个字段在说什么、如何只更新一个依赖而不动其他、follows 如何避免同一份 nixpkgs 被拉多份、私有仓库怎么带认证,以及多人协作时锁文件该怎么治理。
目录
- 1. inputs 是什么:flake 的依赖声明
- 2. input 类型全解:path、git、github 与 url
- 3. flake.lock 结构逐字段解析
- 4. nix flake update 与选择性更新
- 5. follows:去重与依赖统一
- 6. 私有 input 与认证
- 7. input 覆盖与本地开发
- 8. 锁文件治理与团队协作
- 9. 常见坑与排错
- 10. 速查表与一句话记忆
- 延伸阅读
1. inputs 是什么:flake 的依赖声明
1.1 一次求值的「输入闭包」
Flake 是纯函数:outputs = f(inputs)。inputs 就是这次求值的全部外部输入——nixpkgs 的某个 commit、别人的 flake、一份本地配置目录。只要 inputs 相同,outputs 必然相同。
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, home-manager }: { /* ... */ };
}
1.2 inputs 与 lock 的分工
| 文件 | 记录什么 | 谁改 |
|---|---|---|
flake.nix | 依赖的「意图」(仓库、分支、标签) | 人手写 |
flake.lock | 依赖的「事实」(commit、narHash、rev) | 命令生成 |
关键点:flake.nix 里写分支,实际用的是锁文件里的 commit。所以 nixpkgs.url = ".../nixos-24.11" 不代表每次构建都取最新——除非你显式 nix flake update。
1.3 inputs 的惰性
只有 outputs 函数签名里实际引用的 input 才会被求值。声明了但没用的 input 不会拉取(nix flake show 会提示 unused input)。这既是优化也是坑:删掉引用后忘了删声明,会留下永不更新的死依赖。
记忆:Flake 的可复现性来自 inputs(意图)+ flake.lock(事实)的组合——flake.nix 写分支、lock 记 commit,不 update 就不会变;outputs 未引用的 input 不会被拉取。
2. input 类型全解:path、git、github 与 url
2.1 六种类型一览
| 类型 | 写法示例 | 锁文件里记录 | 适用场景 |
|---|---|---|---|
path | path:/home/me/cfg | narHash + 路径 | 本地目录、monorepo 内引用 |
git | git+https://x/y.git?ref=main | rev + narHash | 任意 git 服务、私有仓库 |
github | github:owner/repo/branch | rev + narHash | GitHub 公开仓库 |
gitlab | gitlab:owner/repo/branch | rev + narHash | GitLab 公开仓库 |
tarball | https://x/y.tar.gz | narHash | 固定发布的压缩包 |
indirect | nixpkgs | 解析到 registry | 走 registry 的简写 |
2.2 path 类型的特殊语义
{
inputs = {
# 相对路径:相对本 flake 所在目录
local-lib.url = "path:./libs/mylib";
# 绝对路径:不推荐进 git,只用于本地实验
dev-cfg.url = "path:/home/me/devcfg";
};
}
path: 的内容是「目录快照」而非 git 引用:目标目录若在同一个 git 仓库里,Nix 会做 git 感知的过滤(只取 tracked 文件),这也是 monorepo 内共享库的常用手法。
2.3 git 类型与 ref 的三种写法
{
inputs = {
# 分支(会随 update 移动)
a.url = "git+https://git.example.com/team/lib.git?ref=main";
# 标签(相对稳定)
b.url = "git+https://git.example.com/team/lib.git?ref=v1.2.0";
# 精确 commit(最稳,但 update 不会动)
c.url = "git+https://git.example.com/team/lib.git?rev=9f2c1a4b";
# 子目录:仓库很大而只需要其中一个目录
d.url = "git+https://git.example.com/mono.git?ref=main&dir=pkgs/tool";
};
}
github:NixOS/nixpkgs/nixos-24.11 只是 git+https://github.com/NixOS/nixpkgs.git?ref=nixos-24.11 的简写,两者在锁文件里记录的形态完全一样(都是 rev + narHash),混用不会造成语义差异。
记忆:input 六型各有分工——path 是目录快照(monorepo 共享)、git 是通用引用(ref/rev/dir 三参数)、github/gitlab 是简写、tarball 是固定压缩包、indirect 走 registry;github 简写与 git 全写在锁文件里等价。
3. flake.lock 结构逐字段解析
3.1 顶层结构
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1730000000, "narHash": "sha256-AAAA...",
"owner": "NixOS", "repo": "nixpkgs",
"rev": "b1c2d3e4f5...", "type": "github"
},
"original": {
"owner": "NixOS", "ref": "nixos-24.11",
"repo": "nixpkgs", "type": "github"
}
},
"root": { "inputs": { "nixpkgs": "nixpkgs" } }
},
"version": 7
}
3.2 每个字段在说什么
| 字段 | 含义 | 关键性 |
|---|---|---|
original | 你在 flake.nix 里写的原始意图 | 更新时从这里重新解析 |
locked | 解析后的精确锁定值 | 构建实际使用 |
rev | git commit 哈希 | 决定「取哪个版本」 |
narHash | 该源展开后的 NAR 哈希 | 决定「内容是否被篡改」 |
lastModified | 源的最后修改时间 | 供 nix flake metadata 展示 |
type | 源的种类(github/git/path) | 决定抓取方式 |
version | 锁文件格式版本 | 跨 Nix 版本兼容 |
3.3 为什么 rev 与 narHash 都要
rev 只保证「取的是同一个 commit」,但无法防止上游改写历史(force push)或镜像不一致。narHash 是内容哈希,一旦内容对不上就直接报错——这是安全边界,也是锁文件在公开仓库可安全提交的原因:它只含 rev 与哈希,不含凭据。查看锁状态用 nix flake metadata,取某节点的精确锁定值用 nix flake metadata --json | jq '.locks.nodes.nixpkgs.locked'。
记忆:flake.lock 的 original 记意图、locked 记事实;rev 决定版本、narHash 决定内容完整——两者缺一不可;
nix flake metadata是查看锁状态的第一入口。
4. nix flake update 与选择性更新
4.1 全量更新与选择性更新
nix flake update # 重新解析所有 input,重写 flake.lock
nix flake update --commit-lock-file # 更新并自动 git commit 锁文件
# 只更新 nixpkgs,其余保持不动
nix flake update nixpkgs
# 一次更新两个
nix flake update nixpkgs home-manager
全量更新会同时把 nixpkgs、home-manager、所有社区 flake 推到最新——副作用大,通常不推荐在功能开发中间做。选择性更新是日常推荐的姿势:一次只动一个变量,出问题容易定位回滚。
4.2 更新到指定版本
# 把某个 input 钉到具体 ref
nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.05
# 之后若要回到 flake.nix 声明的 ref,重跑一次普通 update 即可
nix flake update nixpkgs
4.3 更新后必做的验证
git diff flake.lock # 看 rev/narHash 变了哪些
nix flake check # 跑 flake 自带的 checks
nix build .#默认包 -L # 完整日志构建一遍
记忆:update 要「一次一个变量」——
nix flake update <input>选择性更新、--override-input临时钉版本、全量 update 慎用;更新后先看 git diff flake.lock、再 flake check、最后完整构建。
5. follows:去重与依赖统一
5.1 问题:同一份 nixpkgs 被拉多份
home-manager 自己也声明了 inputs.nixpkgs。若不管,你会同时锁两份 nixpkgs:一份是你的、一份是 home-manager 的。后果是同一个包出现两个 store 路径,缓存命中率下降、闭包膨胀、构建时间翻倍。
5.2 follows 的写法与语义
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager";
# 让 home-manager 的 nixpkgs 跟随本 flake 的 nixpkgs
inputs.nixpkgs.follows = "nixpkgs";
};
nix-darwin = {
url = "github:LnL7/nix-darwin";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
| 写法 | 效果 |
|---|---|
inputs.nixpkgs.follows = "nixpkgs" | 子 flake 的 nixpkgs 指向本 flake 的 nixpkgs |
inputs.foo.follows = "bar/baz" | 也可以指向别的 input 的间接依赖 |
| 删除 follows 后 | 需要 nix flake update 才会真正移除多余节点 |
follows 是指向关系而非复制。锁文件里被 follow 的节点会变成对同一 node 的引用("inputs": { "nixpkgs": ["nixpkgs"] }),因此只有一个真实节点。排查是否有多余节点可数一数:jq '[.nodes | to_entries[] | select(.key | test("nixpkgs"))] | length' flake.lock,或直接 nix flake metadata 看依赖树。
记忆:follows 是去重的关键——把子 flake 的 nixpkgs 指向自己的,避免同一份 nixpkgs 被锁多次(缓存命中率与闭包大小都受益);改完 follows 记得 update 一次才真正移除多余节点。
6. 私有 input 与认证
6.1 私有 git 仓库的三种接入方式
{
inputs = {
# 方式一:ssh(依赖本机 ssh key / agent)
private-ssh.url = "git+ssh://git@git.example.com/team/lib.git?ref=main";
# 方式二:https + token(token 放 URL 里,注意不要进 git)
private-https.url = "git+https://oauth2:TOKEN@git.example.com/team/lib.git";
# 方式三:https 裸地址(由 netrc / credential helper 提供认证)
private-netrc.url = "git+https://git.example.com/team/lib.git";
};
}
| 方式 | 优点 | 风险 |
|---|---|---|
| ssh | 无需把密钥写进配置 | CI 里要额外配 agent 与 known_hosts |
| https + token | CI 简单 | token 会出现在 flake.nix 与日志中 |
| netrc | 凭据不进配置文件 | 依赖 ~/.netrc 权限与 CI 注入 |
6.2 CI 里的安全做法
# 用 netrc 注入凭据,flake.nix 保持干净
echo "machine git.example.com login oauth2 password $GIT_TOKEN" > ~/.netrc
chmod 600 ~/.netrc
# 或用 access-tokens 配置(Nix 2.13+):写进 ~/.config/nix/nix.conf
echo "access-tokens = git.example.com=$GIT_TOKEN" >> ~/.config/nix/nix.conf
私有源的 narHash 同样会被记录——锁文件可以安全提交到公开仓库,因为它只含哈希与 rev,不含凭据。但要确保凭据来源(netrc/token)在每台机器与 CI 上都可复现,否则会报 401。
记忆:私有 input 走 ssh / https+token / netrc 三选一;flake.nix 里别写 token(会进 git 与日志),CI 用 netrc 或 access-tokens 注入;锁文件只含 rev 与哈希,可安全提交。
7. input 覆盖与本地开发
7.1 override-input:不改锁文件的临时替换
# 用本地目录替换 nixpkgs,验证某个修复
nix build .#mypkg --override-input nixpkgs path:/home/me/nixpkgs
# 用本地分支替换私有库
nix develop --override-input mylib git+file:///home/me/mylib?ref=fix-bug
# 只对某条命令临时换依赖,验证兼容性后再正式 update
nix flake check --override-input nixpkgs github:NixOS/nixpkgs/nixos-unstable
--override-input 不会写入 flake.lock,只在本次命令生效——非常适合「改一个依赖看效果」。
7.2 本地 flake 作为 input 的开发循环
# 上层 flake
{
inputs.mylib.url = "path:../mylib"; # 开发期用相对路径
# inputs.mylib.url = "github:me/mylib"; # 发布期切回远程
}
path: 类型会让 Nix 直接读取磁盘内容,每次求值都是最新——改了本地库立刻生效,无需 update。
记忆:
--override-input是本地开发的利器——临时替换依赖、不写锁文件、不改 flake.nix;开发期用path:input 做「改即生效」的循环,验证通过后再切回远程 URL 并正式 update。
8. 锁文件治理与团队协作
8.1 锁文件必须提交
flake.lock 是可复现性的载体,必须进版本控制。忽略它等于放弃锁定:每个开发者各锁一份,CI 与本地环境不一致。
# 反例:千万不要把 flake.lock 加进忽略列表
# flake.lock
8.2 更新策略
| 策略 | 做法 | 适用 |
|---|---|---|
| 集中更新 | 固定周期(如每周)专人全量 update + 跑 CI | 小团队、包集稳定 |
| 按需更新 | 谁需要谁 update <input> | 大团队、活跃开发 |
| 机器人更新 | CI 定时提 PR 只更新 nixpkgs | 追求自动化的团队 |
8.3 冲突处理与审阅要点
多人同时更新锁文件必然冲突(JSON 里同一节点的 rev 不同)。处理原则:不要手工合并 JSON,选一边后重解析一次更安全。
git checkout --theirs flake.lock # 或 --ours,选一边
nix flake update # 重新生成一份自洽的锁文件
git diff flake.lock
# ☐ rev 是否变化(变了 = 依赖真的动了)
# ☐ narHash 是否变化(变了但 rev 没变 = 上游改写了内容,需警惕)
# ☐ 节点数是否异常增多(follows 被误删会导致依赖重复)
# ☐ version 字段是否被升级(跨 Nix 版本兼容性)
记忆:flake.lock 必须进 git;更新走「集中/按需/机器人」三策略之一;冲突别手工合并 JSON——选一边后重跑 update;审阅锁文件先看 rev/narHash/节点数。
9. 常见坑与排错
9.1 高频错误对照
| 报错 | 原因 | 修复 |
|---|---|---|
unable to download ... 401 | 私有源凭据缺失 | 配 netrc / access-tokens |
input 'x' has an unsupported type | flakeref 拼写错 | 检查 github: 与 git+ 前缀 |
does not provide attribute 'packages...' | input 不是 flake | 用 flake = false 声明 |
error: hash mismatch | 上游改写了内容 | 重新 update 该 input |
infinite recursion | input 互相引用成环 | 拆出共同依赖 |
9.2 排错命令与清单
nix flake metadata --json | jq . # 看完整依赖树
nix eval --raw .#nixpkgs.legacyPackages.x86_64-linux.hello.version
nix flake archive --json # 把整棵依赖树归档并列出
# ☐ 报 401:先确认凭据来源,再确认 URL 里的用户/路径
# ☐ 报 hash mismatch:多半是上游 force push,update 即可
# ☐ 构建结果与同事不一致:diff flake.lock,确认锁文件已提交
# ☐ 闭包异常大:jq 数 nixpkgs 节点,检查 follows 是否被删
# ☐ 更新后构建失败:git diff flake.lock 定位是哪个 input 动了
记忆:inputs 排错三板斧——401 查凭据来源、hash mismatch 是上游改写(update 解决)、构建不一致 diff 锁文件;非 flake 仓库用
flake = false当纯源码用。
10. 速查表与一句话记忆
| 需求 | 命令 / 写法 | 一句话 |
|---|---|---|
| 全量更新 | nix flake update | 慎用,副作用大 |
| 单个更新 | nix flake update nixpkgs | 一次一个变量 |
| 临时替换 | --override-input x path:... | 不写锁文件 |
| 去重 | inputs.x.inputs.nixpkgs.follows = "nixpkgs" | 一个 nixpkgs 就够 |
| 查看锁状态 | nix flake metadata | 依赖树一眼看 |
| 私有源认证 | netrc / access-tokens | 凭据不进 flake.nix |
| 非 flake 源 | flake = false | 当纯源码用 |
| 冲突处理 | 选一边 + 重跑 update | 别手工合并 JSON |
一句话记忆:Flake 的可复现性 = inputs(意图,写在 flake.nix)+ flake.lock(事实,rev 定版本、narHash 定内容),锁文件必须进 git 且不 update 就不变;input 六型(path/git/github/gitlab/tarball/indirect)各有分工,nix flake update <input> 选择性更新、--override-input 临时替换做本地开发;follows 把子 flake 的 nixpkgs 指向自己以去重(缓存命中率与闭包都受益);私有源用 ssh / netrc / access-tokens 注入凭据、绝不把 token 写进 flake.nix;锁文件冲突选一边后重跑 update 而非手工合并 JSON——「一次一个变量地更新,用 diff 锁文件定位变化」是 inputs 治理的核心纪律。
延伸阅读
- Nix Flakes 现代包管理
- Flakes 最佳实践
- 源码获取与 fetchers
- flake-parts 与大型仓库组织
- Nix 二进制缓存与替换器
- DevOps 专题 — 依赖治理与 CI 实践
- Nix 官方 Flake 手册
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。