若者がパッケージ管理について思うこと

App::llenvというのを書いたり、Touryoというサーバの設定管理ツールを書いたりする中で、広義な「パッケージ管理」というものにすごい興味を持っているので、思うことを書いてみる。

**【追記】**タイトルが意味不明っていっぱい言われたのでえいやと変えてみた

**【追記】**結論書き忘れてたので続きを書いた: 若者がパッケージ管理について思うことの今の結論 – As a Futurist…

パッケージ管理って怖くてよく分からないとか思ってる人に少しでもパッケージ管理に親しんでもらえればと思って書いてる。かく言う僕も Perl の Catalyst や Plagger のインストールに泣いたり、rpm の依存ぶっ壊して戦々恐々としたりした経験があってここにいるわけなんですが、もうみんながそういう苦労するのあほらしいよなぁと思うので、パッケージ管理ってどういうところが勘所なのか知ってもらえれば十分。

とは言ったものの、僕が多少知ってるパッケージ管理ってせいぜい CPAN と RPM くらいで、他全然詳しくないので一般化して語ってるけど全然一般化されてないので、むしろ一般化した論文とかあったらぜひ読みたい。きっと Gentoo の人とかもっとすごいこと考えてる。

パッケージ管理に必要な要素

「パッケージ」の定義を敢えてせずに、「パッケージ管理」に必要なものってなんなのかを列挙してみる。

  • パッケージのバージョン管理
  • パッケージ間の依存関係の管理
  • パッケージのインストール/アンインストール
  • パッケージのアップグレード/ダウングレード
  • パッケージをそろえる事で同じ環境が再現できる

パッケージのバージョン管理

まずは何よりもこれが重要。パッケージと呼ばれる単位に何らかのバージョン識別子をつけて、中身を細かく比べることなく更新を知ることができるようにする。

もしバージョンという概念がないと、git や svn の様なバージョン管理システムなしにソースコードを管理するようなもので、それはそれはおぞましい。一体このバイナリがいつの時点のどういうビルドだったのかとか分かんなくなったら、管理者が死ぬ。

パッケージ間の依存関係の管理

次に重要なのが、バージョンを持ったパッケージ間に依存関係を記述できるようにすること。あるバージョンのパッケージ A を使うためには、バージョン何とか以上のパッケージ B が必要、みたいな感じ。

もちろん使う側が完全に依存を把握できていて順番を定義可能(イメージ的には自分で Makefile のエントリを全部書けるような感じ)だったら、パッケージ管理において依存関係の記述は要らない。でも、世に言うパッケージ管理では色んな人が作ったパッケージを組み合わせることが多いわけで、そうなると他人の作ったパッケージが一体何に依存してるかとかもはや把握できない。なので、パッケージの中に自分が依存するパッケージを記述するのが効率が良い。

パッケージのインストール/アンインストール

通常パッケージ管理というと、インストールができることを指すことが多い気がする。もちろんパッケージとバージョンを指定したら、その OS 上で使うことができる様にインストールしてくれるってのは大事な機能。インストール時にはコンパイル済みのバイナリを配置するだけのものもあればその場でコンパイルするものもある。もちろん、先の依存関係に基づいて必要なパッケージも同時にインストールしてくれる必要がある。難しいのはそれを再帰的に解決しないといけないところ。場合によっては循環参照のような依存(A は B に依存してるのに、B は A に依存することになってるとか)になってしまっている場合もありえるので、それなりにそれを解決してあげないといけない。

アンインストールはちょっと難しい。色々と気を使わないといけない。なので標準では実装してないパッケージ管理もある(例えば CPAN)。何が難しいかというと、例えばプロセスが起動するようなパッケージだとそのプロセスを止めてあげなければいけないし、先の依存関係で自分が誰かに依存されてる場合は、それらのパッケージもアンインストールしないといけない。さらに、インストール前にあるファイルを変更するようなパッケージ(例えば conf に 1 行追加する)の場合、完璧なアンインストールをするためにはその 1 行をキレイに消さなければいけないが、1 行消すとかは以外とめんどくさい。

パッケージのアップグレード/ダウングレード

yum update とか打ったら一発で最新のパッケージ群にしてくれるのは一般人に取ってはとても頼もしい。これができるためには、バージョン、依存関係、インストールと全てができる必要がある。

ダウングレードはアンインストール以上に難しい。なぜなら大抵の開発者がアップグレード方法には気を使って作るけど、ダウングレード方法には普通気を使わないから。いろんなパッケージ管理でダウングレードは地獄の作業。ダウングレードするくらいなら、OS 再インストールから作り直した方が簡単な場合もある。

パッケージをそろえる事で同じ環境が再現できる

これを実現するためには、上記以外に様々なバージョンのパッケージをホスティングさせる方法も柔軟に指定できることが必要だ。パッケージは大抵自分が開発したもの以外を使うわけだが、それらをどうやって入手すればいいかということも合わせて確保しておかないと、例え入っているべきパッケージとバージョンのリストがあったとしても物が入手できずインストールできなくなってしまう。

分かりやすいところだと、CentOS の yum とかは、update レポジトリがどんどん新しくなってしまうので、古いバージョンがいつのまにか取れなくなってしまったりする。もっと細々と運営しているレポジトリだとさらにその危険度は高まる。そういうのに対して、例えばpgkg.orgの様なサイトがあり、僕はここに大変お世話になっている。

パッケージ管理の何が難しいのか

上にもいくつか難しさは書いたけど、追記しつつまとめる。

  • パッケージのバージョン管理

    • 特にプリコンパイル型だと、ビルドの仕方などによってどっちのバージョンが新しいとかが決めづらい
    • 異なるバージョンを 1 つの OS にインストールして使える様にするのがかなり難しい
  • パッケージ間の依存関係の管理

    • 依存関係が完全に書けていない、嘘をふくんでいるパッケージが混ざる可能性
    • 循環依存する可能性
  • パッケージのインストール/アンインストール

    • きれいなアンインストールが難しい
    • 余計なものがインストールされて今まで動いてたものが壊れる可能性
  • パッケージのアップグレード/ダウングレード

    • きれいなダウングレードが難しい
    • 予想外のパッケージまでアップグレードされて、そのバージョンだと他のパッケージが動かなくて死ぬ
  • パッケージをそろえる事で同じ環境が再現できる

    • 古いバージョンのパッケージが取得できなくなる
    • あるパッケージがどういう依存によって入ったものか記録しておく必要がある

既存のパッケージ管理では上に挙げた難しさを一部解決したりしてなかったりしている。自分が今までみた工夫を挙げると以下。超詳しく調べたわけじゃないので間違ってる情報があったら指摘して下さい。

例えば、yum では rpm のバージョンやリリース、アーキテクチャなどの文字列からどちらのパッケージを入れるべきかを決定する関数とかが作られている。

例えば、npm ではあるパッケージ A が依存するパッケージ B が、他のパッケージ C によってバージョンが変わってしまわないように、パッケージ B はパッケージ A の置いてあるディレクトリ以下に入る様になっている(同じバージョンのパッケージ B をパッケージ C も必要としている場合、実質 2 つのパッケージ B がインストールされる形)。

例えば、cpanm では循環的な依存を回避するために、再帰的な依存解決の中で既に出たかどうかをチェックして循環しないようにしている。

例えば、carton では古いパッケージをなるべく取得できるように色んなホスティング先を指定できたり自分で持つことができたり、bundle や pip では丸ごと tar で固めてしまうようなこともできる。

例えば、gem ではパッケージがバージョン付きで配置されるので、複数バージョンを共存させておくことができる。homebrew も似たような感じで、symlink によって使うバージョンを切り替えたりしている。

パッケージ管理の本当の難しさ

上に書いたのは単一のパッケージ管理の中での難しさに帰着するものが多い。そういうものであればそのパッケージ管理を洗練させていくことでどんどん改善されていくだろう。

実はパッケージ管理で最も難しいのは、パッケージ管理同士が依存しあってしまう場面だ。具体例を挙げる。

  • LL のパッケージ管理の中で、OS のパッケージ管理で入れられたライブラリをリンクする様なモジュールが存在する場合

    • 例えば Perl の XS が yum でいれた lib なんちゃらに依存してしまう場面など
  • LL のパッケージ管理を OS のパッケージ管理で半分肩代わりしてしまっている場合

    • CPAN モジュールを rpm によって管理するなど

通常パッケージ管理間での連携の仕組みというのは整えられていない。特に LL のパッケージ管理は OS に依存せずに色んなプラットフォームで同じように管理できることを目標にしてたりするので、OS 毎に違うパッケージ管理に全て対応するのはあまり現実的ではない。

なので、例えば CPAN の Image::Magick などの様に CPAN の外で ImageMagick がライブラリとして入っていることが前提になってることがあるので、そこの依存関係は人力で解決しておく(つまり事前に ImageMagick をインストールしておく)必要がある。

その 1 つの解決策として、そもそも LL のパッケージ管理を OS のパッケージ管理で代替させる方法が 2 つ目に挙げた例だが、これもカオス。LL のパッケージ管理を使って何かをインストールしようとかするとややこしいことになってきて、両方でのバージョンの依存関係とか考えだすともう萎える。

もう 1 つの解決方法は、CPAN のAlien::RRDtoolの様に、LL のパッケージ管理側にライブラリを取り込んでしまう方法。これはエレガントでいい感じなんだけど、じゃあ今まで OS のパッケージ管理でインストールしてしまっているライブラリを全部これでレシピ書きなおす必要があるとか考えるとゾッとする。

今は LL と OS の関係を主に取り上げたけど、それ以外にもパッケージ管理間の依存によって大変ややこしいことになる場面というのは往々にして存在するとおもう。こういう場面に対するベストプラクティスについて想いを馳せることがあるのだが、そういうのを語り合う人があまりいなくて寂しいので、興味のある方はどこかでお話しましょう。

たとえばこの辺とか僕はマジメに考えてみたい><

というわけで最近思ってることをつらつらと書いたのでした。