HN 日本語サマリー

← 一覧へ戻る
プログラミング

サイコロを振るビルドグラフ

A build graph that rolls dice (fzakaria.com)

7 pointsby ingve2 コメント

要約

この記事では、Nixの「動的導出」という概念を探求し、ビルドグラフがビルドの進行中に定義される「モナディック」な性質を持つことを説明しています。これは、ビルドの次のステップが以前のステップの結果に依存できることを意味し、サイコロを振ってビルドの深さをランダムに決定する例を用いて、その動的な性質をデモンストレーションしています。

全文翻訳

NixCon 2026がもうすぐ開催されます。今年も私は残念ながら参加できません。ラインナップを見てみると、動的導出に関するいくつかのトークがあり、最近のカーゴ・ダインドライブのような他の投稿もあり、このトピックを以前の投稿以来再訪することを考えていました。動的導出の意味と、それらがビルドグラフをどのように変えるかをよりよく理解したいと思いました。当初は、それがlang2nixスタイルのビルドグラフ生成をどのように置き換えるかに焦点を当てていましたが、その影響はそれよりもはるかに広範であることに気づきました。グラフは事前に知られている必要はありません。ビルドの進行中に定義できます。🧐 どういう意味でしょうか?伝統的に、Nixは何かをビルドするように要求し、それが「導出」として事前に正確なステップを教えてくれます。これはビルドグラフを構成します。$ nix-store -q --requisites $(nix-instantiate '<nixpkgs>' -A hello) | grep -c '\.drv$' 196 これはNixの重要な特性です。実行せずにビルドについて推論することを可能にし、何がビルドされるかを理解することで可能になります。多くのNixツールは、nix build --dry-runを介してこのプロパティに依存しています。例えば、nix-diffは、ビルドせずに2つのクロージャの違いを教えてくれます。動的導出がこれを変えるのはなぜでしょうか?§Applicative、monadic、bind 関数型言語では、「applicative」や「monadic」のような派手な言葉を使って計算の種類の違いを説明するのは非常に簡単です。違いは微妙ですが、深遠です。Nixは伝統的に「applicative」ビルドシステムでした。動的導出はそれを「monadic」ビルドシステムにします。違いは、applicativeビルドシステムでは、ビルドグラフ全体が事前に知られているのに対し、monadicビルドシステムでは、ビルドの進行中にグラフが定義できることです。11このトピックに関するデファクトスタンダードの読み物は、Build Systems à la Carteという論文です。違いを考える1つの方法は、それらを定義する2つの操作の型シグネチャを見ることです: <*> apply :: f (a -> b) -> f a -> f b >>= bind :: m a -> (a -> m b) -> m b apply (applicative) は、事前に知られている値 f a を受け取り、結果 f b を返します。実行前にグラフ全体を見ることができます。bind (monadic) は、関数 (a -> m b) を受け取り、m b を返します。実行前にグラフ全体を見ることはできません。Applicativeは、家を出る前に買い物リストを書くことと考えることができます。レシピを読み、すべての材料を書き留め、一度店に車で行きます。リストはレシピの関数であり、それ以外のものではありません。Monadicは、ステップに「味見して、塩辛すぎたらポテトを買ってきて」というレシピです。事前にその買い物リストを書くことはできません。ポテトがリストにあるかどうかは塩加減に依存し、塩加減はすでに調理の一部を行ってからでないと存在しません。Nixはかつては前者のみでしたが、動的導出は後者を追加します。§実際、Nixには常にbindがありました はい、おそらく「Nixはスケジューラにおいてモナディックになった」と言うべきでした。Nixには常に評価器にbindがありました。私たちは何年もモナディックビルドを行ってきました。私たちはそれを「導出からのインポート」と呼んでいます。let inner = pkgs.runCommand "inner" {} "sleep 10; echo hi > $out"; in pkgs.runCommand "outer" {} "echo ${builtins.readFile inner} > $out" 導出出力に対するbuiltins.readFileは、まさに上記の意味でのbindです。次に何をビルドするかは、まだ存在しない値の関数です。Nix評価器は、その値なしではグラフを生成できません。そして、それを取得する唯一の方法は、評価を停止してビルダーを実行することです。残念ながら、これは多くのフットガンを引き起こし、nix-instantiateに10秒かかる原因となり、なぜnixpkgsがこのテクニックを完全に禁止しているのかという理由になります。一方、導出からのインポートは評価器におけるbindですが、動的導出は現在スケジューラにbindを追加します。違いは、スケジューラがビルダーを並列に実行でき、リモートマシンに送信でき、キャッシュから結果を置き換えることができることです。評価器はそれらのどれもできません。動的導出はbindを追加しません。bindは常にそこにありました。変わったのは、それを実行するレイヤーです。🤓§サイコロを振ってみましょう 動的導出の多くの例は、lang2nixツールを介したビルドグラフ生成に焦点を当てていましたが、より一般的な意味で動的導出の影響を探求したいと思いました。真のモナディックな意味では、ビルドグラフはビルドの進行中に定義できます。ビルドグラフの次のステップは、前のステップの結果に依存する可能性があります。ここに非常にシンプルな例、chain.nix & step.shがあります。これはサイコロを振り、停止するか、新しい導出ステップをビルドグラフに追加してチェーンを続行します。チェーンの深さはランダムであり、ビルドの結果は到達した深さです。nix-instantiateを実行するときに存在する唯一のボックスは左端のボックスです。それより右側にあるものはすべてビルダーによって書き込まれ、ビルドはすでに進行中です。cluster_eval in chain.nix cluster_run written by step.sh, mid-build step step-N roll a d6 res dice-result echo N > $out step->res six pass passthrough-N cp $inner $out step->pass anything else next step-N+1 depth + 1 pass->next input: ^out^out more … next->more Show chain.nix { depth ? 1 , pkgs ? import <nixpkgs> { } , bash ? pkgs.bash , coreutils ? pkgs.coreutils , nix ? pkgs.nixVersions.latest , chain ? ./chain.nix , step ? ./step.sh }: let roller = derivation { name = "step-${toString depth}.drv"; system = builtins.currentSystem; builder = "${bash}/bin/bash"; args = [ "-e" "${step}" ]; DEPTH = toString depth; BASHPKG = "${bash}"; COREUTILS = "${coreutils}"; NIXPKG = "${nix}"; CHAIN = "${chain}"; STEP = "${step}"; PATH = "${coreutils}/bin:${nix}/bin"; # The builder instantiates derivations, so it needs a store to talk to. requiredSystemFeatures = [ "recursive-nix" ]; # This derivation's output is a .drv file. That is what makes it dynamic. __contentAddressed = true; outputHashMode = "text"; outputHashAlgo = "sha256"; }; in builtins.outputOf roller.outPath "out" Show step.sh set -eu export NIX_CONFIG='experimental-features = nix-command ca-derivations dynamic-derivations' roll=$(( $(od -An -N1 -tu1 < /dev/urandom) % 6 + 1 )) echo "depth $DEPTH rolled a $roll" if [ "$roll" -eq 6 ]; then # Six. Stop. The answer is how deep we got. cat > answer.nix <<NIX let bash = builtins.storePath $BASHPKG; in derivation { name = "dice-result"; system = builtins.currentSystem; builder = "ackslash{bash}/bin/bash"; args = [ "-c" "echo $DEPTH > $out" ]; __contentAddressed = true; outputHashMode = "recursive"; outputHashAlgo = "sha256"; } NIX else # Not a six. My answer is whatever the next level answers. cat > answer.nix <<NIX let bash = builtins.storePath $BASHPKG; coreutils = builtins.storePath $COREUTILS; # This is the bind. Asking chain.nix for the next depth neither builds it # nor evaluates it here: it yields a placeholder standing for "whatever # depth $(( DEPTH + 1 )) eventually answers". inner = import (builtins.storePath $CHAIN) { inherit bash coreutils; depth = $(( DEPTH + 1 )); nix = builtins.storePath $NIXPKG; chain = builtins.storePath $CHAIN; step = builtins.storePath $STEP; }; in derivation { name = "dice-passthrough"; system = builtins.currentSystem; builder = "ackslash{bash}/bin/bash"; args = [ "-c" "cp $inner $out" ]; PATH = "$coreutils/bin"; inherit inner; __contentAddressed = true; outputHashMode = "recursive"; outputHashAlgo = "sha256"; } NIX fi cp "$(nix-instantiate answer.nix)" "$out" この導出の一般的な考え方は次のとおりです。サイコロを振って6が出たら、深さをエコーする導出を書き込みます。通常、動的な要素はありません。これはビルドグラフを終了させます。それ以外の場合は、パススルーを書き込みます。これは、innerがimport chain.nix { depth = n + 1; }である導出であり、その唯一のジョブはcp $inner $outです。どちらの場合も、$outにコピーされるファイルは.drvファイルなので、builtins.outputOfは停止したチェーンと継続したチェーンの両方で同じように機能します。22実際には、無限再帰を回避し、導出のストアパスが異なるようにするために(それらはコンテンツアドレス指定されているため)、深さパラメータが必要です。