- Published on
Nix 2.35がフレークのソースをstoreにあまりコピーしなくなった — 6年8ヶ月かかったイシューと、アップストリームがlazy treesの代わりに選んだ道
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — なぜ nix build はリポジトリ全体を複製するのか
- 2,437日かかったイシュー — #3121
- lazy trees — 二度の挑戦
- その間、Determinate Nixはただ出荷した
- 核心 — lazy-locks、そして完全な怠惰の代償
- Lixの答え — 直さず凍結する
- Nix 2.35が実際にしたこと
- まだ直っていないもの
- で、あなたは何をすべきか
- おわりに
- 参考資料
はじめに — なぜ nix build はリポジトリ全体を複製するのか
フレークを使うリポジトリでファイルを一つ直して nix build を回したことがあるなら、この動作を見たことがあるはずです。
$ vim README.md # 1文字だけ直す
$ nix build .#myapp
copying '/home/me/big-monorepo' to the store...
ビルドにREADMEが必要かどうかにかかわらず、ソースツリー全体が/nix/storeにコピーされます。次にもう1文字直せば、また複製されます。ツリーが数ギガバイト級のモノレポなら、これは冗談では済みません。
これがバグではなく設計だった、という点が重要です。Nix 2.35.0のリリースノートの表現をそのまま移すとこうなります — 歴史的に、フレークのソースツリーは決定論的でヘルメティックな評価を保証するため、その結果生じるstoreオブジェクトがderivationの入力として使われない場合でも、eagerにstoreへ取り込まれ、そこから評価されてきた。実装はシンプルになったが、大きなリポジトリでフレークを使えなくし、ソースツリーが変わるたびに不要な書き込みを発生させていた。
本稿は、この設計を直すのになぜ6年8ヶ月もかかったのか、Nix 2.35.0が実際に何を直したのか、そして何をまだ直していないのかについての話です。最後の項目が最も重要です。
2,437日かかったイシュー — #3121
イシューNixOS/nix#3121 "Copy local flakes to the store lazily"は2019年10月7日に開かれました。開いたのはEelco Dolstra — Nixを作った本人です。
このイシューは2026年6月9日に閉じられました。2,437日、約6年8ヶ月、コメント69件。閉じたのはDolstraではなく、閉じる際のコメントは五語でした — "Completed in #15711"。
#15711はDolstraのPRではありません。ここからが本題です。
lazy trees — 二度の挑戦
Dolstraの答えはlazy treesというものでした。発想はこうです。ソースツリーをstoreにコピーしてそこから読む代わりに、仮想ファイルシステムを一つ作り、評価器には「storeにあるかのように」見せる。そして実際にファイルが必要になったときだけ、必要な分だけ読む。nixpkgs#helloをビルドするのにnixpkgs全体をコピーする理由はありません。
PR NixOS/nix#6530 "Lazy trees"は2022年5月12日に開かれました。コミット402、ファイル56、+1363/-364まで育ちました。そして2025年5月18日、マージされないまま閉じられました。1,102日、3年でした。
閉じる前日の2025年5月17日、DolstraはPR#13225「Lazy trees v2」を開きました。ずっと小さいものです — コミット1、ファイル41、+539/-133。本稿執筆時点の2026年7月16日現在、このPRはまだ開いており、GitHubが報告するマージ可能状態はdirty(競合)、最終更新は2025年10月9日です。コメント16件、レビューコメント17件が付いたまま9ヶ月止まっています。
ここで気をつけたい点があります。PRが動かない理由を私は知りません。レビューの議論の中身を要約して「誰それが止めた」と書くのは本稿の範囲外であり、検証可能な事実でもありません。確認できるのはこれだけです — 開いていて、競合状態で、9ヶ月間更新がない。
その間、Determinate Nixはただ出荷した
DolstraはDeterminate Systemsの共同創業者です。そしてDeterminate Systemsは自社のNixディストリビューションであるDeterminate Nixを出しています。
2025年5月15日、Determinate SystemsのLuc Perkinsがlazy treesがDeterminate Nix 3.5.2に入ったと発表しました。アップストリームのNix 2.28.3をベースにしており、最初はオプトインでした。
# /etc/nix/nix.custom.conf
lazy-trees = true
同じ記事にはこうあります — 「私たちの共同創業者Eelco Dolstraが現在、lazy treesをアップストリームNixに入れるためのPRを開いており、間もなくマージされることを願っています」。そのPRが#13225です。1年2ヶ月経った今も開いたままです。
2ヶ月後の2025年7月11日、Graham ChristensenがDeterminate Nix 3.8.0を発表し、前回のリリースでDeterminate Nixユーザーの5パーセントにlazy treesをロールアウトし、5パーセントのロールアウトが成功したので今度は20パーセントへ拡大する、と書きました。
オープンソースのパッケージマネージャーをパーセンテージ段階ロールアウトで配布する — これはかなり新しい光景です。ユーザーはdeterminate-nixd versionを打てば自分がその20パーセントに入っているか確認でき、同じ記事にはオプトアウトしたい場合はsupport@determinate.systemsへ連絡するようにと書かれています。
性能数値もあります。ただしこれはベンダー自己測定です。3.5.2の発表記事は、Nixpkgsリポジトリ内での評価を例に、「wall timeで3倍以上の削減とディスク使用量で20倍以上の削減をよく見た(frequently seen)」と書き、「時にはこれよりずっと大きな削減」もあったと付け加えています。ここで欠けているものをそのまま書いておきます — ハードウェア、測定方法、比較対象のバージョン、何回の試行か、「よく」が何パーセントかがすべてありません。方向性は信用できますが、あの倍率をあなたのリポジトリにそのまま当てはめてはいけません。
核心 — lazy-locks、そして完全な怠惰の代償
ここからがこの話の技術的な核心です。Determinate Nixのソース(v3.21.7時点)の設定定義を見ると、二つが並んでいます。
Setting<bool> lazyTrees{
this, false, "lazy-trees",
R"(
If set to true, flakes and trees fetched by builtins.fetchTree are only
copied to the Nix store when they're used as a dependency of a derivation.
This avoids copying (potentially large) source trees unnecessarily.
)"};
Setting<bool> lazyLocks{
this, false, "lazy-locks",
R"(
If enabled, Nix only includes NAR hashes in lock file entries if they're
necessary to lock the input (i.e. when there is no other attribute that
allows the content to be verified, like a Git revision).
This is not backward compatible with older versions of Nix.
If disabled, lock file entries always contain a NAR hash.
)"};
lazy-locksのドキュメントにあるあの一文 — "This is not backward compatible with older versions of Nix" — がすべてを説明しています。
論理はこうです。flake.lockの各エントリにはnarHashが入ります。これは入力のソースツリーをNARとしてシリアライズしたもののハッシュです。このハッシュを計算するにはツリー全体を読む必要があります。つまりlockファイルにnarHashを入れる以上、怠惰さには天井があります — ファイル一つしか要らなくても、ツリー全体を走査しなければならないからです。
だから本当に最後まで怠惰になるには、narHashをlockファイルから外さなければなりません。それがlazy-locks = trueです。そしてそうして作られたlockファイルは古いNixでは読めません。チームの一人がそのオプションを有効にしてflake.lockをコミットすると、有効にしていない人のビルドが壊れます。
両方の設定ともコード上のデフォルト値がfalseであることも書き添えておきます。Determinateがロールアウトした20パーセントは、コードのデフォルトではなく、自社のデーモンが管理する設定で有効化されたものです。
Lixの答え — 直さず凍結する
三つ目の実装もあります。2024年に分岐したフォークLixです。
LixウィキのFlakes feature freeze文書(2025年10月5日作成、2026年2月1日更新)は、コアチームがフレークの機能集合とセマンティクスを現時点で凍結することにしたと述べています — バグ修正以外の新機能は入れない、新しい入力タイプも、既存フェッチャーへの新機能も、フレーク中心の評価器変更も入れない。理由はフレーク実装のコードが極度に脆く、保守負担が大きいことです。
同じ文書にこの記事の文脈で最も鋭い一文があります。フレークのロック設計がunsoundだという指摘で、要旨はこうです — lockファイルにあるものがフェッチ可能であっても、このNix実装が同じ入力に対して生成したはずの値でなければロック比較は失敗する。そのため、libfetchersの入力フォーマットやlockファイルフォーマットにどんなdivergenceが生じても、実装間の非互換が発生する。
Determinateのlazy-locksはまさにそのdivergenceです。Lixウィキはそのクラスの問題を一般論として指摘し、Determinateはそれをオプトインフラグの裏に置き、アップストリームはそもそもやらないことにしました。同じ問題への三つの異なる答えです。
Nix 2.35が実際にしたこと
Nix 2.35.0は2026年7月13日にタグ付けされ、同日に発表されました。(リリースノート文書の冒頭に書かれた日付は2026-06-22です — ノートを切った日と実際のタグに3週間の差があります。タグと発表が同じ日である2026年7月13日が実際のリリース日です。)
マージされたのはPR #15711 "Don't copy flakes to the store unnecessarily (maybe 3rd time's the charm)"です。作者はSergei Zimmerman(@xokdvium)、2026年4月19日に開かれ4月27日にマージされました。コミット3、ファイル14、+180/-37。
lazy trees v1が402コミット・56ファイル・+1363/-364だったことを思い出すと、桁が違います。
PR本文にアプローチがそのまま書かれています。
3rd time's the charm! (...) This repurposes a slightly less lazy (but also more deterministic) approach than determinate nix has taken. We do still pay to cost of hashing an input once to compute the store path and narHash daemon-client-side.
「Determinate nixが取ったものよりやや怠惰さは劣るがより決定論的なアプローチ」。そして「入力を一度ハッシュするコストは依然として支払う」。
動作の仕組みはリリースノートにあります — コピー回数を減らすため、Nixは今や先にコピーせずハッシュし、.outPathがderivation属性に入らないだろうから実際にstoreへ取り込む必要はないだろう、と仮定します。この仮定が外れた場合はわずかな追加作業というコストがかかりますが、典型的な使用ケースでは作業が減ります。
つまりコピー(copy)をなくしたのであって、走査(walk)とハッシュ計算をなくしたわけではありません。
これがDolstraと無関係に進んだわけでもありません。Zimmermanはイシュー#3121に2026年4月19日、こう書きました — 「#15711で試してみました。@edolstraがそのPRを可能にする良い作業をたくさんしてくれていて、私は断片を組み合わせただけです」。実際、このPRが土台にしているPR #14050 "Fix fetchToStore caching"は、Dolstraが2025年10月6日にアップストリームへマージしたものです。
fetchTarball側も同じパターンです。Determinate Nix 3.21.0(2026年5月22日)が「Lazy fetchTarball」を搭載し、5日後の5月27日にアップストリームでPR #15920 "fetchTarball: Use the tarball fetcher"がマージされました。どちらも作者はDolstraです。(二つのパッチが同じものだと断定はしません — 確認したのは作者と日付だけです。)
性能数値はリリースノートに一行あります — この変更は典型的な評価で必要なディスク使用量を大きく減らすと期待され(expected to)、nixpkgsのタルボールを取得して展開する際、fetchTree/フレーク経路でもfetchTarball経路でも約2倍の速度向上をもたらす(does)。これはプロジェクト自己測定であり、ハードウェア・方法論・比較対象バージョンは明記されていません。ディスク側は「期待される」、速度側だけ断定形という原文の温度差もそのまま移しておきます。
アップストリームにはlazy-treesやlazy-locksのような設定はありません。GitHubコード検索基準でNixOS/nixリポジトリに両方の文字列のヒットは0件です。オンにするものもオフにするものもなく、デフォルトの動作そのものが変わりました。これがDeterminateの方式との実質的な違いです — フラグを増やす代わりに、互換性を壊さない範囲でだけ怠惰になりました。
まだ直っていないもの
ここが本稿で最も重要な節です。リリースノート自身が限界を書き記していますが、こういう文はたいてい要約から消えます。
原文はソースツリー全体に触れてNARシリアライズをハッシュする作業は不可避(unavoidable)だと述べ、理由を二つ挙げています。
- フレーク入力の場合、
narHashの整合性をeagerに検査しなければならない。 - フレークの
outPath属性は事前に既知でなければならず、下位互換のためにcontent-addressedなstoreパス文字列でなければならない。
つまり、3ギガバイトのモノレポでファイル一つを直すと、nix buildは今や3ギガバイトをstoreにコピーはしませんが、3ギガバイトを読んでハッシュはします。大きなツリーでは、これは依然として目立つコストです。消えたのはstoreへの書き込みとディスク占有であって、ファイルシステムの走査ではありません。
その一つ前の2.32ですでに半分は片付いていたことも、リリースノートは指摘しています — Nix 2.32以降、評価中のソース読み取りに関わるすべてのI/Oは(path:とhg+:スタイルの入力を除き)元のファイルシステムの場所へ、タルボールベースの入力は~/.cache/nix/tarball-cache-v2のbare gitリポジトリへ経路が付け替えられていました。残っていたのはstoreパス計算のためのfetchであり、2.35がそれを取り除いたのです。
そして仮定が外れる場合もあります。.outPathが実際にderivation属性に入る式である場合 — リリースノートの表現でいう「仮定が外れた場合」 — 追加作業のコストを支払います。ノートはこれを「slight」と呼び、具体的な数値は出していません。そうした数値は公表されたことがないので、ここでも作り出しません。
最後に、これは再現性を改善する変更ではありません。性能上の変更です。結果のstoreパスは変わらないままでなければならず、評価器の視点からは今も複製が起きたかのように振る舞います — toString ./.は今もコンテキストなしのcontent-addressedなstoreパス文字列を出し、フレークのoutPathからファイルを読むコードも今も動作し、IFDとは扱われません。ノートがこの互換性リストを長々と並べているのには理由があります。下位互換こそがこの設計の制約条件そのものだったからです。ちなみに、この「再現性のコスト」というテーマを配布物の側から別角度で見たいなら、Debianが再現不可能なパッケージのtesting移行を止め始めた話が同じ日に上がっています。
で、あなたは何をすべきか
アップグレードが割に合う場合
- 大きなリポジトリ・モノレポでフレークを使っていて、
copying '...' to the storeが反復ループの体感コストになっている場合。コピーが消えるだけで得をします。 - CIでnixpkgsのタルボールを繰り返し取得・展開している場合。リリースノートが約2倍を語っているのはまさにここです(プロジェクト自己測定)。
/nix/storeのディスク圧迫が実際の問題になっている場合。評価のたびにソースツリーのコピーが積み上がっていたのが減ります。
大差ない、あるいは急ぐ理由がない場合
- ツリーが小さい場合。走査とハッシュ計算は残っており、もともとコピーも安かったなら、得られるものはあまりありません。
- フレークを使っていない場合。この変更はフレークと
fetchTree/fetchTarball経路の話です。 - モノレポのボトルネックが評価ではなくビルド自体である場合。3ギガバイトを依然として読んでハッシュするという事実は変わりません。
Determinate Nixのlazy-treesを有効にするか迷っているなら — これは2.35とは別物です。より怠惰ですが、最後まで怠惰になるにはlazy-locksが必要で、それはドキュメントが明示的に下位互換を壊すと述べています。チーム全員が同じNix実装を使うと保証されていないならlazy-locksは有効にしないでください。flake.lockはチームで共有するファイルであり、実装間のlock非互換は、Lixウィキがすでに一般論として指摘済みの既知の落とし穴です。
Lixを使っているなら、フレークに新機能は来ません。これは事故ではなく公表済みの方針であり、長期計画はフレークをコードベース外のプラグインへ切り出すことです。
おわりに
まとめるとこうです。2019年にNixの創始者が開いた「ソースを怠惰にコピーしよう」というイシューが、2026年に閉じました。閉じたのは彼の402コミットのPRではなく、彼の先行作業の上に他の誰かが積んだ180行のPRでした。その間に、同じ機能がダウンストリームの商用ディストリビューションでパーセンテージロールアウトとして先に出荷され、別のフォークはこの領域全体を凍結しました。
技術的な教訓はシンプルです。この問題の難しさは怠惰さを実装すること自体にはありませんでした — Determinateが2025年に証明した通り、それはできます。難しさは下位互換を壊さずに怠惰になることにあり、だからアップストリームに入った答えは「やや怠惰さは劣るがより決定論的」なものでした。narHashをlockファイルに入れ続けると決めた以上、ツリー全体のハッシュ計算は残り、それはリリースノート自身が「unavoidable」と書いた通りです。
そしてこれが、この話で最も長く残る一文のように思えます。大きなdiffが問題を解くのではなく、制約条件を受け入れた小さなdiffが問題を解く、ということ。PRのタイトルはすでに知っていました — maybe 3rd time's the charm。
参考資料
- Nix 2.35.0 released — NixOS Discourse 公式発表 (2026-07-13)
- Nix 2.35 リリースノート原文 (rl-2.35.md, タグ 2.35.0)
- NixOS/nix#3121 — Copy local flakes to the store lazily (2019-10-07 ~ 2026-06-09)
- NixOS/nix#15711 — Don't copy flakes to the store unnecessarily (maybe 3rd time's the charm)
- NixOS/nix#15920 — fetchTarball: Use the tarball fetcher
- NixOS/nix#14050 — Fix fetchToStore caching
- NixOS/nix#6530 — Lazy trees (マージされずに終了)
- NixOS/nix#13225 — Lazy trees v2 (まだ開いている)
- Determinate Systems — Changelog: introducing lazy trees (3.5.2, 2025-05-15)
- Determinate Systems — lazy trees rolled out to 20% of users (3.8.0, 2025-07-11)
- Determinate Nix ソース —
lazy-trees/lazy-locks設定定義 (v3.21.7) - Lix Wiki — Flakes feature freeze
- Debianが再現不可能なパッケージのtesting移行を止め始めた (関連記事)