Nix 构建与 CI:GitHub Actions 集成与 Cachix 缓存

把 Nix 接入 CI 的最大价值是「一次构建、处处复用」——通过 Cachix 等二进制缓存,push 和 PR 可以共享完全相同的 store 路径。本文详解 GitHub Actions 集成 Nix、Cachix 推送/拉取、derivation 缓存复用与远程构建,附完整可运行工作流。

1. 为什么 CI 里用 Nix?

传统 CI(apt/yarn/pip 安装依赖)的问题:每次跑都是重新下载、版本漂移、缓存不可控。Nix 改变这一切的核心在于:

  • 内容寻址的 store 路径:/nix/store/<hash>-name-version,hash 由全部构建输入(源码、依赖、编译参数、环境变量)决定
  • 二进制缓存:只要 hash 相同,构建结果可直接从缓存拉取,跳过构建
  • flakeref 可复现:flake.lock 锁定输入,CI 与本地构建完全一致

所以 Nix + CI 的价值不是「构建更快」(首次构建可能更慢),而是**「缓存命中时的秒级复用」**——这正是 Cachix 等缓存服务存在的意义。本文与 https://plumephp.com/nix-flakes-best-practices/ 的仓库布局配合,把发布流水线做成「本地构建一次 → 推缓存 → CI 秒级拉取」。

📌 相关专题:GitHub Actions 基础见 https://plumephp.com/posts/github-actions/;DevOps 整体流程见 https://plumephp.com/posts/devops/。


2. derivation 缓存复用:理解 hash 与引用

2.1 store 路径是怎么算出来的

nix build .#default 的输出路径由 NAR hash 决定;而每一步 derivation 的 hash 取决于:

输入 hash = hash( 所有 src 文件内容 + 依赖的 store 路径 + builder 脚本 + 环境变量 + 平台 )

因此要让缓存命中,必须保证这些完全一致:

# 查看 derivations 之间的依赖引用(closure)
nix path-info --all ./result | head

# 查看闭包总大小(决定缓存体积)
nix path-info --closure-size ./result

# 对比两个 result 是否相同(hash 相同即内容相同)
nix path-info -sh ./result

2.2 什么会导致缓存 miss

因素说明应对
src 变化任何源码改动都换 hash正常行为,Push 级缓存覆盖 PR 级
依赖升级nix flake update 改输入CI 构建后立即推缓存
时间戳/路径漂移构建内嵌绝对路径、动态生成版本号禁用 SOURCE_DATE_EPOCH 之外的非确定性
不确定构建网络下载、随机数、未固定顺序用 sandbox 构建保证确定性
平台x86_64-linux 与 aarch64-linux 是不同路径多架构各自推缓存

2.3 让构建可缓存的小技巧

# 包内避免使用非确定的时间戳
stdenv.mkDerivation {
  pname = "app";
  version = "1.2.0";
  # 固定构建时间(可复现构建)
  SOURCE_DATE_EPOCH = "0";
  # 显式列出所有环境依赖,避免隐式污染
  nativeBuildInputs = [ pkg-config cmake ];
  buildInputs = [ openssl ];
}

3. Cachix:二进制缓存的推送与拉取

3.1 Cachix 是什么

Cachix 是托管二进制缓存服务,URL 形如 https://<name>.cachix.org。任何 Nix 实例配置了该 substituter 后,都能在构建前尝试拉取缓存闭包:

# 安装 cachix 客户端
nix profile install nixpkgs#cachix

# 登录并创建/激活缓存
cachix use mycache          # 自动写入 nix.conf 的 substituters
cachix push mycache ./result

对应的 nix.conf 配置(cachix use 会自动写入,也可手动):

# /etc/nix/nix.conf 或 ~/.config/nix/nix.conf
experimental-features = nix-command flakes
substituters = https://cache.nixos.org https://mycache.cachix.org
trusted-public-keys = mycache.cachix.org-1:xxx... cache.nixos.org-1:xxx...

3.2 push 的粒度:整闭包 vs 单路径

# 推送单个路径及其完整闭包(默认行为)
cachix push mycache ./result

# 推送时排除已存在的(增量推送,CI 中省时)
cachix push mycache --skip-existing ./result

# 推送 derivation 依赖树中「尚未被缓存」的中间产物
# 让 PR 也能命中大部分依赖缓存
cachix push mycache $(nix build .#default --no-link 2>/dev/null; nix path-info -r .#default)

3.3 缓存命中验证

# 查看从缓存拉取的情况(日志中会打印 substituter 来源)
nix build .#default --show-trace 2>&1 | grep -E "downloaded|will be built"
# 输出 "1 store paths downloaded" 表示命中;"will be built" 表示需要构建

# 查看已配置的 substituters
nix config show substituters

4. GitHub Actions 集成:完整工作流

4.1 基础骨架:安装 Nix + 拉缓存 + 构建

# .github/workflows/build.yml
name: Nix Build

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v30
        with:
          # 显式开启 flakes
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Set up Cachix cache
        uses: cachix/cachix-action@v15
        with:
          name: mycache
          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
          # pushFilter 控制哪些路径推缓存
          pushFilter: '.*(myapp|mylib).*'

      - name: Build
        run: nix build .#default --accept-flake-config

      - name: Run checks
        run: nix flake check --accept-flake-config

4.2 PR 与 push 的分层缓存策略

事件缓存角色目标
push 到 main生产者:构建并推全量缓存让所有依赖闭包进 Cachix
pull_request消费者:拉缓存快速验证命中率达 90%+,秒级完成
手动 workflow_dispatch全量重建 + 校验清理可疑缓存
jobs:
  build:
    strategy:
      matrix:
        # 多架构:x86_64 与 aarch64 各跑一个 job
        system: [x86_64-linux, aarch64-linux]
    runs-on: ubuntu-latest
    steps:
      - name: Setup QEMU (for cross)
        if: matrix.system == 'aarch64-linux'
        uses: docker/setup-qemu-action@v3
      - name: Install Nix
        uses: cachix/install-nix-action@v30
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes
      - name: Build for ${{ matrix.system }}
        run: |
          # 通过 --system 强制目标平台构建(需要远程构建器或 emulation)
          nix build ".#packages.${{ matrix.system }}.default" --system ${{ matrix.system }}

4.3 并发安全:缓存写冲突

多个 job 同时 cachix push 相同路径是无害的(内容寻址,幂等)。但要注意:

  • 多个 PR 构建相同输入时会争抢构建锁;用 concurrency 限制同 PR 的重复构建
  • cachix-action 的 skipUsingPushFilter / skipPush 可用于「只读 job」
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

5. 远程构建:把重活交给专用构建机

5.1 为什么需要远程构建

CI runner(如 GitHub-hosted)构建 Nix 有天然劣势:CPU 共享、内存受限、每次全新环境。大型 derivation(LLVM、浏览器引擎)动辄数十分钟。把构建转发到专用 Linux 构建机,是工程化标配。

5.2 本地配置远程构建器

# /etc/nix/nix.conf
# 把指定机器加入构建池
builders = ssh://builder1.mydomain.com aarch64-linux ssh://builder-arm.mydomain.com
builders-use-substitutes = true
# 可选:构建结果的 substituter 优先
# 测试远程构建
nix build .#default --max-jobs 0 --builders 'ssh://builder1.mydomain.com x86_64-linux'
# --max-jobs 0 表示不在本地构建,全部转发

5.3 GitHub Actions + remote builder

GitHub Actions 无法直接「持有」你的远程构建机,但可以:

  1. 在本地/自建 runner 上跑 Nix job(self-hosted runner)
  2. 或把构建机暴露为 substituter:远程构建机构建完推 Cachix,CI 从 Cachix 拉取

最省事的组合是方案 2——CI 永远只做「拉缓存 + 轻量验证」:

- name: Install Nix
  uses: cachix/install-nix-action@v30
  with:
    extra_nix_config: |
      experimental-features = nix-command flakes
      substituters = https://cache.nixos.org https://mycache.cachix.org

6. 缓存命中率调优:让 CI 快 10 倍的工程细节

6.1 拆细 derivation,放大缓存粒度

把「大包」拆成多个可独立缓存的小 derivation,改动局部时缓存命中率骤升:

# 反模式:一个巨大 derivation
stdenv.mkDerivation {
  pname = "monolith";
  src = ./src;
  # 任何源码改动都导致全量重建
}

# 正模式:分层缓存
{ lib, stdenv, fetchurl }:
let
  # 层 1:锁定不变的第三方依赖(几乎永远命中缓存)
  vendor = stdenv.mkDerivation {
    pname = "app-vendor";
    src = fetchurl { url = ".../vendor.tar.gz"; sha256 = "..."; };
  };
  # 层 2:业务代码(小改动 → 只重建这一层)
in stdenv.mkDerivation {
  pname = "app";
  src = ./src;
  # 通过 buildInputs 引用 vendor,形成依赖链
  buildInputs = [ vendor ];
  # 缓存上会保留两个独立路径
}

6.2 优先推送「依赖闭包」而非「单个结果」

CI 推缓存时如果只推 ./result,PR 场景下的中间依赖(CMake、编译器、库)仍然要重新构建。推整棵依赖闭包能显著提升命中率:

- name: Push full closure
  run: |
    nix build .#default --no-link
    cachix push mycache $(nix path-info -r .#default | tr '\n' ' ')

6.3 定期全量刷新

当 nixpkgs 大版本更新时,缓存中的大量路径失效。安排每周/每双周的「全量重建 + 推送」job,把热路径重新预热:

- name: Weekly cache refresh
  if: github.event_name == 'schedule'
  run: |
    nix flake update
    nix build .#default
    cachix push mycache $(nix path-info -r .#default | tr '\n' ' ')

7. 多平台矩阵:cross 与远程构建协同

跨架构构建详见 https://plumephp.com/nix-cross-compilation-overlay/,这里给出 CI 层面的组合拳:

jobs:
  build-matrix:
    strategy:
      fail-fast: false
      matrix:
        include:
          - system: x86_64-linux
            runner: ubuntu-latest
          - system: aarch64-linux
            runner: [self-hosted, linux, arm64]   # 原生 ARM runner
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
      - name: Install Nix
        uses: cachix/install-nix-action@v30
      - name: Build
        run: nix build .#packages.${{ matrix.system }}.default
      - name: Push cache
        uses: cachix/cachix-action@v15
        with:
          name: mycache
          authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

8. 常见问题与排障

症状原因解法
CI 一直显示 will be builtsubstituter 未配置或 key 不信任cachix use + 检查 trusted-public-keys
error: substituter ... returned store path ... which does not exist推送的是符号链接或未闭合推 $(nix path-info -r ...) 整闭包
缓存巨大、push 很慢闭包包含大量 nixpkgs 全量依赖pushFilter 只推项目相关路径
构建「在别处成功、CI 失败」环境差异(locale、PATH、网络)用 sandbox = true + builders-use-substitutes
权限错误 401 UnauthorizedCachix token 失效secrets 里更新 CACHIX_AUTH_TOKEN
nix flake check 很慢checks 构建了全量测试拆分 checks,PR 只跑轻量集

9. 总结

Nix + CI 的本质是把「构建」变成「缓存查找」:

  • 可复现:flake.lock + 内容寻址 store 让 Push 与 PR 构建同一路径
  • 可复用:Cachix 提供二进制缓存,闭包级推送让命中率最大化
  • 可分层:push 生产缓存、PR 消费缓存、schedule 预热缓存
  • 可扩展:远程构建器 + --max-jobs 0 把重活移出 CI 沙箱

落地路径:先搭好 https://plumephp.com/nix-flakes-best-practices/ 的模块化布局 → 用本文的 workflow 接入 GitHub Actions 与 Cachix → 配合 https://plumephp.com/nix-cross-compilation-overlay/ 扩展到多架构。至此,你的「一次构建、处处复用」流水线就成型了。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力