编程 用 Nix Flakes + Home Manager 把 dotfiles 整理成可复现环境:按 host 拆分、混合配置、更新先过 CI

2026-09-07 00:06:31

用 Nix Flakes + Home Manager 把 dotfiles 整理成可复现环境:按 host 拆分、混合配置、更新先过 CI

dotfiles 仓库用 Nix Flakes + Home Manager 做声明式环境管理,目标不只是让多台机器共享同一份包列表,而是复现出“实际能用的开发环境”。Home Manager 官方仓库在 nix-community/home-manager。以下记录这套仓库当前的结构和运维方式。

Flake 层:固定依赖,按 host 出环境

flake.nix 用来声明外部依赖:nixpkgshome-manager。环境不是一把梭地配置所有机器,而是按主机切分,例如 hal@MacBook-Pro-M4

目录结构上做两层拆分:

  • hosts/:吸收物理机差异,比如硬件设置、OS 差异。
  • config/:放应用自身的配置,也就是真正意义上的 dotfiles。

这样 flake.lock 把输入版本固定住,home-manager switch 时也只会影响当前 host,不会把另一台机器的系统相关设置带进来。

配置管理走 hybrid:不是所有东西都 Nix 化

是否把某个配置改成 Home Manager 模块,取决于“描述成本”和“开发体验”的平衡。

用 Nix 模块抽象:programs.*

shell、git 这类与 shell 环境变量、包引入关系紧密的配置,用 Home Manager 的 programs.xxx 模块写。包和配置能放在同一个抽象里,适合会被 Nix option 覆盖的场景。

保留原生配置:xdg.configFile

Neovim 的 Lua 配置、Ghostty 配置这类已有完整生态和原生写法的东西,不强行 Nix 化。直接放在 config/ 下,通过 xdg.configFile 让 Home Manager 展开成符号链接。Home Manager 对这种用法支持得很好。

真正的冲突出现在 Zed 这类会自己改写配置文件的程序上。xdg.configFile 展开出的符号链接指向 nix store 里的文件,而按 Nix 的设计,store 里的文件本来就不该被运行时修改。Zed 在变更 LLM agent 模型等设置时会回写配置文件,这和“只读符号链接”天然矛盾,所以不适合用这种方式管理。

Homebrew 集成:哈希检查 + linkGeneration 之后执行

Nix 对 GUI app 和字体管理仍然不方便,所以这里和 Homebrew 并用。

最开始的问题是:如果每次 home-manager switch 都执行一次 brew bundle,开销太大。用 home.activation 实现“Brewfile 哈希没变就跳过”:

home.activation.brewBundle = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
  BREWFILE=~/.config/homebrew/Brewfile
  HASH_FILE=~/.local/share/home-manager/brew-hash

  if [ ! -f "$BREWFILE" ]; then
    exit 0
  fi

  new_hash=$(cksum "$BREWFILE" | awk '{print $1 $2}')
  old_hash=$([ -f "$HASH_FILE" ] && cat "$HASH_FILE" || echo "")

  if [ "$new_hash" != "$old_hash" ]; then
    /opt/homebrew/bin/brew bundle --cleanup --global
    echo "$new_hash" > "$HASH_FILE"
  fi
''

关键点是 lib.hm.dag.entryAfter [ "linkGeneration" ]。这个激活脚本必须等配置文件的符号链接全部生成后再执行,因为 Brewfile 本身也在这些链接里;执行太早就会拿到旧文件。

效果是:GUI app、字体继续交给 Homebrew,Nix 生命周期内的切换开销又被哈希检查压住,剩下的是声明式同步。

CI 兜底:本机不用先踩 flake update 的坑

最需要避免的场景是:在本机跑 nix flake update,结果环境坏了,当天开发直接中断。

nixpkgs 的 unstable 分支偶尔会出现平台间依赖混入的问题。实际遇到过:d2 包的 Linux 依赖在 macOS 侧评估时被带进来,导致配置无法正常展开。

所以现在用 CI 兜底:

  • GitHub Actions 在 PR 阶段评估所有主机的 homeConfiguration,macOS、Linux 都要无错通过。
  • nix flake update 放在 CI 上跑,evaluation 和 build 都通过后才把更新后的 flake.lock 合入。
  • 本地不需要再用“先更新再试”的方式验证。

小结

Nix 的学习成本确实高,但目标是长期环境稳定的话,这套结构是合理的:Flake 固定依赖、hosts 吸收机器差异、配置层允许原生文件存在、Homebrew 只在内容变化时执行、flake.lock 更新先在 CI 上验证。

不需要一次性把全部配置 Nix 化。先从能动手的地方开始,Nix 也可以按项目为单位使用,而不只用来管理 dotfiles。

复制全文 生成海报 Home Manager Nix Flake dotfiles NixOS nix-darwin

推荐文章

程序员茄子在线接单