## はじめに
Nixは評価(Evaluation)とビルド(Build)という2つのフェーズが厳格に分離されている。評価フェーズでは純粋関数的な計算によってDerivation(.drvファイル)を生成し、ビルドフェーズではその指示に従って成果物を生成する。ビルドを開始する前に、すべての依存関係が静的なグラフとして確定しているのがNixの特徴であり、この設計が再現性と監査可能性を保証している。
しかし、この静的グラフモデルは現代的な言語エコシステムへの対応において構造的な課題を抱える。npm、Cargo、Go Modulesでは、依存関係はソースコード内のロックファイルに基づいて決定される。依存関係を確定させるにはロックファイルの内容が必要だが、ロックファイルを読むにはソースの取得が必要であり、ソースの取得はビルドフェーズの仕事である。
なお、fetchPnpmDepsのようにFODで全依存関係をまとめて取得するアプローチもある。しかしこれは粒度を犠牲にしており、キャッシュ効率やインクリメンタルビルドの観点では不十分である。Dynamic Derivationsは「細粒度」と「動的な依存解決」の両立を目指して提案された。
本記事では、その前身となるIFDとlang2nixを扱い、細粒度での依存管理がなぜ困難だったのかを明らかにする。
## シリーズ概略
- Nixの評価モデルとDynamic Derivationsが必要とされた背景
- 本記事: IFDとlang2nixツール: ワークアラウンドとその限界
- CA DerivationとRecursive Nix: Dynamic Derivationsの技術的基盤
- RFC 0092の設計:
.drv出力とbuiltins.outputOfの仕組み - ユースケースと現状: Dynamic Derivationの実践例など
## Import From Derivation (IFD)
### IFDとは何か
IFDとは、評価フェーズの途中でビルドを実行し、その生成物を評価器に読み込ませて、残りの評価を続行するテクニックである。
通常のNixでは、評価フェーズは純粋関数的な計算であり、ネットワークアクセスもファイルへの書き込みも行わない。しかしIFDを使うと、この原則を破って評価中にビルドを実行することができる。
### 処理の流れ
通常の評価: 評価 -----------------------> 評価完了 -> ビルド開始
IFDの評価: 評価 --> 中断(ビルド実行) --> 再開 -----> 評価完了 -> ビルド開始
### 具体例
「長いシェルスクリプトを .nix ファイルに直書きしたくない」という動機から、外部のテンプレートファイルを処理して読み込もうとするケース。
例えば、以下のようなテンプレート(script.sh.in)があるとする。
#!/bin/bash
# @heavy-dependency@ はNix側から注入したい
# 長い複雑なスクリプト...
@havy-dependency@ --do-something
# 長い複雑なスクリプト...
...
これをNix側で処理する際、以下のように書いてしまうとIFDになる。
let
# 1. テンプレート内の @heavy-dependency@ を実際のパスに置換する
# runCommandを使うため、これは「ビルド」扱いになる
processedScript = pkgs.runCommand "process-script" {} ''
substitute ${./script.sh.in} $out \
--replace "@heavy-dependency@" "${pkgs.heavy-dependency}"
'';
# 2. 置換済みのスクリプトの中身を文字列として取得したい
# -> ここで出力のハッシュを計算するために processedScript のビルド完了待ち(IFD)が発生!
scriptContent = builtins.readFile processedScript;
in
# 3. 読み込んだ文字列を使用
systemd.services.my-service = {
script = scriptContent;
# ...
};
この例の「落とし穴」は、Nixには substituteAll という便利な関数があるため、それを使ってファイルを生成し、その結果を「ただの文字列」として扱おうとしてしまう点にある。
ファイルシステム上に「置換済みのファイル」を作成する(ビルド)ことと、その中身を評価時に知る(評価)ことは、Nixの時間軸では明確に区別される。
### IFDの問題点
IFDはNixの設計上、アンチパターンと見なされている。その理由は主に4つある。
#### 1. 評価器ブロック
Nixの評価は基本的にシングルスレッドで行われる。IFDが発生すると、そのビルドが完了するまで評価プロセス全体が停止する。
評価スレッド: [評価中...] → [待機(ビルド完了まで)] → [評価再開...]
↓
ビルド: [ネットワークダウンロード中...]
ネットワークが遅い場合や、重い計算を伴うビルドの場合、評価が長時間ブロックされることになる。
IFDを使用するプロジェクトでは、単純なflake.nixを評価するだけでも、数分かかることがある。
nix replやnix flake showといったツールの対話性が著しく低下し、開発体験を大きく損なう。
#### 2. 並列性の低下
IFDによる「評価→中断→ビルド→再開」のサイクルは、Nixのスケジューラによる最適化を妨げる。
通常の静的評価では、評価完了時点で全ての依存関係が判明しているため、独立したDerivationを並列にビルドできる。しかしIFDがあると、ビルドの途中で新たな依存関係が発覚し、その都度スケジューラが計画を調整しなければならない。
評価器はブロックされている間、外部ビルドの完了を待ってメモリとロックを保持し続ける。これは並列処理の利点を殺し、システムリソースを非効率に占有する。
#### 3. クロスコンパイルとの相互作用
IFDの致命的な側面として、クロスコンパイルとの相互作用がある。
クロスコンパイルでは、ビルドを実行するマシン(build)と、生成されたバイナリが実行されるマシン(host)が異なる。
通常のビルド:
x86_64マシン ──ビルド──> x86_64バイナリ
クロスコンパイル:
x86_64マシン(build) ──ビルド──> aarch64バイナリ(host)
│
├── ビルドツール(gcc等): x86_64用(buildで実行)
└── 生成物(ライブラリ等): aarch64用(hostで実行)
IFDを使用すると、この区別が破綻する。
IFDの問題:
評価中にジェネレーターを実行 ──> Nix式を生成 ──> 導出を構築
│ │
└── x86_64で実行可能である必要 aarch64用パッケージを
記述している必要
評価中にジェネレーター(例:ロックファイルパーサー)を実行するには、buildマシン(x86_64-linux)で実行可能なバイナリが必要である。しかし、そのジェネレーターが生成する導出は、hostマシン(aarch64-linux)用のパッケージを記述していなければならない。この2つの要求を同時に満たすことが困難になる
## lang2nix
IFDを避けつつ動的な言語エコシステムをNixに統合するために、コミュニティで広く使われてきたのがlang2nixである。
### 概要
lang2nixツールは、Nixの評価フェーズの外で実行される外部ツールである。ロックファイルを解析し、その結果を静的なNix式(.nixファイル)として出力する。
有名な例としてnode2nixやpoetry2nixが存在する。
### 仕組み
1. 開発者がロックファイル(package-lock.json, Cargo.lock等)を用意
↓
2. *2nixツールを実行
↓
3. ロックファイルを解析し、静的なNix式を生成
↓
4. 生成された.nixファイルをリポジトリにコミット
↓
5. Nixの評価時にはこの静的ファイルを読み込む
この手法では、依存関係の「発見」をNix評価の外で行うため、評価時には全てが静的に確定している。IFDは発生しない。
### 生成されるファイルの例(概念的)
# node2nixが生成するファイルのイメージ
{
"lodash" = {
name = "lodash";
version = "4.17.21";
src = fetchurl {
url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
sha512 = "...";
};
};
"express" = {
name = "express";
version = "4.18.2";
src = fetchurl { ... };
dependencies = [ "lodash" "body-parser" ... ];
};
# ... 数百〜数千のパッケージ定義が続く
}
### 問題点
#### 1. 二つの真実問題(Drift)
ロックファイルからNix式を生成することで、依存関係の「真実のソース」が2つになる。ロックファイルと生成されたNixファイルである。
# 依存関係を追加
npm install some-package
# Nixファイルを再生成(この手順を忘れがち)
node2nix -i package.json -l package-lock.json
# 生成されたファイルをコミット
git add node-packages.nix
git commit -m "Update node2nix output"
開発者が依存関係を更新した後、lang2nixツールの再実行を忘れることがある。この場合、CIは古い依存関係グラフでビルドするか、ロックファイルとの不整合でエラーになる。
これを防ぐには、CIで「生成ツールの出力がコミット済みファイルと一致するか」を確認するチェックが必要になる。
#### 2. リポジトリの肥大化
生成されるNixファイルは数千行に及ぶことがある。言語エコシステムは信じられないほど深く、広い依存関係ツリーを持つ傾向がある。単純なNode.jsアプリケーションであっても、数千の推移的依存関係を引き込むことは珍しくない。
-
ストレージへの影響: 生成された
node-packages.nixやCargo.nixファイルが数十MBに達することは珍しくない。モノレポのような極端なケースでは、100MBを超えることさえある。これらのファイルはGitにコミットされなければならず、リポジトリのサイズを著しく肥大化させる -
評価への影響: Nix評価器は、導出が評価されるたびに、これらの巨大なファイルを解析しメモリにロードする必要がある。Nixはインタプリタ言語であり、数千の属性セットを含む50MBのテキストファイルを解析することはCPU負荷が高く低速だ。
nix flake checkやnix shellといったコマンドの応答性が低下し、開発体験を損なう
#### 3. フォーマット変更への追従
lang2nixツールはロックファイルのフォーマットを理解してパースする必要がある。パッケージマネージャーがフォーマットを変更すると(Poetryのバージョン2.0移行、RustのCargo.lockフォーマット変更など)、lang2nixツールも追従して更新する必要がある。
## ケーススタディ
これらの問題の深刻さを具体化するために、主要ツールの具体的な闘争を検証する。
### poetry2nix: オーバーライド地獄
poetry2nixはNixコミュニティの英雄的存在だが、同時に絶え間ないフラストレーションの源でもある。poetry.lockを解析することでPythonエコシステム全体をサポートしようとしている。
オーバーライド問題: Pythonパッケージは、システム依存関係(libzやlibsslなど)をNixが理解できる方法で宣言しないことが多い。poetry2nixは、数千のPyPIパッケージに対する手動パッチの巨大な内部リスト「オーバーライド」を維持している。ツールは実質的にメタデータの並列レジストリを維持しなければならない。
node2nixは同様の問題をnixpkgs内部で抱えていた
### 純粋Nixパースの罠
poetry2nixやdream2nixのような一部のツールは、ロックファイルパーサーを完全にNix言語自体で実装することで、IFDの「ビルド」側面を回避しようと試みている。
パーサーバイナリをビルドする代わりに、builtins.fromTOMLやbuiltins.fromJSONを使用し、再帰的なNix関数を使ってデータ構造をトラバースする。これは「ビルド」ステップを回避しているが(技術的にはIFDではない)、その代わりに「評価パフォーマンス」の壁にさらに激しく衝突する。
-
インタプリタの速度制限: Nixは高性能なデータ処理言語として設計されていない。1万行の
poetry.lockファイルを解析し、再帰的な属性セットロジックで導出にマッピングする処理は、信じられないほど低速だ -
メモリの爆発: 巨大なJSON/TOMLファイルをNixのメモリ構造にロードすると、評価器のメモリ使用量が爆発的に増加する。
nix flake checkが30GBのRAMを消費するという報告は、複雑な依存関係ツリーを処理するこれらの純粋Nix実装に起因することが多い -
「フリーズ」体験: IFDと同様に、純粋パースも評価器をブロックする。ユーザーが
nix buildを実行すると、CPUがグラフ計算のために100%で回転し続け、20秒間何も起きないという状況が発生する
## 比較分析
### IFD vs lang2nix 比較表
| 特徴 | IFD | lang2nix |
|---|---|---|
| 処理の場所 | Nix評価中 | Nixの外部 |
| 処理の流れ | 評価→中断(ビルド)→再開 | 事前生成→評価 |
| 評価器への影響 | ブロックする | なし(ただし巨大ファイルのパースは遅い) |
| Hydra対応 | 禁止 | 対応 |
| 同期の負担 | なし | 手動で再生成が必要 |
| ドリフトリスク | なし | 高い(ロックファイルと.nixファイルの同期ずれ) |
| リポジトリへの影響 | なし | 生成ファイルが肥大化 |
| 再現性 | Nix内で完結 | 外部ツールに依存 |
| UXの摩擦点 | 「なぜnix replが20秒もフリーズするのか?」 | 「default.nixの再生成を忘れた」 |
### 「板挟み」の構造
証拠が示すように、どちらの方法も真に満足のいく体験を提供していない。
- 静的生成(lang2nix): 機械(高速な評価、Hydra互換性)を満足させるが、人間(手動同期、リポジトリ肥大化)に負担をかける
- IFD: 人間(Automagic、単一の真実のソース)を満足させるが、機械(低速な評価、リソースロック、CI非互換)に負担をかける
どちらの方法も、根本的にはNixの核となる設計制約、すなわち計画フェーズ(評価)と実行フェーズ(ビルド)の分離を回避しようとしている。IFDは計画中に実行を行おうとし、静的生成はシステムの外部で計画を行おうとしているのだ。
両者とも、NPM/Pip/Cargoの世界の動的な混沌を、Nixストアの静的な秩序に適合させるために設計された「ハック(一時しのぎ)」であり、本質的な解決ではない。
## まとめ
- IFDは評価中にビルドを実行することで動的な依存解決を可能にするが、評価器をブロックし、クロスコンパイル時には問題を引き起こすアンチパターンである
- lang2nixはNixの外部で依存関係を解析しIFDを回避するが、二つの真実問題(Drift)、リポジトリ肥大化、再実装の追従限界といった問題を伴う
- 両者とも静的グラフの限界に対するワークアラウンドであり、本質的な解決ではない
なお、nixpkgsではallow-import-from-derivation = falseが設定されておりIFDは使用できない。またlang2nix(node2nix等)も肥大化・メンテナンス性の問題から非推奨とされており、FODベースのアプローチが主流となっている。一方、ユーザーリポジトリではIFDやlang2nixが使われることもあり、公式と非公式でアプローチが異なる状況が生じている
### Nix依存関係管理のトリレンマ
現在のエコシステムは、以下の3つを同時に達成することができない。
- 細かい粒度(キャッシュ効率のため)
- 動的(生成ファイルなし)
- 高速(評価ブロックなし)
IFDは2と3を、*2nixは1と3を犠牲にしている。このトリレンマこそが、Dynamic Derivationsが解決を目指す構造的課題である。
次回は、Dynamic Derivationsを実現するための技術的基盤であるContent-Addressed DerivationとRecursive Nixについて解説する。

