OrePANを作っていて一番難しいのはディストリビューションからモジュールのパッケージ名とバージョンを抜き出す部分。ExtUtils::MakeMakerやModule::Metadata、PAUSEのソースを参考にして書いているところ。
例えば、Log::Minimalであれば、パッケージを展開して
$ tar zxf Log-Minimal-0.04.tar.gz
$ cd Log-Minimal-0.04
$ find -name "*.pm" | grep "VERSION"
のような処理をします。(実際はperl)
これが一筋縄ではいかない作業でかなり苦労している
最初に引っかかったのがcommon::sense。これは*.pmファイルがない。実際にはsense.pm.PLというファイルがあり、実行することでperlのバージョンにあったモジュールを作りだす。
まずこれに対応するために、対象とするファイルを
\.pm(\.PL)$
この正規表現にする必要がある。また、sense.pm.PLではDATA以下にpackageとVERSIONがあるので、DATA以下もパースしなければならない。Module::Metadataではこれに対応できなかった。
次に実行しないとバージョンがわからないタイプ。Encodeでは
our $VERSION = sprintf "%d.%02d", q$Revision: 2.42 $ =~ /(\d+)/g;
と、VERSION行が書かれている。このような場合はバージョン行を特定したあと、evalで実行してバージョンを得る必要がある。ExtUtils::MakeMakerだと
if ( m{(?<!\\) ([\$*]) (([\w\:\']*) \bVERSION)\b .* =}x ) {
my $eval = qq{
package ExtUtils::MakeMaker::_version;
no strict;
BEGIN { eval {
undef *version;
require version;
"version"->import;
} }
local $1$2;
\$$2=undef;
do {
$_
};
\$$2;
};
local $^W = 0;
$result = eval($eval); ## no critic
}
こうやって実行している。OrePANではこのコードをコピーして使ってる
DBIもはまったものの一つ
# $Id: DBI.pm 14568 2010-12-14 15:23:58Z mjevans $
# vim: ts=8:sw=4:et
#
# Copyright (c) 1994-2010 Tim Bunce Ireland
#
# See COPYRIGHT section in pod text below for usage and distribution rights.
#
require 5.008_001;
BEGIN {
$DBI::VERSION = "1.616"; # ==> ALSO update the version in the pod text below!
}
これがDBI.pmの頭からのコピー。package宣言より先にpackage名を含んだVERSIONが来ている。なんとなくVERSION宣言がpackage宣言よりあとにあることを期待してコードを書いていると裏切られる例。
どうやって対応したかというと、
while (<$fh>) {
if ( m{^ \s* package \s+ (\w[\w\:\']*) (?: \s+ (v?[0-9._]+) \s*)? (?:\s+)?; }x ) {
push @pkgs, [$1, $2];
}
elsif ( m{(?<!\\) ([\$*]) (([\w\:\']*) \bVERSION)\b .* =}x ) {
.. 実行する部分 ..
push @pkgs, [$package, $version] if $package;
$pkgs[-1]->[1] = $version;
}
}
とりあえず見つけた順に配列に追加していって最後に判断する。package名がないVERSIONの場合はおそらくその前にpackage宣言があると仮定して、配列の一番最後のversionとして利用する。
DBIがそれなんだけど、1つのモジュール内で複数のpackage宣言がある場合、配列のなかから適したpackage名を選ぶ必要がある。そのために、OrePANではModule::Metadataを参考にファイル名と照らし合わせている
my $basename = fileparse("$parsefile");
$basename =~ s/\..+$//;
my @candidates = sort { !$a->[1] <=> !$b->[1] }
grep { $_->[0] =~ m/$basename$/ } @pkgs;
return @{$candidates[0]} if @candidates;
ぱっとみ分かりにくコードだけど、ファイル名と一致するモジュールをgrepして、バージョンがあるものを先に取り出している。そして見つかったものの最初を採用している。
ここまでやってきてまだ解決できないことがいくつかある。例えば Module::Setup::Flavor::PBP。コード中にバージョン名はなく、DATA以下のモジュールのテンプレート(フレーバ)に
package Module::Setup::Flavor::PBP;
use strict;
use warnings;
use base 'Module::Setup::Flavor::SelectVC';
1;
__DATA__
...
---
file: lib/____var-module_path-var____.pm
template: |
package [% module %];
use version; our $VERSION = qv('0.0.1');
このVERSION名を拾ってしまう。実際CPANでもそうなってる!
そしてこういうのも難しい(実際にみたわけではない
package Hoge;
use strict;
use warnings;
{
package Hoge::Foo;
our %foo = ();
}
our $VERSION = 0.01;
1;
こうなると構文を解析するか、実行してしまうしかないんじゃないかと思う。
かわった書き方をしたモジュールをCPANにアップロードした際、自分が指定したバージョンでインデックス登録されているか届くメールで確認したり、CPAN Searchで確認するのがいいかもしれない