依存性地獄を解決するディストリビューション非依存のパッケージマネージャ、Nix

 Ubuntu、Debian、SUSE、Fedora、Red Hatなどの多様なLinuxに対応した新世代のパッケージマネージャとして開発された Nix には、ディストリビューションに依存しない簡潔な方式にてバイナリやソースパッケージを導入するための手法が組み込まれている。更にNixでは、既存のパッケージマネージャとの競合回避も配慮されているだけでなく、これら旧世代のパッケージマネージャでは行えなかった、バージョンの異なるソフトウェアどうしを共存させた上で、いったん施したアップグレードのロールバックをさせるという操作も可能にするのだ。こうしたNixは、異種環境を包含するシステム管理ツールとして優れている他、複数のライブラリ、コンパイラ、インタープリタを併用するソフトウェア開発者にとっても役立つ存在のはずである。

 新世代のパッケージマネージャと言っても今更ながらの気がしない訳でもないが、そうしたものを新たに開発する意義はいったい何なのだろうか? その理由の1つは、多くのユーザー間に見られるアップグレードサイクルの鈍化が、現存するパッケージマネージャの抱える本質的な欠点に起因していると見られているためだ。ソフトウェア依存性にまつわるトラブルは、誰でも1度は経験したことがあるだろう。そして、たいていのディストリビューションでは定期的なメジャーリリースが行われる以上、多くのユーザーは中途半端なアップグレードは控えて、完全にクリーンなインストールができるまで少しの間待つという姿勢が一般化しているのだ。ところがNixの場合は、過去にインストールされたパッケージを上書きしない方式が採用されているため、いつでも安全なアップグレードが行えるのである。つまりNixを用いれば、旧バージョンを残したまま作業を継続し、必要な場合のロールバックも簡単に行えるのだ。

 そもそもNixは、オランダのユトレヒト大学(Utrecht University)における学術プロジェクトの1つとして開発されたものであり、一種のジョークとして、その名称は“nothing”を意味するオランダ語を基にしている。

不可逆なアップグレード、ソフトウェアのバージョニング、異種環境にまつわる問題

 不可逆なアップグレードは、APTやRPMおよびFreeBSDのPorts Collectionなど、メジャーなパッケージマネージャのすべてで発生しうる問題である。その本質的な原因は、単一のアプリケーションであれオペレーティングシステム全体であれ、アップグレードを実行する際にパッケージマネージャが、現状のシステム上にある関連ファイルを新バージョン用のもので上書きしてしまうからに他ならない。それでも完全な下位互換性を有しているパッケージであれば、上書きに起因した問題の発生は回避可能なはずだが、この世の常として完全な下位互換性などは期待できないものである。

 例えば手元のFirefoxをアップグレードする場合に、パッケージマネージャがGTKの新バージョンも必要だと判断したとしよう。この新規のGTKが下位互換性を有していなかった場合、当該システム上の(GTKを利用する)他のアプリケーションはどれも動作不能に陥るかもしれないのだ。同様の問題はWindowsにても発生し、それはDLL地獄(DLL hell)という名称で呼ばれることもあるが、Unixにおける依存性地獄(dependency hell)は致命的とまではいかないものの、より本質的で厄介な性質を有している。それはUnix系プログラムの多くが、広範な外部依存性を有す傾向にあるからだ。

 不可逆アップグレードの場合、アップグレードを取り消すロールバック作業も困難である。実際問題として、事前にパッケージマネージャないしユーザー自身が上書き対象の全ファイルをバックアップしておかない限り、1度行ったアップグレードは簡単に取り消すことはできないはずだ。

 また、パッケージマネージャが関連ファイルの上書きを完了するまでの間、当該システム中のコンポーネント間の整合性が崩れ、パッケージが機能不全に陥る可能性がある。確かにこれはごく一時的な状況として終わるはずのものだが、仮に新規OSへのアップグレード処理中に誤って電源を落としてしまったような場合、次回起動時にこのコンピュータは正常に立ち上がってくれるだろうか?

 結局のところ、パッケージをアップグレードすれば古い方のバージョンは消え去るものと心得ておくべきなのだ。確かに、パッケージマネージャによってはgcc-3.4とgcc-4.3というように隣接するバージョンを共存させることもあるが、それにはインストール先のパスが2つのバージョン間で重複しないための設定がパッケージ制作の段階で配慮されていることが前提となる。それでもユーザーからの要望として、現状のシステムを乱さない形でgcc-4.0.3をテストしてみたい、使用するコンパイラ、ライブラリ、インタープリタを変更したバージョンのソフトウェアを試してみたい、あるいはベータ版アプリケーションを試験してみたいが既存インストレーションには手を付けたくないという訴えが出てくるのは、それほど珍しいケースではないはずだ。

 次に、複数のディストリビューションをマルチ態勢でサポートする場合の問題点に話を進めよう。開発者にせよシステム管理者にせよ、個々のユーザーが最終的にどのような組み合わせのカーネル、ライブラリ、パッケージを用いるかは、事前に窺い知ることのできない性質の情報である。例えば、ソフトウェアのインストール時にライブラリの不足を発見したユーザーに対して助言できるのは、適切なパッケージを利用するようにと伝えるのが関の山で、それが実際に依存性上の問題を出さないコンビネーションであるかなどは誰にも確認できないものだ。またソースからのビルドを推奨しておいたとしても、バージョン的に不適切な組み合わせのライブラリとコンパイラが使われるかもしれない。あるいは、新規ソフトウェアの導入先となる組織にて使用されるディストリビューションが一本化されていたまではいいが、そこでは目的のソフトウェアをサポートしていない旧世代のライブラリが使われていたというケースに遭遇しても不思議はないだろう。

 既存のパッケージマネージャ群は、多数のユーザーによる長期間の使用経験がある分、安定したシステム構築という点では信頼が置ける。だが現実には、バグの修正や必要な機能の追加された最新バージョンのソフトウェアが必要となる場面も存在し、そうした状況に陥ったユーザーは“テスト段階”や“不安定版”などと呼ばれる状態のパッケージに手を出すことになるだろうが、この種のものには複雑な依存性関連の制限が付きもののため、それらの導入は当該システムにおける残り多数のソフトウェアコンポーネント群を不安定化させる危険性を高めることになってしまう。

 こうした問題点を踏まえて構築されたNixでは、バージョンの異なるソフトウェアを共存させる上で、より洗練された方式が採用されている。それはパッケージのインストール先として/usrのような共通化された保管場所を使用するのではなく、/nix/store下にパッケージごとの専用ディレクトリを用意するというものだ。こうした各パッケージ別の最上位ディレクトリには、パッケージビルド時の入力情報を基に生成された暗号化ハッシュによる名前が付けられる。これはバージョンの異なる同一系統のパッケージを区別するための識別子として機能するため、このシステムに置かれたパッケージ群は、依存性的な競合を引き起こすことなく様々なバージョンを使い分けられるという仕組みになっている。つまりこの場合、パッケージのアップグレードだけでなく、その完全なロールバックすら可能となるのだ。

Nixの導入法

 皮肉なことに現状でNixをインストールする最適な方法は、入手したソースをコンパイルするというもので、その際にroot権限下で使用するのも「./configure ; make ; make install」というお馴染みのコマンドであるが、この方式であればMac OS XやBSDも含めたすべてのシステムで共通して機能するはずだ。この作業では標準GNU g++のビルド環境が必要であり、またNixのブートストラップにはcurlのインストールも必須となるが、Nixのインストール後これらのツールは不要となる。またNixではパッケージ群の本体だけでなく関連するメタデータを/nixディレクトリに格納するため、その分だけ余分なディスク容量を用意しておかなくてはならない。ルートとなる/ディレクトリに充分な空きがない場合は、/nix用の専用パーティションを設けるか、5から50GB程度の空き容量を持つファイルシステムに/nixをバインドマウント(bind-mount)すればいいだろう。その他の要件として、各自のbashrcファイルに/usr/local/etc/profile.d/nix.shを追加しておく必要があるが、その役割はNixの必要とする環境変数($PATHなど)を設定することである。

 Nix本体のインストールが完了すれば、次に行うのはNixを用いたソフトウェアのインストールである。例えばNixのWebサイトにアクセスすれば、多数のNix用パッケージ群を1つのコレクションにまとめたNix Packages(Nixpkgs)をダウンロードできるし、同様なコレクションはSubversionリポジトリに収録されているかもしれない。あるいは一番手軽な方法として以下のコマンドでNixpkgsチャンネルに登録しておけば、ユーザーの手元には最新バージョンのNixpkgsが自動で配信されるようになる。

nix-channel --add http://nixos.org/releases/nixpkgs/channels/nixpkgs-unstable
nix-channel --update

 Nixpkgsの内訳は、パッケージ群の構成法をNixに指示する様々なNix expressionを組み合わせたものであり、Gentooにおけるebuildに相当するものだと考えておけばいいだろう。基本的にNixはPortage同様のソースベースのパッケージマネージャとして機能するのだが、そのハッシュ値によって格納位置を識別できたパッケージ群については、コンパイル済みバイナリのダウンロードをするようになっている。こうしたコンパイル済みバイナリはNixpkgsチャンネルに多数収録されており、その利用はNixを介したパッケージインストールを大幅に高速化してくれるはずだ。

 nix-channelからNixpkgsのダウンロードが完了すると、実際にどのようなパッケージが使用可能かを確認できるようになる。そのためにはyumやapt-getに相当する機能を有した、nix-envというツールを使用すればいい。パッケージ名を一覧させる構文は「nix-env -qa '*'」であり、あるいはFirefox関連のパッケージ名だけを表示させるには「nix-env -qa firefox」と指定する。後者の実行結果は下記のようになる。

firefox-2.0.0.17
firefox-2.0.0.17-with-plugins
firefox-3.0.4
firefox-3.0.4-with-plugins

 なお-sフラグを付けるとNixpkgsチャンネルから入手できるコンパイル済みパッケージが確認でき、下記の出力例のように、バイナリ形態でダウンロードできるパッケージにはSが付けられる。

--S firefox-2.0.0.17
--- firefox-2.0.0.17-with-plugins
--S firefox-3.0.4
--- firefox-3.0.4-with-plugins

依存関係のあるパッケージのインストール

 次にNixの実力を確認するため、依存関係およびFlashなどのプラグイン群付きでFirefox 2をインストールさせ、本当にシステム上の既存ファイルが上書きされないかを見てみよう。その際にユーザーが行うべき操作は「nix-env -i firefox-2.0.0.17-with-plugins」というコマンド指定だけであり、必要なパッケージのダウンロードとコンパイルはNixが自動で実行してくれる。先に確認したように、このチャンネル中にコンパイルされたfirefox-2.0.0.17-with-pluginsは収録されていないが、その他の依存関係にある多数のコンポーネントはコンパイル済み状態にて格納されているので、Nixは可能なものはバイナリ形態にてダウンロードし、それ以外のものはソースからのコンパイルを施すはずだ。実のところFirefoxは、GNU CライブラリからGTKに至る幅広い依存関係を有している。その多くは通常のディストリビューションにて最初から収録されているものだが、Nixを用いたインストールの場合は、ユーザーの必要とするバージョンを確実に導入すると同時に、システムの他の領域に影響を及ぼさないようにするため、これらは重複を許す形で追加されていくのである。今日の環境はかつてほどディスク容量的な制限が厳しくないので、扱いやすいシステムを構築するためなら、ディスクスペースの多少の無駄遣いも致し方ないといったところだろう。

 こうしてダウンロードしたFirefoxを起動させるには、コマンドラインにてfirefoxとだけ入力すればいい(システムフォントを認識させるため/etc/fonts/fonts.confにて環境変数FONTCONFIG_FILEの設定が必要となることもある)。

 次にこの状態からFirefox 3へのアップグレードを実行してみよう。そのためのコマンド指定は「nix-env -i firefox-3.0.4-with-plugins」だが、あるいは「nix-env -u firefox」とだけしておくとNixが最新バージョンを選んでくれる。どちらのコマンドでもFirefox 3がインストールされるはずだが、ここで注目すべきは、旧バージョンのFirefoxが上書きされることなくそのまま残されていることだ。そして下記のように、新旧2つのバージョンにはそれぞれ異なるインストールパスが選ばれている。

/nix/store/vskr06rlblihz22...-firefox-2.0.0.17-with-plugins
/nix/store/w1i05b7s30zqz...-firefox-3.0.4-with-plugins

 これがNixにて採用された、同系統のパッケージ間の干渉を防止したインストール法である。ここの例で用いたFirefoxも上記のパスを指定することで、新旧2つのバージョンを自由に使い分けられるはずだ。

 ただしNixから一歩離れれば、各システムにて最初からインストールされていたパッケージをそのまま使い続けることも可能である。つまり現状のNixパッケージは、あくまで独立した形で存在する追加機能の1つなのだ。

プロファイル

 Nixを介してインストールしたプログラムの起動に使うパスは、直接入力するだけでなくシステムの検索パスに追加しておくこともできるが、いずれにせよ長大なパス名の使い勝手が悪いことは変えようのない事実だ。そのためnix-envでは、インストールパッケージに対するツリー状のシンボリックリンクを自動生成するようになっている。この機能を利用するにあたっては、各自の$PATHに設定される~/.nix-profile/binなどを介して、個々のシンボリックリンクツリーの1つをポイントすることになるだろう。そしてNixにおける完全なアップグレードとロールバックも、これと同様の機構で動作するように設計されており、Nixは新たなパッケージ群に対してはシンボリックリンクツリーを作成するだけで、その後の参照では~/.nix-profileを調べることでポイント先を特定するのである。

 こうしたプロファイルやシンボリックリンクツリーは複数作成することが可能で、それぞれ異なるパッケージに関連付けておくことができる。つまり1つのプロファイル中には、各アプリケーションの特定バージョン用プロファイルを個別にセットアップできるのだ。例えば、Firefoxの最新バージョンをテストするための専用プロファイルをセットアップするには、下記のようなコマンドを実行すればいい。

nix-env --profile my-firefox-test -i firefox-3.0.4-with-plugins

 その後、最新バージョンを試用するには./my-firefox-test/bin/firefox(これは/nix/storeにあるFirefox 3の実際の格納位置を示すシンボリックリンク)を指定すればいいが、こうした操作は~/.nix-profile/binの示すもう一方のFirefoxには何の影響も与えないはずである。

ロールバック

 ここでの例として用いたFirefox 3のインストールは、その導入前から存在したFirefox 2を上書きしない形で行われているため、新バージョンから旧バージョンへのロールバックも簡単に実行することができる。実際Firefox 2に戻したければ「nix-env --rollback」とだけ実行すればいいのだ。

 逆に後日になって、もう2度とロールバックすることはないので不要となった旧バージョンを完全に削除したい、という状況が訪れることもあるだろう。それにはシステムから無用なパッケージを削除する「nix-collect-garbage -d」コマンドを実行すればよく、ここでの例の場合はFirefox 2の本体およびその依存関係にあるファイル群が削除されるが、後者に関しては他のパッケージ(現状で有効なもの)にて使用されないものだけが選ばれるようになっている。

独自パッケージの作成法

 最後に説明するのは、Nixに用意されているドメイン固有のパッケージ記述言語についてだ。これは個々のパッケージにて必要となる依存関係を指定するための機能を有した簡単な構造の言語であり、例えばPHPとその依存性を記述したパッケージは以下のように記述されている。

# Nix expression for building PHP. This is a function that given some arguments
# (like flex and libxml2) builds an instance of PHP in the Nix store.

# These are the function arguments...
{stdenv, fetchurl, flex, bison, libxml2, apacheHttpd, postgresql ? null, mysql ? null}:

# and this is the result of the function: a build action.
stdenv.mkDerivation {
  name = "php-5.2.4";
  src = fetchurl {
    url = http://nl3.php.net/distributions/php-5.2.4.tar.bz2;
    sha256 = "1h513j7crz08n7rlh8v7cvxfzisj87mvvyfrkiaa76v1wicm4bsh";
  };

  inherit flex bison libxml2 apacheHttpd;

  builder = ./builder.sh;

  buildInputs = [flex bison libxml2 apacheHttpd];

  patches = [./fix.patch];
}

 このNix expressionの使用に関しては、libxml2その他のパッケージが必要であり、インストレーションのバックエンドで機能させるSQLについての選択もしなくてはならない。いずれにせよNix expressionにはパッケージ定義に用いる多数のコマンドが用意されており、通常の用途であればこれらを組み合わせるだけで、簡潔かつ可読性の高いパッケージが記述できるはずだ。またビルド時に用いるのは他のライブラリが介在しないクリーンな環境であるため、必要な依存関係は明示的に指定しておかなくてはならない。逆に依存関係に指定漏れがあれば確実に検出されるため、ソフトウェア開発者によっては、こうした性質を有用なチェック機構として用いるという利用法も考えられるだろう。

 そして柔軟なパッケージ管理を可能にするために用意されているのが、パッケージ設定にオプションを渡す機能である。この機能を使うと、例えばGUIサポートをデスクトップ版では有効化するがサーバ版では無効化するという切り替えが、1つのパッケージにて行えるのだ。あるいは、最低限必要な機能のみを装備した軽装版のソフトウェアや関連ドキュメントを別途用意するといった使い方も行えるが、より複雑な使用例についてはパッケージのソースツリーを参照して頂きたい。

まとめ

 実際にNixを運用する場合のベースとしては、安定版のDebianやRed Hat Enterprise Linuxなどの手堅く安定したディストリビューションを使用すればいい。そして、依存関係に束縛されず複数バージョン間でのロールバックを可能にするというNixの特長を利用すれば、ディストリビューションに取り込まれる前の最新版ソフトウェアであっても、安心して導入できるはずだ。つまりNixが可能とするのは、極めて明快なソフトウェア管理が行える環境なのであり、そうしたシステムを手に入れたユーザーは、これまで苦しめられてきた依存性地獄とは無縁の生活を送れることになるだろう。

Linux.com 原文(2008年12月22日)