なぜ依存関係管理が重要なのか
プロジェクトが成長すると、使用するCPANモジュールの数も増えていきます。開発環境、ステージング環境、本番環境で同じバージョンのモジュールが動作することを保証するのは、思いのほか難しい問題です。
よくある問題
1
2
3
4
5
6
7
8
9
10
11
|
# 開発者Aの環境
# Mojolicious 9.30 がインストールされている
use Mojolicious; # 問題なく動作
# 開発者Bの環境
# Mojolicious 8.50 がインストールされている
use Mojolicious; # 一部の機能が動かない!
# 本番環境
# Mojolicious 9.35 がインストールされている
use Mojolicious; # 開発環境と微妙に動作が違う...
|
このような「私の環境では動くのに…」という問題を解決するのが、Carton と cpanfile です。
cpanfileとは - 依存関係の宣言
cpanfileは、プロジェクトが必要とするCPANモジュールを宣言するための仕様です。Ruby の Gemfile、Node.js の package.json、Python の requirements.txt に相当します。
基本的な書き方
プロジェクトのルートディレクトリに cpanfile という名前のファイルを作成します:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# cpanfile
requires 'Mojolicious', '9.30';
requires 'DBIx::Class', '0.082842';
requires 'Plack', '1.0048';
# テスト時のみ必要
on 'test' => sub {
requires 'Test::More', '0.98';
requires 'Test::MockModule';
};
# 開発時のみ必要
on 'develop' => sub {
requires 'Perl::Critic';
requires 'Perl::Tidy';
};
|
cpanfileの文法は非常にシンプルで、Perlそのものです。requires 関数でモジュールとバージョンを指定するだけです。
バージョン指定の方法
cpanfileでは、柔軟なバージョン指定が可能です:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# cpanfile
# 正確なバージョン指定
requires 'Plack', '== 1.0048';
# 最小バージョン指定(これ以上)
requires 'Mojolicious', '>= 9.0';
# 範囲指定
requires 'DBI', '>= 1.643, < 2.0';
# バージョン指定なし(最新版を使用)
requires 'JSON';
# 特定バージョン以上、かつ特定バージョン未満
requires 'DBIx::Class', '>= 0.082840, < 0.083000';
|
フィーチャーによるオプショナルな依存関係
特定の機能を使う場合にのみ必要なモジュールは、feature を使って宣言できます:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# cpanfile
requires 'Mojolicious';
feature 'mysql', 'MySQL support' => sub {
requires 'DBD::mysql', '4.050';
};
feature 'postgresql', 'PostgreSQL support' => sub {
requires 'DBD::Pg', '3.14.0';
};
feature 'redis', 'Redis cache support' => sub {
requires 'Redis', '2.0';
requires 'Redis::Fast';
};
|
インストール時に、必要なフィーチャーを選択できます:
1
2
3
4
5
|
# MySQLサポートを含めてインストール
carton install --deployment --with-feature=mysql
# 複数のフィーチャーを有効化
carton install --with-feature=mysql --with-feature=redis
|
Cartonとは - 依存関係の分離管理
Cartonは、cpanfileに記述された依存関係を、プロジェクトごとに分離してインストール・管理するツールです。
なぜCartonが必要なのか
CPANモジュールを cpanm で直接インストールすると、システム全体(またはユーザー環境全体)にインストールされます。これには以下の問題があります:
- バージョンの競合: プロジェクトAはMojolicious 8.x、プロジェクトBは9.xが必要な場合、どちらか一方しか満たせない
- 環境の再現性: 「このモジュールのこのバージョンがインストールされている」状態を他の環境で再現するのが困難
- クリーンな環境: プロジェクトを削除してもモジュールが残り続ける
Cartonは、これらの問題を local/ ディレクトリへのローカルインストールで解決します。
Cartonのインストール
1
2
3
4
5
6
|
# cpanmを使ってCartonをインストール
cpanm Carton
# インストール確認
carton -v
# carton v1.0.34
|
Carton自体はシステム全体(またはユーザー環境)にインストールしますが、各プロジェクトの依存関係は local/ に分離されます。
Cartonの基本的な使い方
ワークフロー例
実際のプロジェクトでの使い方を見ていきましょう。
1. プロジェクトの初期化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# プロジェクトディレクトリを作成
mkdir my-app
cd my-app
# cpanfileを作成
cat > cpanfile << 'EOF'
requires 'Mojolicious', '9.30';
requires 'DBI';
requires 'DBD::SQLite';
on 'test' => sub {
requires 'Test::More', '0.98';
};
EOF
|
2. 依存関係のインストール
1
2
3
4
5
6
7
8
|
# cpanfileに基づいてモジュールをインストール
carton install
# 実行後のディレクトリ構造
# my-app/
# ├── cpanfile
# ├── cpanfile.snapshot # 自動生成される
# └── local/ # 依存モジュールがここにインストールされる
|
carton install を実行すると:
- cpanfileを読み取る
- 必要なモジュールを
local/ ディレクトリにインストール
- インストールされた正確なバージョン情報を
cpanfile.snapshot に記録
3. アプリケーションの実行
Cartonでインストールしたモジュールを使うには、carton exec を使います:
1
2
3
4
5
6
7
8
9
10
11
|
# 通常の実行(システムのモジュールを使用)
perl app.pl # NG: local/ のモジュールが見つからない
# Carton経由で実行(local/ のモジュールを使用)
carton exec -- perl app.pl # OK!
# plackupの場合
carton exec -- plackup app.psgi
# スクリプトの場合
carton exec -- perl -Ilib script/myapp.pl
|
carton exec は、local/ ディレクトリをモジュール検索パス(@INC)に追加して、指定されたコマンドを実行します。
アプリケーション例
実際のアプリケーションでCartonを使った例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#!/usr/bin/env perl
# app.pl
use strict;
use warnings;
use Mojolicious::Lite;
use DBI;
# データベース接続(DBD::SQLiteを使用)
my $dbh = DBI->connect('dbi:SQLite:dbname=app.db');
get '/' => sub {
my $c = shift;
$c->render(text => 'Hello from Carton-managed app!');
};
app->start;
|
1
2
3
4
5
6
|
# 依存関係をインストール
carton install
# アプリケーションを実行
carton exec -- morbo app.pl
# Server available at http://127.0.0.1:3000
|
cpanfile.snapshot - 完全な再現性
cpanfile.snapshot は、carton install 実行時に自動生成されるファイルで、インストールされたすべてのモジュールの正確なバージョンとその依存関係を記録します。
cpanfile と cpanfile.snapshot の違い
1
2
|
# cpanfile(開発者が書く)
requires 'Mojolicious', '>= 9.0'; # 「9.0以上」という緩い指定
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# cpanfile.snapshot(Cartonが生成)
# DATE: 2025-12-10T12:00:00
# PERL_VERSION: 5.38.0
DISTRIBUTIONS
Mojolicious-9.37
pathname: S/SR/SRI/Mojolicious-9.37.tar.gz
provides:
Mojo 9.37
Mojo::Base 9.37
# ... 全サブモジュールの正確なバージョン
requirements:
ExtUtils::MakeMaker 0
IO::Socket::IP 0.37
# ... 依存関係の正確なバージョン
|
cpanfile.snapshotがあることで、どの環境でも完全に同じバージョンのモジュールをインストールできます。
デプロイ時の使用
本番環境やCI環境では、--deployment オプションを使用します:
1
2
3
4
5
6
7
|
# デプロイメントモード(cpanfile.snapshotを使用)
carton install --deployment
# このモードでは:
# - cpanfile.snapshotに記録された正確なバージョンをインストール
# - cpanfile.snapshotがない場合はエラー
# - cpanfileとcpanfile.snapshotの不整合があればエラー
|
これにより、開発環境と全く同じバージョンのモジュール構成を本番環境に展開できます。
チーム開発での活用
推奨ワークフロー
開発者Aの作業(新しい依存関係を追加)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 1. cpanfileに新しい依存関係を追加
echo "requires 'JSON::XS';" >> cpanfile
# 2. インストール(cpanfile.snapshotが更新される)
carton install
# 3. 動作確認
carton exec -- perl app.pl
# 4. cpanfileとcpanfile.snapshotをコミット
git add cpanfile cpanfile.snapshot
git commit -m "Add JSON::XS dependency"
git push
|
開発者Bの作業(変更を取り込む)
1
2
3
4
5
6
7
8
|
# 1. 最新のコードを取得
git pull
# 2. 依存関係を更新(cpanfile.snapshotに従う)
carton install
# 3. そのまま開発を続行(追加の作業不要!)
carton exec -- perl app.pl
|
.gitignoreの設定
local/ ディレクトリはバージョン管理に含めず、cpanfile と cpanfile.snapshot だけを管理します:
1
2
3
|
# .gitignore
/local/
/.carton/
|
local/ ディレクトリには大量のファイルが含まれるため、Gitで管理するのは非効率です。cpanfile.snapshotがあれば、いつでも同じ環境を再現できます。
CI/CDでの利用
GitHub Actions での例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: '5.38'
- name: Cache Carton dependencies
uses: actions/cache@v3
with:
path: local
key: ${{ runner.os }}-carton-${{ hashFiles('cpanfile.snapshot') }}
restore-keys: |
${{ runner.os }}-carton-
- name: Install Carton
run: cpanm -n Carton
- name: Install dependencies
run: carton install --deployment
- name: Run tests
run: carton exec -- prove -lr t/
|
--deployment オプションにより、cpanfile.snapshotに記録された正確なバージョンがインストールされます。キャッシュを活用することで、2回目以降のビルドが高速化されます。
GitLab CI での例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# .gitlab-ci.yml
image: perl:5.38
cache:
paths:
- local/
before_script:
- cpanm -n Carton
- carton install --deployment
test:
script:
- carton exec -- prove -lr t/
deploy:
stage: deploy
script:
- carton install --deployment --without-develop
- rsync -av --exclude 'local' ./ user@server:/path/to/app/
only:
- main
|
Dockerでの利用
Dockerコンテナでも Carton は有効です。特に、マルチステージビルドと組み合わせると効果的です。
シンプルなDockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
FROM perl:5.38
WORKDIR /app
# Cartonをインストール
RUN cpanm -n Carton
# cpanfileとcpanfile.snapshotをコピー
COPY cpanfile cpanfile.snapshot ./
# 依存関係をインストール
RUN carton install --deployment --without-develop --without-test
# アプリケーションコードをコピー
COPY . .
# アプリケーションを実行
CMD ["carton", "exec", "--", "plackup", "-s", "Starman", "--workers", "4", "app.psgi"]
|
マルチステージビルドの例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# ビルドステージ
FROM perl:5.38 AS builder
WORKDIR /app
RUN cpanm -n Carton
COPY cpanfile cpanfile.snapshot ./
RUN carton install --deployment --cached
COPY . .
# 実行ステージ(軽量)
FROM perl:5.38-slim
WORKDIR /app
# ビルドステージからlocal/とアプリをコピー
COPY --from=builder /app/local ./local
COPY --from=builder /app .
# Cartonをインストール(execに必要)
RUN cpanm -n Carton
CMD ["carton", "exec", "--", "plackup", "-s", "Starman", "app.psgi"]
|
マルチステージビルドを使うことで、最終的なイメージサイズを小さくできます。
Docker Composeでの開発環境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
- carton-cache:/app/local
environment:
- PLACK_ENV=development
command: carton exec -- plackup -R lib,app.psgi app.psgi
volumes:
carton-cache:
|
ボリュームを使って local/ をキャッシュすることで、コンテナ再起動時の依存関係インストールを省略できます。
他の言語との比較
Carton/cpanfileは、他の言語の依存関係管理ツールと同様の思想で設計されています。
Ruby - Bundler/Gemfile
1
2
3
4
5
6
7
8
9
|
# Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.0'
gem 'pg', '>= 1.0'
group :development, :test do
gem 'rspec-rails'
end
|
1
2
|
bundle install
bundle exec rails server
|
対応するPerl版:
1
2
3
4
5
6
7
|
# cpanfile
requires 'Mojolicious', '~> 9.0';
requires 'DBD::Pg', '>= 3.0';
on 'test' => sub {
requires 'Test::More';
};
|
1
2
|
carton install
carton exec -- morbo app.pl
|
Node.js - npm/package.json
1
2
3
4
5
6
7
8
9
|
{
"dependencies": {
"express": "^4.18.0",
"pg": "^8.7.0"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
|
1
2
|
npm install
npm start
|
Perlとの対応:
1
2
3
4
5
6
7
|
# cpanfile
requires 'Mojolicious', '>= 9.0';
requires 'DBD::Pg', '>= 8.0';
on 'develop' => sub {
requires 'Test::More';
};
|
1
2
|
carton install
carton exec -- perl app.pl
|
Python - pip/requirements.txt
1
2
3
|
# requirements.txt
Django==4.2.0
psycopg2>=2.9.0
|
1
2
|
pip install -r requirements.txt
python manage.py runserver
|
Perl版:
1
2
3
|
# cpanfile
requires 'Mojolicious', '== 9.30';
requires 'DBD::Pg', '>= 3.14.0';
|
1
2
|
carton install
carton exec -- perl app.pl
|
ベストプラクティス
1. cpanfile.snapshotは必ずコミットする
1
2
|
git add cpanfile cpanfile.snapshot
git commit -m "Update dependencies"
|
cpanfile.snapshotがないと、チームメンバーや本番環境で同じバージョンを再現できません。
2. 定期的に依存関係を更新する
1
2
3
4
5
6
7
8
9
|
# 古いlocal/を削除
rm -rf local/
# 最新版で再インストール
carton install
# 動作確認後、cpanfile.snapshotをコミット
git add cpanfile.snapshot
git commit -m "Update dependencies to latest versions"
|
3. バージョン範囲を適切に指定する
1
2
3
4
5
6
7
8
|
# ✗ 悪い例:バージョン指定なし(予期しない破壊的変更のリスク)
requires 'Mojolicious';
# ○ 良い例:メジャーバージョンを固定
requires 'Mojolicious', '>= 9.0, < 10.0';
# ○ または最小バージョンのみ指定
requires 'Mojolicious', '>= 9.30';
|
4. 開発用/テスト用依存関係を分ける
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# cpanfile
# 本番環境でも必要
requires 'Mojolicious';
requires 'DBI';
# テスト時のみ
on 'test' => sub {
requires 'Test::More';
requires 'Test::MockModule';
requires 'Devel::Cover';
};
# 開発時のみ
on 'develop' => sub {
requires 'Perl::Critic';
requires 'Perl::Tidy';
requires 'Reply'; # REPL
};
|
デプロイ時は不要な依存関係を除外:
1
|
carton install --deployment --without-develop --without-test
|
5. プライベートCPANミラーの活用
社内プライベートモジュールがある場合:
1
2
3
4
5
6
7
|
# cpanfile
requires 'PublicModule::FromCPAN';
# プライベートリポジトリから
requires 'MyCompany::InternalModule',
git => 'https://github.com/mycompany/internal-module.git',
ref => 'v1.0.0';
|
または、mirror 設定でプライベートCPANミラーを使用:
1
2
3
|
# 環境変数で指定
export PERL_CARTON_MIRROR="https://cpan.internal.company.com"
carton install
|
トラブルシューティング
local/ディレクトリが壊れた
1
2
3
|
# local/を削除して再インストール
rm -rf local/ .carton/
carton install
|
cpanfile.snapshotとcpanfileの不整合
1
2
3
4
5
6
7
8
9
10
11
|
# --deploymentモードでエラーが出る場合
carton install --deployment
# Finding mirrors...
# ERROR: cpanfile.snapshot is out of date. Run `carton install` to update it.
# 開発環境で再インストール
carton install
# cpanfile.snapshotが更新されるのでコミット
git add cpanfile.snapshot
git commit -m "Update cpanfile.snapshot"
|
特定のモジュールのバージョンを固定したい
1
2
3
|
# cpanfile
# 正確なバージョンを指定
requires 'Mojolicious', '== 9.30';
|
その後、再インストール:
1
2
|
rm -rf local/
carton install
|
Carton経由とシステムのモジュールが競合
1
2
3
4
5
6
7
8
|
# Carton経由で実行されているか確認
carton exec -- perl -V
# @INCを確認(local/が含まれているはず)
carton exec -- perl -e 'print "$_\n" for @INC'
# /path/to/project/local/lib/perl5
# /path/to/project/local/lib/perl5/x86_64-linux
# ...
|
必ず carton exec を使ってコマンドを実行してください。
インストールが遅い
1
2
3
4
5
|
# 並列インストールで高速化
carton install -j 4 # 4並列
# ミラーサイトを指定
carton install --mirror http://cpan.cpantesters.org/
|
実際のプロジェクト例
実際のWebアプリケーションプロジェクトでの構成例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
my-web-app/
├── cpanfile # 依存関係定義
├── cpanfile.snapshot # バージョンロック
├── app.psgi # PSGIアプリケーション
├── lib/
│ └── MyApp/
│ ├── Controller/
│ └── Model/
├── t/ # テスト
│ ├── 00_compile.t
│ └── 01_basic.t
├── public/ # 静的ファイル
│ ├── css/
│ └── js/
├── script/ # ユーティリティスクリプト
│ └── myapp.pl
├── .gitignore # local/ を除外
├── Dockerfile # Docker設定
└── README.md
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# cpanfile
requires 'Mojolicious', '>= 9.0';
requires 'Mojo::Pg';
requires 'DBIx::Class', '0.082842';
requires 'DateTime';
requires 'Email::Sender';
requires 'Crypt::Passphrase::Argon2';
on 'test' => sub {
requires 'Test::More', '0.98';
requires 'Test::Mojo';
requires 'Test::PostgreSQL';
};
on 'develop' => sub {
requires 'Perl::Critic';
requires 'Perl::Tidy';
requires 'Devel::NYTProf'; # プロファイラ
};
|
1
2
3
4
5
6
7
8
9
10
|
# 開発開始
carton install
carton exec -- morbo app.psgi
# テスト実行
carton exec -- prove -lvr t/
# 本番デプロイ
carton install --deployment --without-develop --without-test
carton exec -- plackup -s Starman --workers 10 app.psgi
|
まとめ
Carton/cpanfileは、モダンなPerl開発に欠かせないツールです:
Carton/cpanfileのメリット
- 環境の再現性: cpanfile.snapshotにより、どの環境でも同じバージョンのモジュール構成を再現
- プロジェクトの分離: local/へのインストールにより、プロジェクト間でモジュールバージョンが競合しない
- チーム開発の効率化: 「私の環境では動く」問題を解消
- CI/CDとの親和性: 自動化されたビルド・テストが容易
- 他言語との共通パターン: Bundler、npm等と同様のワークフローで学習コストが低い
導入のステップ
- Cartonをインストール:
cpanm Carton
- cpanfileを作成: プロジェクトの依存関係を記述
- 依存関係をインストール:
carton install
- アプリを実行:
carton exec -- perl app.pl
- cpanfile.snapshotをコミット:
git add cpanfile cpanfile.snapshot
他の選択肢
Carton以外にも、Perl依存関係管理ツールはいくつか存在します:
- cpm: 高速な並列インストーラー(Cartonの代替として使える)
- Pinto: プライベートCPANリポジトリ管理
- Dist::Zilla: モジュール開発向けのツールチェーン(依存関係管理も含む)
しかし、チーム開発や本番環境での依存関係管理という観点では、Carton/cpanfileが最も広く使われているデファクトスタンダードです。
Perlの依存関係管理の進化
Perl の依存関係管理は、長い歴史の中で進化してきました:
- CPAN時代: モジュールを手動でインストール、バージョン管理なし
- cpanm時代: 自動依存関係解決、しかしプロジェクト単位の管理はなし
- Carton時代: プロジェクト単位での依存関係管理、バージョンロック(現在)
Carton/cpanfileは、Ruby の Bundler、Node.js の npm、Python の pip といった他言語のベストプラクティスに影響を受け、Perlに取り入れられた成果です。TMTOWTDI(やり方は一つじゃない)の精神を持つPerlですが、依存関係管理に関しては Carton/cpanfile がモダンな標準となっています。
参考リンク
Carton/cpanfileを活用して、安定したPerl開発環境を構築しましょう!チーム全体で同じ環境を共有できることは、開発効率の大幅な向上につながります。