BackPANで失せ物を探すB!

今年も残るところ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について

  1. Indexing Backpan
  2. Backpan Archeology
  3. Making Your Own CPAN
  4. Grokking The CPAN

ローカルCPAN関連(DarkPAN面白そう)

  1. 野良パッケージと依存PerlモジュールのインストールセットをCPAN::Mini::Injectで
  2. Create your own "BackPAN"
  3. A preview of DPAN
  4. Cataloging BackPAN: MiniCPAN done in 9 hours

gitpanについて

  1. gitPAN

Acme::ManekiNekoについて

  1. Acme::ManekiNeko/HISTORY
  2. Acme::ManekiNekoの画像
  3. 招き猫モジュール
  4. 素晴らしきPerlモジュールの世界

その他

  1. 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/などは初めて知りました。