rpm を Ruby でいじる

はじめに

もりきゅうです。 本稿では ruby-rpm を解説します。 ruby-rpm は Ruby で RPM を扱うための拡張ライブラリです。 RPM については RPM HOWTOMaximum RPM をご参照ください。

ruby-rpm を用いると、rpm コマンドを経由せずに Ruby から RPM DB を扱え、問い合わせ・インストール・アップグレード・削除の処理を実行できます。1

私が Momonga Project に誘われて ruby-rpm をいじるようになったのは、つい最近のことです。 RPM についてもあまり理解していなかったので、ruby-rpm と一緒に調べていくことにしました。 本稿はその調査記録として ruby-rpm の簡単な使い方を紹介します。2

RPM は Red Hat によって開発が進められています。RPM の機能は C ライブラリとして提供されており、これを用いた Python による拡張ライブラリ (rpm-python) が Red Hat によって提供されています (rpm-python は RPM のソースツリーに含まれています)。

拡張ライブラリという点で、ruby-rpm は rpm-python の Ruby 版と考えてよいのですが、仕様を見る限りでは特に rpm-python を参考にして開発されているわけでもないようです。

ruby-rpm はいくぶん古い API を元にしているので、最近の RPM を扱うために少々いびつな修正が行われています。 また、独自の仕様がいくつか見られます。これはおそらく momonga からの要請に沿ったものでしょう。 このような理由から、その実装は少々見通しが悪くなっています。改善の余地は大いにあります。

Momonga Project では Ruby を積極的に利用していますが、特にパッケージ管理ツールである OmoiKondara, mph-get は Ruby を用いた momonga 独自のツールです。そして、これらを支えているのが ruby-rpm です (でした)。 残念ながら、現在の OmoiKondara, mph-get は直接 rpm, rpmbuild コマンドを呼び出しており、ruby-rpm をあまり利用していません。 それは、ruby-rpm にいくつかバグがあった (また、RPM のバージョンを上げたときに十分対応できていなかった) ことが原因ではないかと考えられます。

ほとんどの場合、直接コマンドを呼び出しても十分な結果を得られます。しかし、ディストリビューションに含まれる全パッケージの依存関係を元にするようなツールを書くとき、コマンド呼び出しでは効率が悪かったり、きれいに実装できないところが出てくると思います (その前に、そのような問題が出てくるようなアイデアが必要なわけですけど)。 そこで ruby-rpm の出番というわけです。

どのような拡張ライブラリでも同じことが言えますが、ruby-rpm の開発を通して RPM の実装 (ソース) に対する理解を深めることができます。RPM は今後も多くのディストリビューションで使われていくパッケージングシステムであると思います。Ruby を通して RPM の理解を深めるのもまた一興ではないでしょうか。興味のある方は是非 ruby-rpm の開発にご協力ください。そして Momonga Project へご参加ください。

ruby-rpm のインストール

ホームページを確認すると、MURATA さんによって公開されている ruby-rpm の最終リリースは ruby-rpm-1.1.1 のようです。その README:

 Requirements
 ------------

 * ruby 1.6
 * gcc
 * rpm 4.0.0

今となっては少々古い環境であることは否めません。ここでは momonga で配布されているより新しい ruby-rpm を試してみたいと思います。

ビルド手順

momonga は svn で管理されています。現在の trunk/pkgs/ruby-rpm/ に置かれているのは次のファイルです。3

  • ruby-rpm-1.2.0.tar.bz2
  • ruby-rpm-1.2.0.diff
  • ruby-rpm.spec

momonga ではパッケージを作るときに、基礎となるソースはそのまま置き、そこにいくつかのパッチをあてて momonga としてリリースしています。 ここで扱う ruby-rpm では ruby-rpm-1.2.0.tar.bz2 に ruby-rpm-1.2.0.diff というパッチをあてて作ります。

 $ tar xvjf ruby-rpm-1.2.0.tar.bz2
 $ cd ruby-rpm-1.2.0
 $ patch -p1 < ruby-rpm-1.2.0.diff

このようなパッケージを作るときに必要なソースやパッチのあて方は、RPM で扱う際には spec ファイルという設定ファイルに記述しておき、rpmbuild というコマンドにこの spec ファイルを渡します。

 $ rpmbuild --rcfile rpmrc --target i586 -ba ruby-rpm.spec

さらに momonga では OmoiKondara が rpmbuild コマンドを呼び出してくれます。

 $ ../tools/OmoiKondara ruby-rpm

というわけで、実際には手間要らずなのでした。 ただデバッグのときには手動で rpmbuild を使うことがあります。

Windows で RPM

RPM の実験を Windows 上で行いたい方も居られるでしょう (私もそのひとりです)。

まず、Cygwin の RPM を導入する手があります。しかし今のところ Cygwin は RPM のヘッダとライブラリを配布に含めていません。ですから ruby-rpm を作ることができません。

それでも Cygwin で ruby-rpm を使おうとするなら RPM をソースからビルドする必要があります。私の試した rpm-4.1 では Cygwin でもビルドできました。そして ruby-rpm も (少し Makefile をいじる必要がありましたが) ビルドできました。

Cygwin を使わない方法として X on Win パッケージがあります。 Cygwin で RPM を使う方法を探していると、HOLON Linux が提供している X on Win パッケージを見つけることができました。4 X on Win のブートパッケージを使えば Windows 上で一から RPM 環境を作ることができるようです。興味のある方はお試しください。

ruby-rpm を使う前に

ruby-rpm はまだ十分にテストできていません (テストケースを書きましょう!)。そのため、何らかの拍子に RPM DB が壊れてしまうことがあります。 実行する前に、RPM DB のバックアップをとる手順と、壊れたときのリカバリの手順を確認しておきます。

バックアップのとり方

/var/lib/rpm を tar などで固めれば ok です。

リカバリの仕方

壊れると

 rpmdb: PANIC: fatal region error detected; run recovery
 error: db4 error(-30978) from db->open: DB_RUNRECOVERY: Fatal error, run database recovery
 rpmdb: PANIC: fatal region error detected; run recovery
 error: db4 error(-30978) from db->close: DB_RUNRECOVERY: Fatal error, run database recovery
 error: cannot open Group index using db3 -  (-30978)

このようなエラーが延々と出ます。 こうなってしまったら RPM DB を再構築するために

 sudo rpm --rebuilddb

を実行してください。/var/lib/rpm/Packages さえあれば再構築できるようです。

テスト用の RPM DB を用意する

RPM DB の位置は RPM の root という設定項目で決まります。デフォルトでは /var/lib/rpm に作られます (この場合 root=/ です。どこに root を設定したとしても必ず #{root}/var/lib/rpm ディレクトリは作られます)。 root を明示的に指定するには –root オプションを与えます。 例えば

 rpm --initdb --root ~

とすれば ~/var/lib/rpm/Packages という DB (Berkeley DB) が作られます。 すでに DB が存在するときは rpm –initdb は何も行いません。 でも、からっぽの DB を用意してもあまり使い道がないですね。

ruby-rpm での –initdb, –rebuilddb

ruby-rpm 上でも rpm –initdb, rpm –rebuilddb と同様の操作を行えます (あまり使う機会はないかもしれませんけど)。

  • rpm –initdb
 #!/usr/bin/ruby
 require 'rpm'
 RPM::DB.init
  • rpm –rebuilddb
 #!/usr/bin/ruby
 require 'rpm'
 RPM::DB.rebuild

ruby-rpm を使ってみよう

ruby-rpm を使って RPM DB に問い合わせる方法、パッケージをインストール・アップグレード・削除する方法を見ていきます。 対応する rpm コマンドも書いておきます (オプション・フラグの類は今回扱いません)。

RPM DB に問い合わせる (rpm -q)

インストール済みのパッケージについて問い合わせるには RPM::DB オブジェクトを用います。

  • rpm -q ruby-rpm
 #!/usr/bin/ruby
 require 'rpm'
 db = RPM::DB.open
 db.each_match(RPM::TAG_NAME, "ruby-rpm") {|i|
   p i
 }

RPM::DB.open で RPM DB を開き、db.each_match(RPM::TAG_NAME, “ruby-rpm”) で ruby-rpm という名前を持つパッケージを問い合わせています。 同じ名前であってもバージョンが異なるパッケージをインストールできますから、each_match の結果は複数になることもあります。

 #!/usr/bin/ruby
 db = RPM::DB.open
 db.each_match(RPM::DBI_LABEL, "ruby-rpm") {|i|
   p i
 }

RPM::DBI_* 系のタグは大きなくくりになりますが、RPM::TAG_NAME と RPM::DBI_LABEL で違いはないと思われます。

外部イテレータ

db.each, db.each_match は内部で db.init_iterator, mi.next_iterator を呼び出しており、外部イテレータから内部イテレータへ変換しています。

 # mi は RPM::MatchIterator オブジェクト
 mi = db.init_iterator(RPM::TAG_NAME, "ruby-rpm")
 while i = mi.next_iterator
   p i
 end

db.each_match を Ruby で実装すれば次のようになります。

 def db.each_match(tag, val)
   mi = db.init_iterator(tag, val)
   while i = mi.next_iterator
     yield i
   end
 end

引数の形式

引数の形式として正規表現やグロブを与えることもできます。 現在の ruby-rpm の仕様では、引数の形式を指定するために mi.set_iterator_re を用いなければならず、結果的に外部イテレータの形で記述する必要があります。 rpm コマンドではグロブ形式で引数を与えることができます。 これを ruby-rpm で表現するためには、次のように書きます。

  • rpm -qa “py*”
 mi = db.init_iterator(RPM::DBI_PACKAGES, nil)
 mi.set_iterator_re(RPM::TAG_NAME, RPM::MIRE_GLOB, "py*")
 while i = mi.next_iterator
   p i
 end

mi.set_iterator_re の第二引数 mode=RPM::MIRE_GLOB が引数の形式を表します。RPM::MIRE_GLOB のほかに RPM::MIRE_DEFAULT, RPM::MIRE_STRCMP, RPM::MIRE_REGEX を与えることができます。

-q –requires

rpm -q (–query) のオプションは多彩です。 これらの多くは popt ライブラリによる alias として実現されています。 alias の設定は rpm-4.3.2 ならば /usr/lib/rpm/rpmpopt-4.3.2 にあります。また、ユーザごとに ~/.popt に記述することができます。

この設定を見ると

 -q --requires

 -q --qf "[%{RequireName} %{RequireFlags:depflags} %{RequireVersion}\n]"

と同等であることがわかります。 –qf (–queryformat) は ruby-rpm では RPM::Package#sprintf として指定できます。

  • rpm -q –requires ruby-rpm
  • rpm -qR ruby-rpm
 #!/usr/bin/ruby
 require 'rpm'
 db = RPM::DB.open
 db.each_match(RPM::TAG_NAME, "ruby-rpm") {|i|
   puts i.sprintf("[%{RequireName} %{RequireFlags:depflags} %{RequireVersion}\n]")
 }

-qf (RPM::TAG_BASENAMES)

ファイル名からパッケージを得るには -qf を用います。これを ruby-rpm で実現するためには、タグとして RPM::TAG_BASENAMES を与えます。

  • rpm -qf /bin/sh
 #!/usr/bin/ruby
 require 'rpm'
 db = RPM::DB.open
 db.each_match(RPM::TAG_BASENAMES, "/bin/sh") {|i|
   p i
 }

パッケージをアップグレードする (rpm -U)

RPM DB を更新する処理 (トランザクションを扱う処理) では、RPM::DB.open の第 1 引数として true を指定する必要があります。 また、rpm コマンドと同様、RPM DB を更新する場合は sudo が必要になります。

パッケージをアップグレードするには次のようにします。

  • rpm -U ~/PKGS/i586/mph-0.92.7-11m.i586.rpm
 #!/usr/bin/ruby
 require 'rpm'
 db = RPM::DB.open(true)
 db.transaction {|ts|
   p ts
   pkg_filename = "#{ENV["HOME"]}/PKGS/i586/mph-0.92.7-11m.i586.rpm"
   pkg = RPM::Package.open(pkg_filename)
   pkg_key = "mph"
   ts.upgrade(pkg, pkg_key)
   ts.check
   ts.order
   pkg_file = nil # block global scope
   ts.commit {|cb|
     p [cb.type , cb.key , cb.package , cb.amount , cb.total]
     case cb.type
     when RPM::CALLBACK_INST_OPEN_FILE
       if cb.key == pkg_key
         pkg_file = open(pkg_filename)
         next pkg_file
       end
     when RPM::CALLBACK_INST_CLOSE_FILE
       if cb.key == pkg_key
         pkg_file.close
       end
     end
   }
   p :last
 }
 p :done

ちょっと長くて読みにくいですが、長くなっている commit {} のブロックを外してみてください。

 db.transaction {|ts|
   p ts
   pkg_filename = "#{ENV["HOME"]}/PKGS/i586/mph-0.92.7-11m.i586.rpm"
   pkg = RPM::Package.open(pkg_filename)
   pkg_key = "mph"
   ts.upgrade(pkg, pkg_key)
   ts.check
   ts.order
   ts.commit {}
   p :last
 }

ts.upgrade(pkg, pkg_key) でアップグレードトランザクションを追加しています。

ts.check で依存関係を確認します。

ts.order で処理順序を決定します。

ts.commit でトランザクションを実行します。

note

RPM の API ではトランザクションセットにトランザクションを追加していくと考えます。 db.transaction が返す ts は Transaction Set なのです。 でもロールバックは ts 単位なんですよね……謎。 ちょっとこの辺り ruby-rpm の名前付け (というより RPM の API 名) は混乱しています。

ts.commit に渡すブロック

   pkg_file = nil # block global scope
   ts.commit {|cb|
     p [cb.type , cb.key , cb.package , cb.amount , cb.total]
     case cb.type
     when RPM::CALLBACK_INST_OPEN_FILE
       if cb.key == pkg_key
         pkg_file = open(pkg_filename)
         next pkg_file
       end
     when RPM::CALLBACK_INST_CLOSE_FILE
       if cb.key == pkg_key
         pkg_file.close
       end
     end
   }

commit にブロックを渡すと、コミット処理の進行状況に応じてブロックが評価されます (このようなブロックの使い方をコールバック (callback) といいます)。 ブロックを渡さないとデフォルトのコールバックを実行します (キャラクタベースのプログレスバーが表示されます)。

ts.commit を呼ばなかったときは ts.transaction {} を抜けるときに内部で ts.commit が呼ばれます。 この際、ブロック呼び出しが妙なことになって、ts.transaction に渡したブロックが ts.commit を読んだ際にも呼ばれてしまいます (これはおそらく Ruby の不具合だと思います5)。

パッケージをインストールする (rpm -i)

パッケージをインストールするときは、アップグレードの例の ts.upgrade を ts.install に置き換えればいいです。

  • rpm -i [package]

パッケージを削除する (rpm -e)

パッケージを削除するには次のようにします。

  • rpm -e ruby-rpm
 #!/usr/bin/ruby
 require 'rpm'
 db = RPM::DB.open(true)
 db.transaction {|ts|
   p ts
   ts.delete("ruby-rpm")
   ps = ts.check
   p ps
   ts.abort if ps
   ts.order
   ts.commit {}
   p :last
 }
 p :done

ts.check の結果、依存関係に問題があれば ps (Problem Set) は RPM::Require オブジェクトを要素とする配列になります。問題がなければ ps は nil になります。

削除のトランザクションでは (インストールやアップグレードとは違って) 依存関係に問題があっても ts.commit は abort しないことに注意してください。 明示的に ts.abort しなければ、依存関係に問題があっても削除のトランザクションは実行され、あろうことか成功してしまいます (そしておそらく RPM DB は panic に陥り、–rebuilddb のお世話になります)。

まとめ

  • ruby-rpm は RPM の Ruby 拡張ライブラリです。
  • ruby-rpm を用いると、rpm コマンドを経由せずに Ruby から RPM DB を扱え、問い合わせ・インストール・アップグレード・削除の処理を実行できます。これらの機能を元に、さまざまなツールを開発できます。
  • ruby-rpm は現在 Momonga Project で開発が続けられています。開発の成果はパッチとして提供されます。現在いくつか不具合が見つかっています。rpm-python などと比較して、見直すべき点もあります。
  • ruby-rpm の開発を通して RPM に対する理解を深めることができます。

参考文献

[1] Red Hat RPM Guide Eric Foster-Johnson Published by Wiley Publishing, Inc. Copyright (c) 2003 by Red Hat, Inc.

著者について

もりきゅうは異業種社長 4 名 + 主婦 2 名 + 私という妙なパーティで運営している会社ミッタシステムのプログラマです。

著者の連絡先は moriq@moriq.com です。

  1. 本稿を書き始める前には、これらの処理は当然できるものと考えていました。しかし、テストしてみると不具合が見つかりました。幸いなことに直し方が判ったので、パッチを投げつつその成果を元に執筆することになりました。これぞ執筆駆動開発! 

  2. 実は、ruby-rpm の開発過程も書くつもりでした。しかし、ruby-rpm が何なのか事前に説明しておかないと話を始められないのでした。というわけで、このような紹介記事になりました。 

  3. 現在パッチは増えています。 

  4. http://www.mars.dti.ne.jp/~sohda/cygwin/holon.html に詳しい。 

  5. [[ruby-dev:24252]] で報告済み