前回、Plamo-5.3の改訂版であるPlamo-5.3.1を紹介すると共に、パッケージ更新用ツールであるget_pkginfoスクリプトについて簡単に触れました。今回はこのget_pkginfoスクリプトについてもう少し詳しく解説しましょう。
スクリプトの基本アイデア
前回も触れたように、このスクリプトはPythonの辞書型データにパッケージ名やバージョン、ビルド番号などの情報を収め、それを可搬なファイルにしてやりとりしよう、というアイデアに基づいています。
Pythonの「辞書(dictionary)」は、他の言語では「連想配列(associative array)」とも呼ばれているデータ形式で、配列中の要素を指定するための添字に数字以外の任意のデータ、たとえば文字列などを指定することが可能なデータ形式です。
Plamo Linuxの各パッケージは"bash-4.2.53-x86_64-P1.txz"のように、"ベース名"-"バージョン"-"アーキテクチャ"-"ビルド番号"."拡張子"という形式になっています。このうち「ベース名」はシステム上で一意になっているので、ベース名を添字にした辞書を作れば、バージョンやビルド番号などを簡単に管理できそうです。
これらのデータを管理するために、当初はベース名を1つのリストに記録して、バージョンやビルド番号をそれぞれ異なる辞書に記録するようなデータ構造を考えていました。
しかし、こういう形でデータを持つと、パッケージ名のリストとバージョン、ビルド番号の辞書をそれぞれ個別に管理しないとならなくなり面倒です。もう少しシンプルにできないかな、と考えているうちPythonには「タプル(tuple)」と呼ばれるデータ形式があることを思いだしました。
「タプル」は、複数のデータをひとまとめにして扱うためのデータ構造で、文字列や数値といった異なる形式のデータを(...) 内にまとめてしまうことができます。この機能を使えば、Plamo Linuxの各パッケージの情報は、パッケージの「ベース名」をキーにした辞書に「バージョン」や「ビルド番号」をタプルにしたデータを登録することで表現できそうです。
加えてPythonには、あるスクリプトで作ったデータ構造をファイル等に書き出して、別のスクリプトで再現するためのpickleという機能があります。
辞書や連想配列と呼ばれるデータ構造では、キーと値を結びつけるために動的なハッシュテーブルなどを利用しており、メモリ上でのデータ配置などはスクリプトを実行するたびに異なってしまいます。そのため、通常、この種の動的なデータ構造を異なるスクリプト間で共有することはできません。しかしPythonでは、スクリプト内部のデータ配置などを共有するための仕様を定めており、その仕様に従ってデータを出力したり読み込んだりするのがpickle機能です。
たとえば、FTPサーバにある最新のパッケージの情報をftp_package{}というデータに記録して、data.pickleというファイルに書き出すには以下のような処理になります。
こうして作ったdata.pickleというファイルを別のマシンに持っていき、pickle.load()してやれば、FTPサーバ上で作った辞書をそのまま再現することができます。
タプルにまとめたパッケージの情報を辞書に整理し、そのデータをpickle経由でやりとりすれば、バージョンやビルド番号のチェックは辞書の値の比較だけで済む、そう思いついた時、このスクリプトの原型が出来上がりました。
スクリプトの解説
前節で紹介したように今回のスクリプトでは、FTPサーバにある最新パッケージの情報を辞書に記録しておき、それをローカルにインストールされたパッケージの情報の辞書と比較する、という処理になります。
そのため、まずはFTPサーバ側で最新パッケージの情報を記録するコードを書いてみました。
9行目までは文字コードやモジュール読み込み、各種変数の定義で、実際の処理は10行目からのループになります。
10行目からのループでは、32ビット用(x86)、64ビット用(x86_64)ごとに、/home/ftp/pub/Plamo-5.x/{x86,x86_64}/plamo/以下にあるファイルをPythonのos.walk()を用いて集め(12行目)、ファイルの拡張子が.txzあるいは.tgzの場合、ベース名やバージョン番号等の必要な情報を切り出して(18-21行目)、ベース名をキーとした辞書に記録していきます。
何度か例にあげた"bash-4.2.53-x86_64-P1.txz"の場合、18行目で('bash', '4.2.53', 'x86_64', 'P1.txz')と切り出され、22行目でallpkgs{}という辞書に
と記録されるわけです。
24行目からはこうして整理したデータをpickleで保存する処理で、この結果、サーバに置かれた最新パッケージの情報がallpkgs_x86.pickleとallpkgs_x86_64.pickleという2つのファイルに記録されることになります。
このファイルを用いてローカルにインストール済みのパッケージと比較するスクリプトはこんな風にしてみました。こちらは少し長いので適宜省略しつつ紹介します。
リスト中で省略しているget_arch()はuname -mの結果を元に32ビット環境か64ビット環境かを見分ける処理、get_localpkgs()は/var/log/packages/以下に記録されているインストール済みパッケージのリストから、バージョンやビルド番号を辞書に整理する処理です。
36行目のget_localpkgs()で、インストール済みの全パッケージのバージョンとアーキテクチャ名、ビルド番号をlocal_pkgs{}という辞書に記録します。一方、サーバ側で用意している最新パッケージの情報は、get_ftp_pkgs()を使ってftp_pkgs{}に読み込みます。
get_ftp_pkgs()で得られたFTPサーバ上のパッケージ情報のタプルにはダウンロードの際に必要となるパス名も記録されているので、パス名を除くようにデータを組み直し(42-43行目)、local_pkgs{}のタプルと比較(44行目)して、両者の間にバージョンやビルド番号に違いがあるかをチェックします。
違いがあった場合、45行目からの処理で、ローカルにインストールしたパッケージの情報と、サーバにあるパッケージの情報を表示します。省略した部分には、ダウンロード用にサーバにあるパッケージのURLを表示する処理があります。
51行目からは例外処理で、自前でビルドしたパッケージをインストールしているなど、local_pkgs{}に存在するキーがftp_pkgs{}に無い場合にその旨を表示します。Pythonでは、辞書に存在しないキーを指定した場合、"KeyError"と呼ばれる種類のエラーが発生し、そのままでは処理が止まってしまうものの、例外処理に登録することでエラーを積極的に利用することができます。
このようなごくシンプルなコードなものの、動かしてみると、ちゃんとパッケージのバージョンやビルド番号の違いを検出し、入手先のURLを示すことができました。
前回も触れたように、Plamo Linuxでは更新されたパッケージをいかに周知するかは長年の課題でした。その問題が「Pythonの辞書をpickleしてやりとりする」というシンプルな方法で解決できた時には、まさにアルキメデス同様「ユーレカ!」と叫びたい気分でした。
なかなか解決できなかった問題が、少し考え方や視点を変えてみるだけであっけないほど簡単に解決できる、そのパズル解きに似た快感が筆者をソフトウェアの世界から抜け出せなくしている気がします。
実のところ、今回紹介したスクリプトはget_pkginfo.pyの最初のバージョンで、実際に機能するかどうかを確認するために書いたレベルのコードです。
Plamo Linuxのメンテナ用MLにこのコードを投げたところ、
- 「表示されるパッケージをダウンロードできれば便利」、
- 「ダウンロードできるならupdatepkgで更新してしまってもいいのでは」、
- 「でも、いくつかのパッケージは単純にupdatepkgじゃ更新できないよね」、
- 「ダウンロード中の状態表示が欲しい」、
- 「ローカルに更新しているパッケージはチェック対象にして欲しくない」、
等々、さまざまな機能リクエストと共に、機能追加のためのパッチが集まりました。
そこで、メンテナの加藤さんがgithub上にget_pkginfoのリポジトリを作ってくれ、田向さんがどんどん新機能の追加やコードのリライトを行ってくれました。その結果、2ヵ月ほどの間にget_pkginfoのコードはここで紹介したバージョンの8倍近くのサイズになり、さまざまなオプションで動作を変更できるようになりました。
現在では、このスクリプトもPlamo Linuxの正式パッケージとして配布され、誰でも簡単にFTPサーバにある最新のパッケージに追従できるようになっています。
簡単なアイデアやコードが触媒となって興味ある人々の間でどんどん発展していき、元の作者が思いもしなかったレベルにまで成長していくというのはフリーソフトウェアの醍醐味です。linuxに比ぶべくもないものの、get_pkginfoは久しぶりにその面白さを感じさせてくれるコードになりました。