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 に手を出す前に、以下の方法で解決できないか試してみましょう。
-
builtinsやlibの fetcher -fetchTarball、fetchurl,fetchFromGitHubなど -
*2nix系ツール -bun2nixなど
また、仮に使うとしても、後述の制限もあるため、独自の fetcher として定義するのが良いと思います。
以下の例ではわかりやすさのため trivial なネットワークフェッチに FOD を使いますが、これはこれを推奨するものではありません。単純にファイルを持ってきたいだけであれば fetcher を使いましょう。
Derivation を Fixed-Output モードにする
普通に derivation を書こうとすると、このようになると思います。derivation のビルダーは基本的にネットワークにアクセスできないので、この derivation はビルドすることができません。
# 注意: ビルドできません
{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 を指定します。これにより、ビルダーサンドボックスの制限が緩くなり、ネットワークにアクセスできるようになります。
{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=
と出るので、ハッシュを正しい値に設定します。
- 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
# (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";
}
)
);
}
他にも、専用のパッケージマネージャがあり、そのパッケージマネージャにダウンロードさせたい場合 (
注意点
1. ストアにすでにあるとビルドをスキップする
FOD は同じストアパスの derivation がストアにすでにあると、入力が変わっていてもビルドをスキップします。
fetchers についても同様のことが言えるため、 fetchurl などで URL を変更したあとは必ず hash を一旦空にして新しく fetch させる必要があります。そうしないと、デバイス間でビルドが成功したり、hash mismatch で失敗したりと、挙動に差が出てしまいます。
2. ハッシュの整合性チェックは行われない
1 により、Nix は FOD の出力する derivation が固定であるかのチェックは行いません。そのため、こんな derivation でもハッシュを指定するとビルドが通ってしまいます。これももちろん別デバイスでのビルドは通らないので、デバイス間の挙動差になります。
{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 を作るのがいいでしょう。
# 注意: ビルドできません
{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