飛行機の中で Haskell プロジェクトをビルドする

いや別に飛行機の中でビルドするのが主題なわけではないのですが、オフラインモードのことを機内モードと言いますからね。最近の飛行機は Wi-Fi の提供があったりするらしいですが。

さて、あなたの Haskell プロジェクトをオフラインモードでビルドすることができますか? まあ、今時オフライン環境も珍しいですし「そんな必要あるのか?」という感覚もあるかもしれません。Nix ではビルド再現性のためにオフライン環境でビルドできることが求められます1

Nix で Haskell プロジェクトをビルドするには Input Output(iohk.io)の作成した haskell.nix が使用されるのが普通です。

github.com

これは cabal ファイルや stack ファイル、もしくは plan ファイルを入力とし、Cabal のパッケージひとつにつきひとつの Nix デリベーションに翻訳するシステムです2。Cabal がやってることを Nix(と Haskell)で再実装したと言ってもいいんじゃないかと思います。

いやあ、これが「結構大掛かりなシステムだなあ」と思うわけですよね。「もうちょっと cabal-install に頼ってビルドできないかなあ」と前々から思っていて、とりあえず Proof of Concept が動いたので紹介しようと思います。

コンセプト

自分のアイデアの肝腎は Cabal の「ローカルリポジトリー」を使うことです。普通はローカルリポジトリーを使うことはありません。Hackage つまりリモートリポジトリーを使いますから。ですが、Cabal は Hackage 以外のリポジトリーも使えるように作られています。一般的には ~/.config/cabal/config にある設定ファイルには下記のように書かれています3

repository hackage.haskell.org
    url: http://hackage.haskell.org/ 

このファイルは、cabal update の実行時にこのファイルがなければデフォルトの内容で生成されます。そのデフォルトの内容に Hackage の URL が記載されているので、みんなは Hackage からパッケージを取ってこられます。なので、やろうと思えば社内リポジトリーを立てて社内ライブラリーを配布したりすることもできます(認証は秘密鍵でできるっぽい)4。そして、この URL の部分は file+nocache というスキーマの URL も書くことができます5

repository my-local-repository
    url: file+noindex:///absolute/path/to/directory

/absolute/path/to/directoryディレクトリーにはパッケージの sdist ファイルを配置します。

/absolute/path/to/directory/
    foo-0.1.0.0.tar.gz
    bar-0.2.0.0.tar.gz

ここまで準備できれば後は簡単で、下記ステップでオフラインビルドができるようになります。

  1. ローカルリポジトリーのパスを書いた config ファイルを用意する
  2. 依存ライブラリーの sdist ファイルをローカルリポジトリーに配置する
  3. 環境変数コマンドライン引数で、用意した config ファイルを参照するようにする
  4. cabal build する

ツール

さて「ここまで準備できれば後は簡単」と書きましたが、依存ライブラリーの sdist を取得するはちょっと邪魔くさいです。間接依存や依存間のバージョン制約もあります。なので plan ファイルを元に sdist を取得するちょっとしたスクリプトを用意しました。

github.com

dependencies=$(cabal-plan topo --hide-builtin | grep -v ' ')for d in $dependencies
do
  url=https://hackage.haskell.org/package/$d/$d.tar.gz
  if [ $verbose -eq 0 ]
  then
    wget --quiet "$url"
  else
    wget --no-verbose "$url"
  fi
done

cabal-plan で依存のリストを取得して Hackage から wget でダウンロードするようにしています。

あとは flake.nixbuildPhase が肝腎なので気になる人は見ておくといいと思います6

github.com

runHook preBuild

# `cabal build` writes a file at a local repository,
# and so it must be writable.
cp -r --no-preserve=all $src/.local-repository .
# This cabal.config file declares using the local repository.
export CABAL_CONFIG=$src/cabal.config
# Set a writable directory for cabal
export CABAL_DIR=$TMPDIR/cabal
cabal="cabal --project-dir=$src --builddir=$TMPDIR --verbose"
$cabal v2-build --only-dependencies all
$cabal v2-build all

runHook postBuild

まだ確認できていないこと

cabal.project ファイルに source-repository-package が記載されていたらどうなるのか確認できていません。もしかすると cabal.project ファイルも切り替えないといけないかもしれないです。cabal.project.local に何か記載すれば source-repository-package を無効化できたりしないかなあ。誰か教えてください。


  1. 厳密に言うと Nix の用意する環境の中がオフラインで、Nix の処理系自体はネットワークで情報を取ってこれる。これは「Haskell の言語の中では副作用禁止だが、Haskell 処理系は副作用できる」みたいな話と似ているように思う。
  2. もしかしたらデリベーションは、コンポーネントつまり lib とか exe とか test とかと対応するのかも。
  3. ファイルの場所は環境や環境変数で変わる。詳しくはセクション 4.1.3. Directories に記載がある。
  4. 詳しくはセクション 4.1.4. Repository specification に記載がある。
  5. 詳しくはセクション 4.1.4.2. Local no-index repositories に記載がある。
  6. 記事を書いていたら「If the directory is not writable, you can append #shared-cache fragment to the URI, then the cache will be stored inside the remote-repo-cache directory.」というのを見つけたので、ローカルリポジトリーをコピーするステップはなくすことができそう。