NixによるHaskell開発環境の構築

Nix package manager によって Haskellスクリプティングおよびパッケージ開発の環境構築をしていく。

こいついつも環境構築してんな

環境構築以外はブログに書くような一般性のあることをやっていないということで……。

ここで触れられないこと

  • cabal.project によるマルチプロジェクトを扱う方法
  • 外部の nix store を使うこと
  • nix の新しいコマンド群 (nix コマンドのサブコマンド群)
  • デプロイや公式 Docker イメージについて
  • Stack integration
  • IOHKのリソース
  • NixOS
  • その他 Nix の深い話

Nix

Nix がなにかよく分からないが様々な環境で使えるパッケージセットとして利用している。

そこだけ聞くと stackage みたいだが全然 Haskell に閉じたものではなくて普通の OS のパッケージマネージャのようなカバー範囲を持っている。 それでいて Haskell のライブラリ依存関係も相当なリソースを投入して安定に保たれているということのようだ。

自分は MacUbuntu on WSL で同じ設定ファイルで Haskell の環境を構築できている。

さて Nix で検索すると NixOS というのが出てくるが OS を考えずにパッケージマネージャだけ利用することができる。

利用形態:

  • Nix package manager のみ
  • NixOS と Nix package manager

Nix ではパッケージセットを channel と呼び、これには何種類かある。
https://nixos.wiki/wiki/Nix_channels

Nix channel:

  • nixpkgs-unstable (= nixpkgs)
  • NixOS-xx.yy (NixOS-21.11 など)

NixOS channel は名前の通り NixOS 用であり Nix package manager だけのときに使うことは推奨されない。 つまり Nix package manager のみの利用のときには nixpkgs を使うことになる。 なお nixpkgs-stable という channel はないようだ。

プロジェクトの環境を固定したいときはこの channel のバージョンを固定すればいい。
https://nixos.wiki/wiki/FAQ/Pinning_Nixpkgs

スクリプティング

まず、Nix を使わなくても Haskell スクリプティングはできる。 cabal および stack のスクリプトはだめぽ氏による記事がとても参考になると思う。
https://zenn.dev/mod_poppo/scraps/e2891dbebb235d

Nix による方法は、まず Nix package manager をお使いの環境にインストールする。

https://nixos.org/download.html

するとシンプルなスクリプトは以下のように書ける (標準入力から1行読んで適当な色をつけて出力するスクリプト)。

> echo "ヤッホー" | ./yamabiko-nix.hs
ヤッホー

nix-shell は Nix の環境の中に入るためのコマンドだが、 shebang に書くことで特殊な動作をしてそのスクリプトを Nix 環境の中で実行するように動く。

コマンドライン引数は複数行に分けて記述できる。

  • -p / --packages: 依存パッケージを指定。デフォルトの channel は nixpkgs が選ばれる。
  • -i / --interpreter: このスクリプトを処理するコマンドを指定。

パッケージ指定が Nix Haskell の特徴的な記述になっている。 これは次のように構成されている。

  • pkgs: nixpkgs のルート
  • haskell.packages.ghc921: GHC 9.2.1 用の Haskell 依存関係の名前空間
  • ghcWithPackages: 依存ライブラリとGHCを同時に指定するための Nix 式 (関数)
  • p: [p.ansi-terminal]: 依存ライブラリのリストを返す無名関数 リストはスペース区切り

GHCバージョンにこだわりがない場合 haskell.packages.ghc921haskellPackages で置き換えてもいい。

選択可能なGHCバージョンは次のコマンドでわかる。

> nix-env -f '<nixpkgs>' -qaP -A "pkgs.haskell.compiler"
pkgs.haskell.compiler.ghc8107                 ghc-8.10.7
pkgs.haskell.compiler.ghc884                  ghc-8.8.4
pkgs.haskell.compiler.ghc902                  ghc-9.0.2
pkgs.haskell.compiler.ghc921                  ghc-9.2.1
...

また channel で管理されている依存ライブラリのバージョンは次のコマンドでわかる。

> nix-env -f '<nixpkgs>' -qaP -A "pkgs.haskell.packages.ghc921.ansi-terminal"
pkgs.haskell.packages.ghc921.ansi-terminal  ansi-terminal-0.11.1

ghcWithPackages の実装は ここ にある。 GHCとライブラリを別々に指定してしまうとGHCがライブラリを見つけられないらしい。

ちゃんとした開発

haskell-language-server の導入

haskell-language-server はエディタが見るものだし、 複数のGHCバージョンに対応するものでもあるので、 他に使いそうなものと一緒にグローバルにインストールしてしまう。

次のファイルを $HOME/nix/haskell/default.nix にでも用意して nix-env -i をたたくとグローバルにインストールされる。 (この default.nix は こちら を参考にさせてもらっている)。

> nix-env -i --file $HOME/nix/haskell/
...
building '/nix/store/knnnspc3dxxbk9nwv9rqznrrf01rrl8d-user-environment.drv'...

overlay によって haskell-language-server にオプションを与えている。 supportedGhcVersions は読んでの通り。

dynamic=true については何のドキュメントも見つけられないが Template Haskell のコードを扱うなら必要なようだ。 ただしこのオプションを有効にすると haskell-language-server のインストールに余計に時間がかかるかもしれない。 オプションの実装は ここ にある。 haskell-language-server 側のドキュメントでは ここ に相当する処理をしていると思われる。

パッケージの設定

Nix での Haskell 開発について調べていると cabal2nix という名前がよく出てくるが、これを直接使う機会はあまりなさそうだ。 今は nixpkgs の Haskell module が cabal2nix をラップした便利関数をいろいろ提供していて、 それらを使うのが主流のようだ。 この記事でも cabal2nix を直接は使わない方法を説明する。

まずはいつものように cabal init でパッケージを初期化する。 (--enable-nix オプションは不要かもしれない)。

> mkdir test-package
> cd test-package
> nix-shell -p "pkgs.cabal-install" -p "pkgs.haskell.compiler.ghc921" --run "cabal --enable-nix init --exe"

初期化ができたら次のような default.nix を用意する。

これで次のようにすればパッケージの executable が実行できる。 (nix-build は初回だけでいいはず)。

> nix-build
> nix-shell --run "cabal run"
"Hello, Haskell!"

あとは普通に .cabal ファイルを編集して開発していけばいい。

さて、この default.nix は nixpkgs の Haskell module が提供する developPackage を呼んで Haskell パッケージの開発のための設定を構築している。

source-overrides で依存関係のオーバーライドができる。 Hackage のバージョンを指定する方法と tarball のURLを指定する方法がある。

modifier では cabal のいくつかのオプションを設定できる。 設定項目の内訳は ここ にある。

なお、ghcBuild 引数に値を与えて別のGHCバージョンを指定することもできる。 (GHCと base のバージョンが合わないときは version-history を参照して調整する)。

> nix-shell --argstr "ghcBuild" "ghc902" --run "cabal run"

高度な依存関係オーバーライド

この項目を書くときに力尽きた。 依存関係オーバーライドはそれほど困難な道だった。 どうしてもオーバーライドしたいなら次の記事が参考になる。

Nix recipes for Haskellers – Sridhar Ratnakumar

How to override dependency versions when building a Haskell project with Nix

トラブルシューティング

nix-shell の立ち上がりが遅い

direnv Nix integration を導入するといいかもしれない。 direnv 自体は Nix と関係なく使えるひとつのツールだが、 Nix がこれと連携するのにはいくつかのオプションがある。

https://github.com/direnv/direnv/wiki/Nix

自分はシンプルそうな nix-direnv を使っている。

VSCode に direnv 拡張を導入すると VSCode からも direnv の環境が見えるようになる。 ただ過渡期にあるようで何を選ぶか注意が必要そうだ。

cabal run で cabal 自体の出力を無視したい

これは Nix というより cabal の Tips だが次のようにするといいかもしれない。

> nix-shell --run "cabal build && \$(cabal list-bin test-package) | clip.exe"

Mac で nix-env -i が失敗する

すでに修正されたようだが Mac 環境で nix-env -i が失敗することがある。 そのときは以下の issue にある方法で回避できることがある。

https://github.com/NixOS/nixpkgs/issues/163374

Mac で channel が消滅する

理由は分からないが Mac で channel 設定が消滅することがある (nix-channel --list が何も返さない)。 nixpkgs を自分で再設定する必要があるかもしれない。

> nix-channel --add https://channels.nixos.org/nixpkgs-unstable nixpkgs 
> nix-channel --update nixpkgs