cabal-plan-boundsの使い方|build-dependsの境界をビルドプランから自動更新
cabal-plan-boundsは、Haskellの.cabalファイルにあるbuild-dependsのバージョン境界を、実際のビルドプラン(plan.json)から導出して書き換えるコマンドラインツールです。手書きの版範囲は「本当はテストしていない版まで許可している」状態になりがちですが、CIで実際に通したビルドの記録をそのまま境界に反映することで、この乖離をなくせます。依存関係を図で可視化するツールではなく、境界を機械的に更新する点が特徴です。作者はJoachim Breitner氏、2024年4月公開の0.1.6.1が現行版です(最新はHackageで確認してください)。
まとめ:cabal-plan-boundsの要点
先に結論を整理します。細部は各セクションで補足します。
- 役割:
build-dependsのバージョン境界を、実際に通したビルドプランから逆算して更新する。依存の可視化ツールではない。 - 入力:各GHCバージョンでビルドして得た
plan.json(<builddir>/cache/plan.json)を複数渡す。 - 実行:
cabal-plan-bounds 各plan.json -c 対象.cabal。build-dependsだけを書き換える。 - 挙動:patchレベルを落として
^>= 1.2.3形式で書き込み(PVP前提)、同じ依存の全記述に同一境界を適用する。 - CI:
--dry-runで「境界が実ビルドと合っているか」を検査でき、ずれていればCIを落とせる。
導入前に、そもそも複数のGHC/依存版でCIを回している前提が要ります。以下でインストールから運用までを順に見ていきます。
cabal-plan-boundsとは何か
Haskellでは依存パッケージの版範囲をbuild-depends: base >=4.14 && <4.20のように手で書きます。ところがこの範囲は、実際に Haskell のプロジェクトを複数環境でビルドして初めて「どこまで動くか」が分かるものです。手書きの範囲は時間とともに実態からずれ、「テストしていない版を許可」「動くのに弾いている」という嘘になりがちです。
cabal-plan-boundsは、この境界を人手で維持するのをやめ、実際に成功したビルドプランから境界を逆算します。テスト済みの版だけが境界に反映されるため、範囲が実態と一致します。
cabal-plan-boundsが解決する課題
手書きのbuild-dependsには二つの失敗があります。範囲を広く取りすぎて未検証の版まで許可してしまうケースと、狭く取りすぎて実際には動く新しい版を弾いてしまうケースです。前者はユーザー環境でのビルド破損、後者は不要な互換性報告や採用機会の損失につながります。cabal-plan-boundsは、CIで実際に通した版の集合をそのまま境界にするため、どちらのずれも起きません。
cabal-planやcabal-boundsとの違い
名前が似たツールと混同されやすいので整理します。用途がそれぞれ異なります。
| ツール | 役割 | 入力 |
|---|---|---|
| cabal-plan-bounds | build-dependsの境界を更新 | 複数の実ビルドプラン(plan.json) |
| cabal-plan | ビルドプランの確認・可視化 | 単一のplan.json |
| cabal-bounds | 依存の版指定を管理・更新 | 導入済み/Hackageの版 |
| cabal gen-bounds | 境界候補の提案 | 現在の単一プラン |
「依存を図で見たい」のはcabal-plan、「複数環境の実績から境界を決めたい」のがcabal-plan-boundsです。現行の解説記事が同ツールを可視化ツールと説明していることがありますが、これは別ツールとの取り違えで、cabal-plan-boundsは可視化機能を持ちません。
Hackageからのインストール
Hackageから入れるのが基本です。GHCとcabalが入っていれば次のコマンドで導入できます。
cabal install cabal-plan-bounds
Nixを使う環境ではhaskellPackages.cabal-plan-boundsとしてnixpkgsにも収録されています。導入後はcabal-plan-bounds --helpでオプション一覧を確認できます。
基本的な使い方
手順は「各環境でビルドしてplan.jsonを集める」「集めたplan.jsonを渡して.cabalを更新する」の二段です。
各GHCでビルドしてplan.jsonを生成する
境界の根拠になるのはビルドプランです。境界に含めたいGHCバージョンごとに、ビルドディレクトリを分けてビルドします。GHCの新旧バージョンをまたいで通しておくほど、境界は実態に沿ったものになります。
cabal build -w ghc-8.10.7 --builddir dist-8.10.7
cabal build -w ghc-9.0.2 --builddir dist-9.0.2
cabal build -w ghc-9.4.4 --builddir dist-9.4.4
各ビルドは<builddir>/cache/plan.jsonにプランを書き出します(--builddirを指定しない既定はdist-newstyle/cache/plan.json)。実運用ではcabal.projectを版ごとに用意してindex-stateを固定し、再現性を確保します。
plan.jsonを渡してbuild-dependsを更新する
集めたplan.jsonを並べて対象の.cabalに適用します。
cabal-plan-bounds \
dist-8.10.7/cache/plan.json \
dist-9.0.2/cache/plan.json \
dist-9.4.4/cache/plan.json \
-c myproject.cabal
実行すると、渡したプランに現れる各依存の最小版と最大版から境界が計算され、build-dependsが書き換わります。触るのはbuild-dependsだけで、他のフィールドやコメントは保持されます。
主なオプション(–dry-run/–extend/–also)
コマンドの形は次のとおりです。
cabal-plan-bounds [-n|--dry-run] [--extend] [--also ARG] [PLAN...] [-c|--cabal CABALFILE]
- -n, –dry-run:ファイルを書き換えず、変更内容だけを表示する。CIでの検査に使う。
- –extend:既存の範囲を狭めず、広げる方向にだけ更新する。手で入れた下限を守りたいときに有効。
- –also ARG:プランに無い版を境界に足す。
pkg-1.2.3や"pkg ==1.2.3"の形で指定する。 - -c, –cabal CABALFILE:更新対象の
.cabalを指定する。
複数コンポーネントや条件分岐(if)で同じ依存が何度も書かれていても、cabal-plan-boundsは全箇所へ同一の境界を適用し、1行1依存の形へ整形します。
CIでの運用
境界の更新は書き込みだけでなく検査にも使えます。CIでは各GHCジョブがplan.jsonを生成するので、それらを集めて--dry-runを実行し、現在の.cabalと差分が出れば失敗させます。GitHub Actionsでの依存管理の自動化と同じ発想で、境界の陳腐化をプルリクエスト時点で検知できます。
cabal-plan-bounds -n \
dist-8.10.7/cache/plan.json \
dist-9.4.4/cache/plan.json \
-c myproject.cabal
差分が出た場合は--dry-runを外して再実行すれば、実ビルドに合わせて境界が更新されます。開発者は境界を手で書く必要がなくなり、「テストした版=許可する版」を常に一致させられます。
挙動と制限
導入前に押さえておきたい前提と制約があります。
- 編集範囲:
build-dependsのみを書き換える。他のメタデータは変更しない。 - PVP前提:patchレベルを落として
^>= 1.2.3のように書く。PVPに従うパッケージであることを前提にしている。 - 入力への依存:境界の質は渡すプランの数で決まる。ビルドしていない版は境界に入らないため、広い互換性を主張したいなら多くのGHC/stackage構成でビルドしておく必要がある。
- 適さない場面:単一環境でしかビルドしない小さなプロジェクトでは、複数プランを集める手間に見合わない。依存の図示が目的ならcabal-planを使う。
裏を返せば、複数のGHCバージョンを跨いでライブラリを提供している場合ほど効果が大きいツールです。CIをすでに多構成で回しているなら、その成果物を境界へ流し込むだけで導入できます。
よくある質問
cabal-plan-boundsとcabal-planは何が違いますか。
cabal-planはビルドプランを表示・可視化するツールで、cabal-plan-boundsは複数の実ビルドプランからbuild-dependsの境界を更新するツールです。名前は似ていますが用途は別です。
plan.jsonはどこにありますか。
cabal build実行時に<builddir>/cache/plan.jsonへ書き出されます。--builddirを指定しない既定ではdist-newstyle/cache/plan.jsonです。
手書きの下限を維持したまま上限だけ更新できますか。
--extendを付けると範囲を狭める方向の変更を行わず、広げる方向にだけ更新します。意図的に設定した下限を守りたいときに使います。
ビルドしていない版を境界に含められますか。
--also pkg-1.2.3で、プランに現れない版を明示的に境界へ足せます。ただし実際にビルド検証していない版を許可することになる点は理解して使ってください。
build-depends以外のフィールドも書き換わりますか。
書き換わりません。cabal-plan-boundsが編集するのはbuild-dependsだけで、書式やコメントは保持されます。