BackPANで失せ物を探す
今年も残るところ10日をきりました。皆様如何お過ごしでしょうか。今日はAcme話を自重するつもりのmakamaka_at_donzokoでございます。
さて、年の瀬ともなりますと大掃除なんかになりまして、無くしたと思っていたものが見つかったとか、あると思っていたものが見当たらない、なんてことがよくあります。見つかる方は良いのですが、気がついたら無くなってた、というのは困ります。
という強引な前振りですが、search.cpan.orgでモジュールを探していて、「あれ? 確か前はあったはずなのに……」なんて経験、ありませんか?
そう、例えば、今年の初めにはCPANにあったAcme::BabyEaterがなくなってたり、2003年には確かにあったAcme::ManekiNekoが見当たらなくなったり(と思ったら後にひょっこり帰ってきましたが)という具合にですね。
% cpan Acme::BabyEater
(中略)
Warning: Cannot install Acme::BabyEater, don't know what it is.
Try the command
i /Acme::BabyEater/
むむむ…… search.cpan.orgで調べるてみてもやっぱり見あたらない。
ディストリビューションがまるまる無くなってしまうのも困りものですが、そうでなくても結構やっかいなケースがあります。
例えば、あるモジュールのバージョンが上がったところ、インストールできなくなったり、今まで動いていたものとの互換性がなくなったり(すいません私、前科あります)ということが、ままあります。
特に古いバージョンのPerl(5.005系や5.6系)で動いていたモジュールがある時点で対応しなくなった上に、以前のモジュールがCPANから削除されていたりすると、まあ大変!(え? Perlのバージョン上げろと? 色々大人の事情でダメな場合がありますので……)
このように、あなたがお探しのモジュールがCPANから消えてしまった時は、BackPANに出かけましょう!
BackPAN "A Complete History of CPAN"は、PAUSE(Perl Authors Upload Server)にアップされたファイルを全て保存しています。そしてPAUSEからモジュールを削除しようとも、BackPANからは削除されません。ですので、BackPANはCPANの「歴史」であり、もはや顧みられなくなった多くのモジュールが静かに(?)眠っているのです。BackPANをCPAN墓場と呼ぶ人もいます。例1 例2
さて、BackPANに行けば、なんでもそろっています。行ってみましょう。
authors/id/....……CPAN ID別にディレクトリが分かれているので、ちょっとモジュールを探すのが面倒そうですね。
そこでParse::BACKPAN::Packagesの登場です。このモジュールは、BackPANのインデックスデータhttp://www.astray.com/tmp/backpan.txt.gzを利用して、ディストリビューション名や作者からBackPAN上のアドレスを教えてくれます。
#!/usr/bin/perl
use strict;
use warnings;
use Parse::BACKPAN::Packages;
my $p = Parse::BACKPAN::Packages->new();
my $distname = $ARGV[0] or die "Distribution name?";
my $check_version;
if ( $distname =~ /^(.+?)-([_.\d]+)$/ ) { # バージョンまで指定
$check_version = $2;
$distname = $1;
}
else { # P::B::PはFoo-Bar形式で受け付けるので、Foo::Bar形式にも対応
$distname =~ s/::/-/g;
}
# リストは日付の古い順
my @dists = $p->distributions( $distname ) or die "Can't find $distname";
my $file;
if ( defined $check_version ) {
for my $dist ( @dists ) {
if ( $dist->version == $check_version ) {
$file = $p->file( $dist->prefix );
last;
}
}
}
else {
$file = $p->file( $dists[ -1 ]->prefix );
}
die "$distname was found, but not version $check_version." unless $file;
print $file->url, "\n";
backpan_url.plという名前をつけて
% perl backpan_url.pl Acme::BabyEater
とやってみましょう。
http://backpan.cpan.org/authors/id/Z/ZO/ZOFFIX/Acme-BabyEater-0.04.tar.gz
が出力されました(http://backpan.cpan.org/ですが問題ありません)。
最初はインデックスデータを取りに行くため、初期化にかなり時間がかかるかもしれませんが、一度データを取得すると、1時間キャッシュされます。
上記のサンプルでは該当するディストリビューションの最新版を返しますが、Acme-Manekineko-0.01
のような形式にすれば、バージョン0.01を探します。
さて、これでモジュールは簡単に手に入るようになりました。後はファイルを展開してcpan .
とでもすれば良いでしょう。でもどうせなら、コマンド一発でインストールしたいです。
cpanコマンドからBackPANサイトにアクセスしてうまいことやってくれないかなあ、と考えたのですが、これはなかなか難しいようです(ローカルに自分の用のCPANサイトを作る方法がありますが、そこまで大掛かりにしたくない)。色々検討した結果、CPAN::Injectを使うことにします。
use strict;
use warnings;
use CPAN::Shell;
use CPAN::Inject;
my $cpan_inject = CPAN::Inject->new(
sources => "$CPAN_HOME/sources",
author => 'MAKAMAKA',
);
my $install_path = $cpan_inject->add( file => 'path/Not-In-CPAN-1.00.tar.gz' );
こうすると$CPAN_HOME/sources
下の適切な位置(例えば /home/makamaka/.cpan/sources/authors/id/M/MA/MAKAMAKA)にNot-In-CPAN-1.00.tar.gzをコピーしてCHECKSUMを生成してくれます(new
の代わりにfrom_cpan_config
を使えばCPAN::Configから自動的に$CPAN_HOME/sources
を設定)。
ここまでくると、後はCPAN::Shellを使ってinstallも簡単。
CPAN::Shell->install( $install_path );
うまくいけばいつものようにインストールされます。先に載せたbackpan_url.plでURLを取得、ファイルをGETして上記のサンプルコードで適宜設定してしまえば、一連の作業を自動化できますね。
というわけで、上記の処理を簡単に行うためにBackPAN::Downloaderを作ってみました。
package BackPAN::Downloader;
use Mouse;
use Parse::BACKPAN::Packages;
use CPAN::Inject;
use LWP::UserAgent;
use CPAN::Debug;
use CPAN::Shell;
use Path::Class;
use Try::Tiny;
use Cwd;
use Data::Dumper;
our $VERSION = '0.01';
has distfile => ( is => 'rw', isa => 'Parse::BACKPAN::Packages::Distribution|Undef' );
has filedata => ( is => 'rw', isa => 'Parse::BACKPAN::Packages::File|Undef' );
has dist_is_found => ( is => 'rw', isa => 'Bool' );
has temp_dir => ( is => 'rw', isa => 'Str', default => './' );
has ua => ( is => 'rw', isa => 'LWP::UserAgent', default => sub { LWP::UserAgent->new } );
has error => ( is => 'rw', isa => 'Str|Undef' );
no Mouse;
sub reset {
my ( $self ) = @_;
$self->dist_is_found( 0 );
$self->distfile( undef );
$self->filedata( undef );
$self->error( undef );
}
sub lookup {
my ( $self, $distname ) = @_;
my $check_version;
$self->reset;
if ( $distname =~ /^(.+?)-([_.\d]+)$/ ) { # バージョン指定なので正式名称
$check_version = $2;
$distname = $1;
}
else {
$distname =~ s/::/-/g; # Foo-BarだけでなくFoo::Barでも検索できるように
}
my $p = Parse::BACKPAN::Packages->new();
# リストは日付の古い順
my @dists = $p->distributions( $distname );
my $found;
if ( defined $check_version ) {
for my $dist ( @dists ) {
if ( $dist->version == $check_version ) {
$found = $dist;
last;
}
}
}
else {
$found = $dists[ -1 ] if ( @dists );
}
if ( $found ) {
$self->distfile( $found );
$self->filedata( $p->file( $found->prefix ) );
$self->dist_is_found( 1 );
}
else {
$self->error( "Can't find $distname." );
}
return $found;
}
sub download {
my ( $self ) = @_;
unless ( $self->dist_is_found ) {
$self->error('donwload() must be called after the dist file was found.');
return;
}
my $url = $self->filedata->url;
my $file = Path::Class::File->new( $self->temp_dir, $self->distfile->filename );
return 1 if ( -s $file );
my $ua = $self->ua;
my $res = $ua->get( $url );
if ( $res->is_success ) {
my $fh = $file->openw();
unless ( $fh ) {
$self->error( "Can't open file." );
return;
}
print $fh $res->content;
}
else {
$self->error( "Can't download, status line is " . $res->status_line );
return;
}
}
sub install {
my ( $self, %opts ) = @_;
unless ( $self->dist_is_found ) {
$self->error('donwload() must be called after the dist file was found.');
return;
}
my $distfile = $self->distfile;
my $cpan_inject = CPAN::Inject->from_cpan_config( author => $distfile->cpanid );
my $installed;
my $file = Path::Class::File->new( $self->temp_dir, $distfile->filename );
my $cwd = getcwd;
try {
my $inst_path = $cpan_inject->add( file => $file );
CPAN::Shell->install( $inst_path );
$installed = 1;
} catch {
$self->error( @_ );
};
chdir( $cwd );
if ( $installed and $opts{ delete_saved_file } ) {
unlink($file) or Carp::carp("Can't delete saved file $file. $!");
}
return $installed;
}
#------- backpan_inst.pl
package main;
use strict;
use warnings;
@ARGV or die "Distribution name?";
my $backpan = BackPAN::Downloader->new( temp_dir => './tmp' );
for my $distname ( @ARGV ) {
$backpan->reset;
unless ( $backpan->lookup( $distname ) ) {
printf( "%s was not found.\n", $distname );
next;
}
printf( "Found %s in %s\n", $distname, $backpan->filedata->url );
$backpan->download() or die sprintf("Can't download. (%s)", $backpan->error);
$backpan->install( delete_saved_file => 1 ) or die "Can't install, " . $backpan->error;
}
さあ、試してみましょう。ダウンロードしたファイルを一時的に保存するtmpディレクトリを作成して
% perl backpan_inst.pl Acme::BabyEater
例によってデータ取得に時間がかかるかもしれませんが、
Found Acme::BabyEater in http://backpan.cpan.org/authors/id/Z/ZO/ZOFFIX/Acme-BabyEater-0.04.tar.gz
Going to read '/home/makamaka/.cpan/Metadata'
Database was generated on Sat, 19 Dec 2009 01:30:46 GMT
CPAN: YAML loaded ok (v0.68)
Running make for Z/ZO/ZOFFIX/Acme-BabyEater-0.04.tar.gz
Checksum for /home/makamaka/.cpan/sources/authors/id/Z/ZO/ZOFFIX/Acme-BabyEater-0.04.tar.gz ok
(..skip..)
ZOFFIX/Acme-BabyEater-0.04.tar.gz
./Build install -- OK
うまくいきました。上のサンプルはhttp://github.com/makamaka/perl-backpan-downloaderにあります。
そうそう、副次効果がありました。Acme::Tinyのように、cpan
コマンドからインストールするためにはCPAN IDとバージョンを調べて
cpan DMUEY/Acme-Tiny-0.4.tar.gz
しないといけないディストリビューションでも、
% perl backpan_inst.pl Acme::Tiny
で、一発インストール可能です。素晴らしい! これでインデックス化されないAcme7ディストリも怖くない!
それから、BackPANの他にも、最近はschwern氏によってCPANの歴史をgithubに移そうという試みがされています(gitpan)。
さて、それでは最後に、冒頭で取り上げたAcme::ManekiNekoについてのちょっといい話をいたしましょう。
2003年の後、作者はこのモジュールをCPANから削除します。ところが、このモジュールを愛する人たちがいることを知ります。しかし、時既に遅し。作者はコードを失っていました。
おお、なんてことでしょう!
作者は自分を責めます。
しかし!
そう、BackPANがあったのです!
こうして2008年に再びAcme::ManekiNekoはCPANに復活するのでありました。クリスマスの季節に相応しい素敵な物語ですね。
明日はka2uさんです。ではでは~
参考:
BackPANについて
ローカルCPAN関連(DarkPAN面白そう)
- 野良パッケージと依存PerlモジュールのインストールセットをCPAN::Mini::Injectで
- Create your own "BackPAN"
- A preview of DPAN
- Cataloging BackPAN: MiniCPAN done in 9 hours
gitpanについて
Acme::ManekiNekoについて
その他
- Mac OS X 10.6(Snow Leopard)にPerl 5.005をインストールする ↑ここでmiyagawaさんがコメントしているhttp://cp5.5.3an.barnyard.co.uk/、http://cp5.6.2an.barnyard.co.uk/などは初めて知りました。