Nix Flake 输入管理与锁定:inputs、flake.lock 与私有源

Nix Flake 输入管理实战:inputs 声明与六种 input 类型(path/git/github/tarball/url/indirect)、flake.lock 结构逐字段解析、nix flake update 与选择性更新、follows 去重与依赖统一、私有 input 与认证(token/ssh/netrc)、--override-input 本地开发、锁文件治理与团队协作、常见坑与排错清单。

引言

Flake 的可复现性并不来自 flake.nix,而来自 inputs 与 flake.lock 的组合:inputs 声明「我依赖谁」,锁文件记录「我依赖的那个精确版本」。前者是可读的意图,后者是可复现的事实——两者分离,才让 nix build 在任何机器上产出同一结果。

本文把 inputs 讲到工程可用:六种 input 类型各自适合什么场景、flake.lock 的每个字段在说什么、如何只更新一个依赖而不动其他、follows 如何避免同一份 nixpkgs 被拉多份、私有仓库怎么带认证,以及多人协作时锁文件该怎么治理。

前置:Flakes 入门、Flakes 最佳实践、源码获取与 fetchers。


目录


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 六种类型一览

类型写法示例锁文件里记录适用场景
pathpath:/home/me/cfgnarHash + 路径本地目录、monorepo 内引用
gitgit+https://x/y.git?ref=mainrev + narHash任意 git 服务、私有仓库
githubgithub:owner/repo/branchrev + narHashGitHub 公开仓库
gitlabgitlab:owner/repo/branchrev + narHashGitLab 公开仓库
tarballhttps://x/y.tar.gznarHash固定发布的压缩包
indirectnixpkgs解析到 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解析后的精确锁定值构建实际使用
revgit 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 + tokenCI 简单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 typeflakeref 拼写错检查 github: 与 git+ 前缀
does not provide attribute 'packages...'input 不是 flake用 flake = false 声明
error: hash mismatch上游改写了内容重新 update 该 input
infinite recursioninput 互相引用成环拆出共同依赖

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」更多文章

  1. nixpkgs 贡献与维护:从 by-name 到 backport
  2. Nix 中的 CUDA 与机器学习环境:cudaPackages 与 PyTorch
  3. nix-darwin:macOS 的声明式系统配置