ActivePerlのオフライン構築

ActivePerl 本体は実行インストーラで何時でもインストールできるが、ppm や cpan で組み込む追加モジュールはオンライン構築が前提になっている。だが世間から断絶された秘匿ネットワークや、そもそもインターネットに接続するという概念のない PCやサーバに納品したり、機材リプレースで開発環境を再構築しなければならない場面では、USBメモリや DVD-Rからのオフラインインストールが出来なければ話にならない。そもそもバージョンが数世代古くなるとダウンロード元の ppmレポジトリが有料プラン専用に切り替えられて、いつもで気軽に再ダウンロードすることができなくなってしまう。それでは10年以上メンテ契約が続くような案件では、開発当時の状況保存は結構重要な問題になるので予防線を張っておくことは必要なのだ。


.ppmxファイルの保存

ppm install を実行すると、レポジトリから .ppmxファイルが tempディレクトリにダウンロードされてインストールされる。だがこのファイルはコマンド終了時に全削除されるため、普段目にすることがない。.ppmxファイル自体は ppm install の引数に渡すことでオフラインインストールできるため、消される前にファイルのコピーを別の場所に取っておけば、あとでおなじ環境を再構築することが容易くなる。

--- C:/Perl64/lib/ActivePerl/PPM/Client.pm      Tue Feb 16 06:02:11 2016
+++ D:/Perl64/lib/ActivePerl/PPM/Client.pm      Fri Mar 10 12:12:03 2017
@@ -1328,6 +1328,9 @@
                    if ($save_len != $len) {
                        die "Aborted download ($len bytes expected, got $save_len).\n";
                    }
+use File::Copy;
+my $File = File::Basename::basename($save, '.tgz');
+File::Copy::copy($save, $File.'.ppmx');
                }
                # XXX An MD5 checksum for the tarball would be a good thing
            }

5.18以降に通用するこのパッチを当てると、カレントディレクトリにダウンロードされた .ppmxファイルが残されるようになる。依存関係で複数のモジュールが取得された場合はその全てが残される。このコードを見れば解るが、 .ppmxファイルの実態は単なる .tgzアーカイブだ。わざわざ拡張子を代えているのは、そうしないと ppmコマンドがオフラインインストール用ファイル名として認識しないからである。

なお ppmコマンドは自前のデータベースとキャッシュでダウンロード履歴を管理しており、毎回必ずしもファイルダウンロードが発生するわけではない。情報は以下のフォルダに保存されているので、作業前にこれを空にしてやればよい。

%LOCALAPPDATA%\ActiveState\ActivePerl

保存しておいた .ppmxファイルは、ppm install でインストールすることが出来るが、依存関係にあるサブモジュールを先にインストールしておかないと、オンラインで取りに行こうとしてしまう。そこで複数の .ppmxファイルから .ppdファイルを取り出し、そこに書かれた依存関係リストを読み出し、優先順位を付けて順次処理することになる。

#!C:/Perl/bin/perl.exe
use utf8;  
use strict;  
use Archive::Tar;  
use File::Basename;  
use File::Find;  
use Cwd;

my $cwd = $ARGV[0] // getcwd;  
print "Search directory: ", $cwd, "\n";  
my(@ppmx_list, %ppmx_hash, %ppmx_path);  
find(sub {  
    if (m/\.ppmx$/io) {
        $ppmx_path{$_} = $File::Find::name;
        push @ppmx_list, $_;
    }
}, $cwd);
foreach my $ppmx (@ppmx_list) {  
    $ppmx_hash{$ppmx} = 9999;
}
foreach my $ppmx (@ppmx_list) {  
    my $tar = Archive::Tar->new();
    $tar->read($ppmx_path{$ppmx}, 1);
    foreach my $file ($tar->get_files()) {
        if ($file->{name} =~ /\.ppd$/io) {
            foreach my $line (split /\n/, $file->{data}) {
                if (my $require = ($line =~ m/REQUIRE NAME="([^\"]+)"/o)[0]) {
                    $require =~ s/::/-/go;
                    foreach my $hash (keys %ppmx_hash) {
                        $ppmx_hash{$hash}-- if 0 == index lc($hash), lc($require);
                    }
                }
            }
        }
    }
}
undef @ppmx_list;  
while (my($key, $var) = each %ppmx_hash) {  
    push @ppmx_list, sprintf "%04u %s", $var, $key;
}
foreach my $ppmx (sort {lc $a cmp lc $b} @ppmx_list) {  
    $ppmx =~ s/^\d+ //o;
    my $command = "ppm install ".$ppmx_path{$ppmx}." --nodeps";
    print $cwd, "> ", $command, "\n";
    system $command and die "$!";
    print "\n";
}

1;  
__END__  

このコードは .ppmxファイルを含む親ディレクトリを引数に取る。引数は複数でもよく、省略されればカレントディレクトリを起点にする。-nオプションは ppmコマンドに --nodeps を付加する。-n オプションなしで起動すると依存解決に失敗した場合(依存する .ppmxが見つからない場合)レポジトリからのダウンロードが生じる。


.ppmxファイルの作成

.ppmxファイルの実態は .tgzで固められた .ppdファイルと blibディレクトリだ。cpanでソースからビルドしたものを .ppmxファイルにして保存しておくと、再ビルドする手間を減らせる。cpanの作業ワークは C:\Perl64\cpan\build または C:\Perl\cpan\build にある。

例えば既に cpan install に成功した Foo::Bar モジュールに対応する Foo-Bar-0.1010-4UofsT というディレクトリがあったなら、まずそこに cd して dmake ppd を実行1する。すると Foo-Bar.ppd というファイルが作成されるだろう。

> cd C:\Perl64\cpan\build\Foo-Bar-0.1010-4UofsT
> dmake ppd
> type Foo-Bar.ppd
<SOFTPKG NAME="Foo-Bar" VERSION="0.1010">  
    <ABSTRACT>foobar foobar</ABSTRACT>
    <AUTHOR>Charlie Root &lt;foobar@example.com&gt;</AUTHOR>
    <IMPLEMENTATION>
        <PERLCORE VERSION="5,006,0,0" />
        <REQUIRE NAME="Carp::" />
        <REQUIRE NAME="Cwd::" VERSION="3.16" />
        <REQUIRE NAME="Exporter::" />
        <REQUIRE NAME="strict::" />
        <REQUIRE NAME="vars::" />
        <ARCHITECTURE NAME="MSWin32-x64-multi-thread-5.24" />
        <CODEBASE HREF="" />
    </IMPLEMENTATION>
</SOFTPKG>  

REQUIRE 行は依存をあらわす。ARCHITECTURE 行はインストール先のプラットフォームを限定するものだが、ピュアPerlで書かれた環境非依存モジュールの場合は noarch に書き換えることでインストール先を選ばないようにすることもできる。

<ARCHITECTURE NAME="noarch" />  

.ppmx(.tgz)ファイルを作るための tar+gzアーカイバは、C:\Perl64\bin に ptar.batとしてインストールされている。すでにパスは通っているので次のようにすれば .ppmxファイルができあがる。

> ptar -v -c -C -z -f foo-bar.ppmx blib foo-bar.ppd

この際注意が必要なのは、.ppmxと .ppdのファイル名は一致させておかなければならない点だ。.ppmxファイル名にバージョン番号を付加する一般的なルールに従う場合は、.ppdファイル名にもバージョン番号を付加しておく。もちろんそのバージョン番号は SOFTPKG行の VERSIONと一致しているべきだ。

> ren foo-bar.ppd foo-bar-0.1010.ppd
> ptar -v -c -C -z -f foo-bar-0.1010.ppmx blib foo-bar-0.1010.ppd

  1. dmake.exe は cpan初期化時に C:\Perl64\sit\bin にインストールされているはずだ。

RECENT LINKS