Nix Flakesが抱える課題—依存関係の複雑さ

Nix Flakesは、再現性の高い開発環境を構築するために非常に有効な機能です。しかし、その強力さの裏には、依存関係の管理における「煩わしさ」が常に付きまとっていました。

特に、複数のFlakeをプロジェクトに組み込む際、それぞれのFlakeが異なるnixpkgsバージョンを参照したり、flake-utilsのような共通ライブラリを個別に持ち込んだりすることが問題視されていました。(出典)

「Flakesは連邦化されているのが良い機能だと宣伝されているが、現実は中央集権的なFlakeのシンプルさを求めている。」

これは、多くのNixユーザーが感じていた共通の不満点ではないでしょうか。私も、新しいFlakeを追加するたびにfollows設定を記述して依存関係を統一する作業に、正直なところ「またか」と感じていました。このプロセスは、Flakeの基本的な思想とは裏腹に、開発者の負担を増やしてしまう側面があったように思います。

既存のFlakeにおける依存関係の問題をまとめると、以下のようになります。

項目 従来のFlake Omniflake
入力数 必要Flake数だけ増える 単一のomniflake入力のみ
依存関係の解決 個々のFlakeでnixpkgsなどが重複する可能性 omniflake内で一元管理、効率的に解決
設定の複雑さ followsによる依存関係の統一作業が必要 不要
利用可能なFlake数 自分で追加したもののみ 約1万2千ものFlakeにアクセス可能

Omniflakeの衝撃—数千のFlakeを単一入力で

fzakaria氏が提示した「Omniflake」は、この長年の課題に対する画期的な解決策です。彼は「一つのFlakeが他のすべてのFlakeを運び、必要なものだけを取り出すことができるのか?」という問いを立て、それを実現しました。(出典)

omniflakeをプロジェクトに追加する方法は非常にシンプルです。

inputs.omniflake.url = "github:fzakaria/omniflake";
inputs.omniflake.inputs.nixpkgs.follows = "nixpkgs";

この設定一つで、執筆時点で約1万2千ものFlakeにアクセスできるようになります。しかも、Nix言語の遅延評価(lazy evaluation)とFlakeロックメカニズムのおかげで、実際に使用したFlakeの分だけコストを支払う形になり、無駄が生じません。これは、まるで巨大なFlakeのライブラリにアクセスするような感覚で、開発体験が劇的に向上すると感じます。

例えば、omniflakeを使って特定のパッケージやNixOSモジュールを利用する方法は以下の通りです。

  • パッケージの利用: environment.systemPackages = [ omniflake.flakes.nh.packages.${system}.default ];
  • Overlayの利用: nixpkgs.overlays = [ omniflake.flakes.rust-overlay.overlays.default ];
  • NixOSモジュールの利用: imports = [ omniflake.flakes.disko.nixosModules.disko ];

コマンドラインから直接Flakeの出力を実行することも可能です。

$ nix run 'github:fzakaria/omniflake#flakes.nh.packages.x86_64-linux.default' -- --version

これを見たとき、正直「ここまで来たか」という驚きを隠せませんでした。Nixの強力なメカニズムが、このような一見不可能に思えるソリューションを可能にしたことに、改めて感銘を受けます。私の個人プロダクトでもNixを積極的に活用しており、このomniflakeはぜひ導入を検討したいと考えています。

Nix Flakesの依存関係管理の仕組みとOmniflakeの実現性

Nix Flakesの基本的な仕組みを理解することで、omniflakeがなぜ実現可能であるかが分かります。Flakeはflake.nixファイルで入力(依存する他のFlake)と出力(それらの入力から生成される関数)を宣言します。(出典)

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
  };
}

flake.lockファイルは、各依存関係がどの特定のコミットに解決されたかを記録し、ビルドの再現性を保証します。しかし、このロックファイルは「推移的な依存関係のグラフ全体」を固定するため、複数の子Flakeがそれぞれ異なるnixpkgsを参照している場合、それぞれが独自のコピーを持つことになります。これが、従来のFlakeにおける依存関係の重複問題の根源でした。

followsという機能は、この重複を解消するために存在します。これは、ある依存関係を既に存在するノードに再マッピングすることで、グラフサイズを削減するものです。しかし、これにより本来作者がテストした環境とは異なるnixpkgsでビルドされる可能性が生じます。

omniflakeは、このNixの遅延評価とロックメカニズムを巧妙に利用しています。数千のFlakeを内部に抱えながらも、実際には必要なものだけが評価され、ロックファイルもそれに応じて生成されるため、巨大なグラフ全体を常に処理する必要がありません。このアプローチは、Nixの設計哲学と非常に良く合致していると感じます。私も、自分のスクリプトで複数のNix Flakeを組み合わせる際に、この複雑さに悩まされていました。omniflakeのような中央集権的なアプローチは、この悩みを解決し、Nixエコシステムをより使いやすくする大きな一歩ではないでしょうか。