Nix — 把每个软件包当成纯函数的输出
是什么
Nix 是一个把”装软件”当成纯函数来做的包管理器。日常类比:传统包管理器像把书堆在同一书架上,新书覆盖旧书;Nix 像图书馆给每本书一个唯一编号——要哪本就按编号取,永不覆盖。
每个包的最终路径长这样:
/nix/store/abc123...xyz-python-3.11.4/前面那串 32 字符是哈希,由”源码 + 依赖 + 构建脚本”算出来。输入一变,哈希就变,路径就变。所以同一台机器上 Python 2.7 / 3.9 / 3.11 可以同时存在,互不干扰。
为什么重要
不理解 Nix,下面这些事都没法解释:
- 为什么 NixOS 用户敢说”升级翻车?切回上一个 generation,1 秒搞定”
- 为什么 10 年前的 Nix 表达式今天还能 build 出 bit-for-bit 一样的二进制
- 为什么
nix-shell -p python3 nodejs ffmpeg能凭空给你一个干净 shell,退出后不留痕 - 为什么函数式思想能从类型推导(HM)一路扩散到部署、构建、CI
核心要点
Nix 的整套设计可以拆成 三件事:
-
Store path:每个包住在
/nix/store/<哈希>-<名字>下,只读。哈希由所有输入(源码 + 依赖 + 构建脚本 + 编译参数)决定,类比”内容寻址”——和 Git 的 object id、IPFS 的 CID 一个思路。哈希一变路径一变,同名不同版本不打架。 -
Derivation(.drv 文件):描述”如何造一个包”的纯函数定义——输入是什么、构建脚本是什么、输出名字叫什么。Nix 求值器读完 .drv 就能算出最终 store path 哈希,连构建都不用真跑(如果 binary cache 里已经有结果就直接拉)。
-
Profile / generation:你当前在用的包集合。每次安装/升级/切换都生成一个新版本号(generation),旧版本不删。回滚就是把
~/.nix-profile这个符号链接指回上一代。整个系统都在符号链接的迷宫里运作。
三件事加起来:同输入同输出 + 不可变 + 多版本共存。这就是”纯函数式部署”——你输入一份描述,Nix 像执行纯函数一样产出确定结果。
实践案例
案例 1:回滚一秒钟
nix-env -u # 升级所有包(万一翻车了)nix-env --rollback # 切回上一个 generation底层只是把 ~/.nix-profile 这个符号链接指回旧 store path。没有 reinstall,没有降级——旧版本的文件本来就还在。
案例 2:临时 shell
nix-shell -p python3 nodejs ffmpeg进去之后这三样都在 PATH 里。exit 退出,系统干干净净——这些包没”装”过,只是临时挂进了 PATH。一次性脚本特别好用。
案例 3:声明式系统配置(NixOS)
{ services.postgresql.enable = true; environment.systemPackages = with pkgs; [ vim git ]; users.users.alice = { isNormalUser = true; };}nixos-rebuild switch 一下,整台机器达到这个状态。重装系统?把这个文件 copy 过去再 rebuild,机器一字不差——包括服务、用户、内核参数全部一致。这是把”系统状态”也当成纯函数输出的极端做法。
案例 4:Nix 怎么算哈希
输入:python-3.11 源码 tarball + glibc 的 store path(已经是哈希) + openssl 的 store path(已经是哈希) + 构建脚本 default.nix ↓ SHA-256输出:/nix/store/h7q8...x9-python-3.11.4所有输入都已经被哈希过,所以整条依赖图都是内容寻址。修改 openssl 一个字符,python 的哈希也变——这叫”传染性重建”,缺点是改底层库要重 build 半个世界,优点是依赖污染零可能。
踩过的坑
-
学习曲线很陡:Nix 表达式语言是另一门函数式语言。新手第一次看到
pkgs.callPackage ./foo.nix { }配上let ... in直接劝退。准备好啃几天文档。 -
磁盘占用大:多版本共存的代价是
/nix/store几十 GB 起步。要定期nix-collect-garbage回收没在用的旧版本。 -
Flakes 是 experimental,但教程一半新一半旧:旧教程用
nix-env/nix-shell,新教程用nix flake/nix develop。新手两边乱抄会卡住——先认准一套(推荐 Flakes,是未来)。 -
macOS 安装麻烦:要单独划分一个 APFS volume 给
/nix,因为 macOS 系统盘只读不让动根目录。Determinate Systems 的安装器把这步自动化了。 -
Bus factor:团队里只有你一个人懂 Nix,你休假时同事改不动。引入前先问”这套学习成本组里愿意付吗”。
-
报错可读性差:构建失败要读
.drv和 builder 脚本,不像apt那样一句 error 就能看懂。新手常常 build 半小时然后挂在某个奇怪的 cmake flag 上。
适用 vs 不适用场景
适用:
- 多版本共存的开发环境(同一项目同时跑 Node 18 和 Node 20 测试)
- 可重现 CI(一份 flake.nix,所有机器结果一致)
- 声明式系统管理(NixOS:整机配置一份文件)
- 替代 Docker 做轻量 dev shell(启动比容器快)
不适用:
- 只想快速
brew install <thing>的日常场景——Nix 心智成本远高于 Homebrew - Windows 原生环境(只有 WSL 里能跑)
- 团队里只有你懂 Nix——一个人扛不动一套基础设施
- 闭源专有软件——传统 FHS 假设和 Nix store 路径冲突,要
buildFHSEnv包一层
历史小故事(可跳过)
- 2003 年:Eelco Dolstra 在乌特勒支大学开始博士研究,前导论文 ICSE 2004 / LISA 2004
- 2006 年:博士论文 The Purely Functional Software Deployment Model 答辩通过,Nix 命名定型
- 2008 年:NixOS 1.0 发布——一个完全用 Nix 模型搭起来的 Linux 发行版
- 2012 年:Guix 项目成立,借用 Nix 模型,把表达式语言换成 Scheme
- 2021 年:Flakes 实验性引入,目标是给 Nix 一个稳定可锁定的项目格式
- 2026 年:Flakes 仍在 experimental 状态,社区在 stable 化路上
学到什么
- 纯函数思想能跨域迁移:从类型推导(HM)到部署(Nix)到构建(Bazel),思路一致——同输入同输出换可预测性
- 内容寻址 + 不可变 是分布式系统反复出现的设计模式,Git / IPFS / Nix 同源
- 声明式 > 命令式:你描述”要什么状态”,工具负责怎么到达;比一步步
apt install可重现得多 - 好工具有代价:陡峭学习曲线 + 高磁盘占用换”回滚一秒 + 零依赖污染”——是不是值,看场景
延伸阅读
- 博士论文 PDF:Dolstra 2006(240 页,第 2 章是核心)
- 小步教程:Nix Pills(20 多 pill,从零到能写 derivation)
- 现代入门:Zero to Nix(Determinate Systems 出品,主推 Flakes)
- 官网:NixOS
- hindley-milner —— 同样是函数式思想跨域迁移:HM 推类型,Nix 推依赖
关联
- hindley-milner —— 函数式思想跨域:一个推类型,一个推依赖路径
- lambda-calculus —— Nix 表达式语言核心是 lambda + 惰性求值
- mccarthy-lisp —— 函数式祖先;Nix 的 attribute set 借鉴 Lisp 思路
- standard-ml —— 同时代另一个把”函数式 + 工程”绑到一起的尝试
反向链接
- asdf —— asdf — 一个 CLI 管 Node/Python/Ruby 等几十种版本
- dagger —— Dagger — 用真正的编程语言写 CI pipeline
- earthly —— Earthly — 把 Make 和 Dockerfile 揉一起的构建工具
- hindley-milner —— Hindley-Milner — 编译器自己猜变量类型
- homebrew —— Homebrew — macOS 上一行命令装好软件的包管理器
- just —— just — 把 make 拆成两半,只留 ‘命令编排’ 那一半
- lambda-calculus —— λ-演算 — 用三条规则表达所有可计算函数
- mccarthy-lisp —— McCarthy LISP 1960
- mise —— mise — 一条命令切换项目用的 Node/Python/Go 版本
- nix —— Nix — 把每个软件包当成纯函数的输出
- scoop —— Scoop — Windows 上的 Homebrew 风格命令行包管理器
- standard-ml —— Standard ML — 让编译器替你把类型补完
- zephyr —— Zephyr — 一份代码树跑遍所有嵌入式芯片的开源 RTOS