❄️

Nix の固定出力 derivation (FOD)

に公開

この記事に書かれていること

  • まえがき
  • FOD を使う前に、 公式/非公式の fetcher を探そう
  • Nix の derivation を Fixed Output Derivation (FOD) にするには、 outputHashAlgo, outputHashMode, outputHash を指定する
  • 使用例
  • FOD や FOD の wrapper を扱う上での注意点

まえがき

Nix を使っている上で、「どうしてもここのビルダーで impure な操作がしたい」ということがごくまれにあるかもしれません。 99.98% のときは既存の fetcher で事足りるのですが、残りの 0.02% のときの、 "Fixed Output Derivation" (FOD) - 固定出力 derivation を直接使う方法について、日本語での情報がほぼなかったので書いておきます。

derivation が何なのかわからない人は、この記事を読んでも得るものが少ないと思うので、先に Asahi 先生の Nix 入門 を読むといいと思います。

FOD を使う前に

99.98% の問題はユーザーが FOD を直接触ることなく解決できます。 FOD に手を出す前に、以下の方法で解決できないか試してみましょう。

  • builtinslib の fetcher - fetchTarballfetchurl, fetchFromGitHub など
  • *2nix 系ツール - bun2nix など

また、仮に使うとしても、後述の制限もあるため、独自の fetcher として定義するのが良いと思います。

以下の例ではわかりやすさのため trivial なネットワークフェッチに FOD を使いますが、これはこれを推奨するものではありません。単純にファイルを持ってきたいだけであれば fetcher を使いましょう。

Derivation を Fixed-Output モードにする

普通に derivation を書こうとすると、このようになると思います。derivation のビルダーは基本的にネットワークにアクセスできないので、この derivation はビルドすることができません。

normal.nix
# 注意: ビルドできません
{pkgs ? import <nixpkgs> {}}: let
  inherit (pkgs) stdenv wget cacert;
in
  stdenv.mkDerivation {
    name = "normal-derivation";
    nativeBuildInputs = [
      wget
      cacert
    ];
    dontUnpack = true;
    buildPhase = ''
      runHook preBuild

      mkdir -p $out
      wget "https://example.com" -O $out/index.html

      runHook postBuild
    '';
  }

Fixed-Output モードを有効にするため、outputHashAlgo, outputHashMode, outputHash を指定します。これにより、ビルダーサンドボックスの制限が緩くなり、ネットワークにアクセスできるようになります。

fod.nix
{pkgs ? import <nixpkgs> {}}: let
  inherit (pkgs) stdenv wget cacert;
in
  stdenv.mkDerivation {
    name = "fixed-output-derivation";
    nativeBuildInputs = [
      wget
      cacert
    ];
    dontUnpack = true;
    buildPhase = ''
      runHook preBuild

      mkdir -p $out
      wget "https://example.com" -O $out/index.html

      runHook postBuild
    '';
    outputHashAlgo = "sha256";
    outputHashMode = "recursive"; # ディレクトリのときは "recursive", ファイルのときは "flat" と指定
    outputHash = ""; # 最初は不明なので空にして、 Nix に調べさせる
  }
$ nix-build ./fod.nix
error: hash mismatch in fixed-output derivation '/nix/store/v2825b7b6rpksfi9nsji8w7b5v5w1l2n-fixed-output-derivation.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-7Sqe1vePBqur6jFcJ6mejWyx4CGVOI9jy52fOT8DOWA=

と出るので、ハッシュを正しい値に設定します。

fod.nix
-  outputHash = "";
+  outputHash = "sha256-7Sqe1vePBqur6jFcJ6mejWyx4CGVOI9jy52fOT8DOWA=";

これで、Fixed-Output モードの derivation をビルドすることができます。

$ nix-build ./fod.nix
/nix/store/fhxk6vsa7h1l10fa99c2d7iwbin4h3ay-fixed-output-derivation

$ ls result
index.html

$ cat result/index.html
<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>

使用例

builtins を除くほとんどの fetcher がこれで実装されています。例えば pnpm.fetchDeps は、簡略化すると以下のような実装です。 (長いので畳みます)

pnpm.fetchDeps
pkgs/development/tools/pnpm/fetch-deps/default.nix
# (modified) source: https://github.com/NixOS/nixpkgs/blob/c8d4dabc4357a22d1c249a9363998bdb00122544/pkgs/development/tools/pnpm/fetch-deps/default.nix
{
  lib,
  stdenvNoCC,
  jq,
  moreutils,
  cacert,
  pnpm,
  yq,
}: {
  fetchDeps = lib.makeOverridable (
    {
      hash ? "",
      pname,
      ...
    }:
      stdenvNoCC.mkDerivation (
        finalAttrs: {
          name = "${pname}-pnpm-deps";

          nativeBuildInputs = [
            cacert
            jq
            moreutils
            pnpm
            yq
          ];

          installPhase = ''
            runHook preInstall

            export HOME=$(mktemp -d)

            pnpm config set store-dir $out
            pnpm install \
                --force \
                --ignore-scripts \
                --registry= \
                --frozen-lockfile

            runHook postInstall
          '';

          fixupPhase = ''
            runHook preFixup

            # Remove timestamp and sort the json files
            rm -rf $out/{v3,v10}/tmp
            for f in $(find $out -name "*.json"); do
              jq --sort-keys "del(.. | .checkedAt?)" $f | sponge $f
            done

            runHook postFixup
          '';

          dontConfigure = true;
          dontBuild = true;
          outputHashMode = "recursive";
          outputHash = hash;
          outputHashAlgo = "sha256";
        }
      )
  );
}

他にも、専用のパッケージマネージャがあり、そのパッケージマネージャにダウンロードさせたい場合 (\fallingdotseq直接依存をダウンロードすることが難しい場合) に使うのがいいでしょう。 (たとえば: npm、 cargo、go mod、git clone など)

注意点

1. ストアにすでにあるとビルドをスキップする

FOD は同じストアパスの derivation がストアにすでにあると、入力が変わっていてもビルドをスキップします。
fetchers についても同様のことが言えるため、 fetchurl などで URL を変更したあとは必ず hash を一旦空にして新しく fetch させる必要があります。そうしないと、デバイス間でビルドが成功したり、hash mismatch で失敗したりと、挙動に差が出てしまいます。

2. ハッシュの整合性チェックは行われない

1 により、Nix は FOD の出力する derivation が固定であるかのチェックは行いません。そのため、こんな derivation でもハッシュを指定するとビルドが通ってしまいます。これももちろん別デバイスでのビルドは通らないので、デバイス間の挙動差になります。

random.nix
{pkgs ? import <nixpkgs> {}}:
pkgs.stdenv.mkDerivation {
  name = "random";
  dontUnpack = true;
  buildPhase = ''
    echo $RANDOM > $out
  '';

  outputHashAlgo = "sha256";
  outputHashMode = "flat";
  outputHash = "";
}

この例はハッシュが揺れることが自明ですが、これ以外にも JSON のキー順が揺れたり、出力に時間が含まれるなど色々な non-determinism が存在しえます。その場合は手動で出力を揃える必要があります。 (先の pnpm.fetchDeps では jq --sort-keys で揃えていますね。)

また、https://example.com/releases/latest のような時間依存のソースからのフェッチもできてしまいます。これをするとビルドが時間依存になってしまうので、これも避けるようにしましょう。

3. 他の derivation に依存できない

FOD はほかの derivation への参照を持つことはできません。具体的には、 derivation のパスを含むファイルを FOD に含めることができません。 ELF などで必要な場合は、ソースの取得とパッチの二段階に分けて derivation を作るのがいいでしょう。

deps.nix
# 注意: ビルドできません
{pkgs ? import <nixpkgs> {}}: let
  inherit (pkgs) stdenv which hello;
in
  stdenv.mkDerivation {
    name = "fod-deps";
    nativeBuildInputs = [
      which
      hello
    ];
    dontUnpack = true;
    buildPhase = ''
      runHook preBuild

      mkdir -p $out
      echo $(which hello) > $out/hello-path.txt

      runHook postBuild
    '';
    outputHashAlgo = "sha256";
    outputHashMode = "recursive";
    outputHash = "";
  }
$ nix-build ./deps.nix
error: fixed-output derivations must not reference store paths: '/nix/store/b5jk6ba2ab1x6aqch7fz5kfyh1v3905c-fod-deps.drv' references 1 distinct paths, e.g. '/nix/store/2bcv91i8fahqghn8dmyr791iaycbsjdd-hello-2.12.2'

あとがき

初記事です。
不正確な点・わかりにくい点あればぜひコメントしてください!

Discussion