Module::Requires で依存モジュールをきっちりチェックB!

今月の advent calendar だけで通算27日分もの記事を書いている Yappo ですみなさんお元気ですか?

なんだか hacker track が人手不足なので今日ネタを書くために昨日思いついたモジュールを CPAN にアップロードした上で二度目の参戦をします。

http://github.com/yappo/p5-Module-Requires

まえおき

みなさんがコードを書く上で依存モジュールの管理に悩む。なんてのは誰しもが通る道だと思います。

もうすでに Makefile.PL の中に依存モジュールを書けば、依存モジュールを全部入れてくれるので悩む事は無いと思います。

さらには、テストケースで必要な依存モジュールのチェックなども Test::Requires が登場した事により、気楽にテストする事も出来ます。

だがしかし、プラガブルなモジュールなどが含まれるディストリビューションを作ったときに、このサブモジュールを使う時は、このモジュールも入れてね的に Makefile.PL に以下のような feature 句を入れるかと思います。

# Plack の Makefile.PL から拝借
feature 'FastCGI daemon and dispatcher',
    -default => 0,
    'FCGI' => 0.67,
    'FCGI::Client' => 0.02;

これは、実際のインストールフェーズでは以下のように依存モジュールを入れるか聞かれます。

[FastCGI daemon and dispatcher]
- FCGI                            ...missing.
- FCGI::Client                    ...missing.
==> Auto-install the 2 optional module(s) from CPAN? [n] 

ユーザがインストールする時に、ここで提示されたモジュールを入れてくれれば何も問題ないでしょう。

しかしながら、最初は要らないと思っていても後で使う気になった時には当然これら必須モジュールがはいっていないので「Can't Locate ...」などのエラーが出てきます。

不足してる依存モジュールが少数だったら許せるでしょうが、足りないモジュールがいっぱいあると「Can't Locate ...」と言われるたびに install するとかいうめんどくさい事になってしまいます。

簡単に言うと Module::Requires は、この「Can't Locate ...」エラーメッセージを一度に出して依存モジュールで足りてないモジュール群を一度に提示してあげるという事に使えるのです。

# もちろん、これを排除するには sub feature 的なモジュールを別ディストリにしちゃうのが一番綺麗でしょう

簡単な使い方

使い方はとても簡単です。

例えば Class::Trigger と Class::Accessor に依存してる場合には以下のように書きます。

use strict;
use warnings;
use Module::Requires 'Class::Trigger', 'Class::Accessor';
use Class::Trigger;
use Class::Accessor;

もし、両方ともインストールされてないときは下記のようなエラーメッセージを出力します。

Can't load Class::Trigger
Can't locate Class/Trigger.pm in @INC (@INC contains: ry) at (eval 1) line 2.
BEGIN failed--compilation aborted at (eval 1) line 2.

Can't load Class::Accessor
Can't locate Class/Accessor.pm in @INC (@INC contains: ry) at (eval 2) line 2.
BEGIN failed--compilation aborted at (eval 2) line 2.
 at lib/Module/Requires.pm line 105
        Module::Requires::import('Module::Requires', 'Class::Trigger', 'Class::Accessor') called at ./a.pl line 3
        main::BEGIN() called at lib/Module/Requires.pm line 3
        eval {...} called at lib/Module/Requires.pm line 3
BEGIN failed--compilation aborted at ./a.pl line 3.

ちゃんと、両方の「Can't Locate ...」エラーメッセージが同時に出てきます。

もちろん片方がインストールされてれば、片方だけのエラーメッセージを出すし10個くらいのモジュールをチェックしてて全部入ってなければ全部のエラーを出します。

バージョンの指定

もちろん通常の use と同じようにバージョンの指定もできます。

use strict;use warnings;
use Module::Requires
    'Class::Trigger'  => 0.99,
    'Class::Accessor' => 14.22;
use Class::Trigger;
use Class::Accessor;

こう書くと以下のようにバージョンがたりねー!と怒ります。

Class::Trigger version 0.99 required--this is only version 0.13
Class::Accessor version 14.22 required--this is only version 0.31 at lib/Module/Requires.pm line 105
        Module::Requires::import('Module::Requires', 'Class::Trigger', 0.99, 'Class::Accessor', 14.22) called at ./a.pl line 4
        main::BEGIN() called at lib/Module/Requires.pm line 5
        eval {...} called at lib/Module/Requires.pm line 5
BEGIN failed--compilation aborted at ./a.pl line 5.

細かいバージョンの指定

例えば、とあるモジュールが 0.10 まで出ていて 0.03 以上が入ってたら良いんだけど、 0.09 だけバグがあるので 0.03 以上で 0.09 以外のモジュールに依存したいとか書く必要が出てくる場合があると思います。

記憶が確かなら他の CPAN モジュールでも上記要求を満たすモジュールが入ってれば load するなんてのもありますが、 Module::Requires の機能としても実装してあります。

use strict;
use warnings;
use Module::Requires
    'Foo'  => [ '>' => 0.03, '!=' => 0.09 ],
    'Bar'  => [ '<=' => 0.02, '>=' => 0.01 ],
    'Baz'  => [ '<' => 0.08 ];
use Foo;

もしもインストールされている Foo のバージョンがが 0.09 であった場合下記のエラーを吐きます。

Foo version > 0.03 AND != 0.09 required--this is only version 0.09 at /lib/Module/Requires.pm line 105
        Module::Requires::import('Module::Requires', 'Foo', 'ARRAY(0x81948c)') called at ./a.pl line 4
        main::BEGIN() called at lib/Module/Requires.pm line 4
        eval {...} called at lib/Module/Requires.pm line 4
BEGIN failed--compilation aborted at ./a.pl line 4.

同時にロードする

実は上の方法では、 Module::Requires::* 以下の名前空間から各種モジュールを require してるだけなので use Module::Requires してる名前空間から正しく use するには別途 use ModuleName として書かないと正しく use 出来ませんでした。

これでは冗長なケースもあるので -autoload というオプションを付ける事により require チェックと同時に require && module->import を行うようになります。

# これは encode_base64 が入ってないのでだめよ
use strict;
use warnings;
use Module::Requires
    'MIME::Base64';
print encode_base64('last day');
# 正しく encode_base64 が load されてる
use strict;
use warnings;
use Module::Requires -autoload,
    'MIME::Base64';
print encode_base64('last day');
# use MIME::Base64; と同等
# decode_base64 しか export されてないので動かない
use strict;
use warnings;
use Module::Requires -autoload,
    'MIME::Base64' => {
        import => ['decode_base64'],
    };
print encode_base64('last day');
# use MIME::Base64 'decode_base64'; と同等
# これは encode_base64 だけを export してるので動く
use strict;
use warnings;
use Module::Requires -autoload,
    'MIME::Base64' => {
        import => ['encode_base64'],
    };
print encode_base64('last day');
# use MIME::Base64 'encode_base64'; と同等

このようにして、import メソッドに渡す引数を指定します。

use ModuleName () と同等にするには下記のように書きます。

# decode_base64 しか export されてないので動かない
use strict;
use warnings;
use Module::Requires -autoload,
    'MIME::Base64' => {
        import => [],
    };
print encode_base64('last day');
# use MIME::Base64 (); と同等

use Module () って空括弧を引数に渡すと import を呼ばなくなる仕様を忘れててすっかりドハマリしてバグ作ってましたが直しました><

autoload しつつ version していする

これも出来ます。

use Module::Requires -autoload,
    'MIME::Base64' => {
        import  => [qw/ encode_base64 /],
        version => 0.03,
    };
# use MIME::Base 0.03 qw( encode_base64 ); と同等

こんな風にシンプルなバージョンの指定から

use Module::Requires -autoload,
    'MIME::Base64' => {
        import  => [qw/ encode_base64 /],
        version => [ '>=' => 0.03, '!=' => '0.08' ],
    };

のような細かいバージョンの制御もできます。

まとめ

Module::Requires を使ってモジュールの依存を細かく定義して、万が一依存モジュールの条件を満たさない場合はユーザが楽できる用にエラーを出すという事を紹介しました。

バグ出しやらなんやらで賞味一時間強で創り上げました。

また、このモジュールの実際の仕様のネタ出しは lestrrat さんと nekokak さんにして頂きましたありがとうございます。

例によって英語ドキュメントが不足気味なので、興味を持たれた方は是非ともドキュメントを充実させてくれると嬉しいです。

ということで地味なモジュールで今年の JPerl Advent Calendar を締めくくりましたが、そんなんで良いんじゃないかとおもいます。

ではでは、関係者の皆さんお疲れ様でした。こんどは寿司屋でありましょう!