• 作成:

Haskell開発環境にNix Flakesを使う

GHCのWASMバックエンドの開発環境の管理のためにNixを導入しました。作業しながらメモをつらつらと書きました。乱文ですが誰かのトラブル解決に役立つかもしれないので公開します。

ghc-wasm-metaを利用したい

Serverless Haskell - GHCのWASMバックエンドで Haskell を Cloudflare Workers に載せるを読んでGHCのWASMバックエンドが割と実用的になったことを知ったので使うことにしました。

なのでhaskell-wasm / ghc-wasm-meta · GitLabを導入したい。

公式が推奨しているのはNixを使うことです。最初の記事のkonnさんと違って私はネイティブでもWSL2でもLinuxを使っているので、 macOSでの問題は発生しないと考えて、 Nixを使って導入することにしました。

NixOSは自分がHaskellをメイン言語にしていることから、前から気になってはいたんですけど、私の本拠地はGentooなので完全に乗り換えるのは気が引けてたんですよね。 NixOSをGentooみたいに使うのは難しいそうですし。 NixOSの使い方とArch Linux、Gentooとの比較

ただ最近はマシンのスペックが上がってきたので、全体を特定のCPU特化で最適化しなくても十分に速いです。それこそWSL2みたいに仮想環境で動かしてもなんとかなるぐらいには。なので今マシンネイティブ最適化に拘っても得るものは少ないです。 -march=nativeでの最適化はwebブラウザやテキストエディタなどのレイテンシがUXを左右するものや、 AVX-512などを活かしているソフトウェアだけで十分かもしれません。

あと昔にNixOSの記事を見た時はストレージ容量を食いまくるらしいことを嫌ったのですが、最近はSSDが安くなっています。また引っ越してネットワーク速度がNICの上限の2.5Gbps(契約は10Gbps)あるので、サイズを気にして削除して再ダウンロードする羽目になることを過剰に怖がる必要はなくなりました。

なので今回良い機会なのでNixを試します。今回はまずWSL2のUbuntuの上にNixパッケージマネージャを導入してみますが、試して相当良かったらGentooからNixOSにOSごと乗り換えます。

nix-community/NixOS-WSL: NixOS on WSL(2) [maintainer=@nzbr] を使えばWSL2にもNixOSがインストール出来るそうなので、 WSL2環境も最初Ubuntu使い倒したらGentooに移行するかと思ってたのですが、 WSL2環境もNixOSに移行しても良いかも知れません。

最近のGentooもgentoo-kernelとかは以下のように設定をGitで管理できてかなり良かったんですけどね。 ncaq/gentoo-kernel-config: My sys-kernel/gentoo-kernel config.

自分のdotfileshome-managerベースの仕組みに置き換えたいとも考えています。

ソフトウェアの設定自動定義が魅力的すぎる。

Nixパッケージマネージャの導入

WSL2のUbuntuの上にNixパッケージマネージャだけを導入してみましょう。

Download | Nix & NixOS

WSL2のUbuntuの上だしわざわざマルチユーザに対応させる必要はないでしょう。 systemd対応のWSL2を実行しているのでマルチユーザ対応も出来るんですけどね。デーモンとかあると複雑になってきますし。

マルチユーザ対応でデーモン付きでインストールした場合、 Nixを使うたびにroot権限必要になって鬱陶しいらしいです。自分はデーモン付きでインストールしたことないので知りませんが。それのイメージのせいでわざわざDevContainerの上にNixをインストールしたがられて、インストール方法をちゃんと指定したらroot権限は以後不要だと説得しました。 VSCodeがあんまり制御効かない時DevContainerの上に入れるメリットはあるんですが。

というわけでインストールオプションには、

sh <(curl -L https://nixos.org/nix/install) --no-daemon

の方を使います。 DeterminateSystems/nix-installer: Install Nix and flakes with the fast and reliable Determinate Nix Installer, with over 7 million installs. の方を使ったほうが色々と楽という話もありますが、インストールしたときは公式しか知りませんでした。

~/.profile~/.zshrcが書き換えられました。まだNixに完全移行する気は無いのでインストールする前から条件分岐しようかと思っていましたが、最初からディレクトリがあるか判定して分岐していますね。気が利きますね。

まあ自分はWSL環境じゃない場合途中で~/.profileを終了させるので書き換えは必要なんですけど。自動対応で出来る限界までやっているとは思います。

機能の有効化

早速、

nix shell 'gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org'

を実行しましたが、

error: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it

とerrorになりました。

みんな有効にしてるけど未だにexperimentalなんですね。いつまでexperimentalなんだろうか。本番環境に入れるのが慎重なのはわかるけど、ずっとexperimentalなのは実験的環境に人を引きずり込んでしまうので良くないと思う。

あたりを見る限りコマンドラインで有効化するよりファイルに書き込んだほうが良さそう。ずっと使うことになりそうですし。

シングルユーザの場合設定ファイルは~/.config/nix/nix.confにあります。

今の設定は以下のようになっています。

experimental-features = nix-command flakes
allow-import-from-derivation = true

有効にしてとりあえずghc-wasm-metaのスタートガイドの通りwasmtime ./hello.wasmが実行できるところまで確認できました。

Stackを使いたいが一度諦め

自分は軟弱なのでCabalではなくStackを使いたいのだけれど、どうしたら良いんだろうか。 ghc-wasm-metaが提供するnixの環境だとcabalは張り替えられているのだけど、 stackは元のままです。 ll /nix/store/ibn7ccknckmfk3zzi7m6jzfd6fxxbbg7-ghc-wasm/bin/ みたいにバイナリ一覧を見てみると、 ghcはwasm32-wasi-ghcみたいな名前で存在してはいるから、 stackの参照するGHCをこちらのファイルにすればいけそうな気がしますが、深みにハマりそうで嫌ですね。とりあえずおとなしくcabalで試していこう。

Emacsの設定

開発環境はnix-shellをEmacsが実行すればうまくいくのでは。 Emacsから見ると強いdirenvがあるようなものだと思うし。 flake.nixをちゃんと読み取れば良いはずだ。

言語は違うけれど似たようなことをしている人は当然いる。 EmacsのReact/Svelte編集環境(Eglot + Tree-Sitter + Puni、Nix flake版) #nix - Qiita

Emacsのnix関連パッケージは公式に提供されてるのはnix-modeだけらしい。それがいくつもモジュールを持っているけれど。あまりEmacs側で解決するのもよくなさそう。 VSCodeとか使っている人も居るわけだし。

Nix言語のコードフォーマッタは色々あるけれど、最終的には公式の、 NixOS/nixfmt: The official (but not yet stable) formatter for Nix code を利用する、 nix-mode付属のnix-format.elを使うのが一番良さそうだ。コマンド的には最近はnixfmt-rfc-styleを使うのが良いらしい。

設定は以下で最低限の形にはなるでしょう。

(leaf nix-mode
  :ensure t
  :init
  (defun nix-mode-setup ()
    (add-hook 'before-save-hook #'nix-format-before-save nil t))
  :hook (nix-mode-hook . nix-mode-setup)
  :bind
  (:nix-mode-map
   ([remap indent-whole-buffer] . nix-format-buffer)))

プロジェクトをNix Flakesで管理する

HaskellのプロジェクトをNixで管理する方法はいくつかあるみたいですが、今回はNix Flakesを使います。

nix flakeのテンプレートは見つからなかったけど、いろんな場所のexampleを参考にすればなんとかなりそうです。 GHCのバージョンは今HLSがサポートしていてnightlyとは言えStackage LTSにもある、 9.10.1が良さそう?

flake.nix · master · haskell-wasm / ghc-wasm-meta · GitLab がある程度参考になりそう。

srid/haskell-flake: A flake-parts Nix module for Haskell development みたいにガチガチにNixで管理することも出来るみたいだけど、これは全部Nixで管理してしまうから、今回は考えないにしてもNixを使わずにプロジェクトをビルドしたい人は困りそうですね。

まずは小さいところからやっていきましょう。今回はフロントエンドは必要ないので小さいシステムで良さそう。

{
  inputs = {
    ghc-wasm-meta.url =
      "gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org";
  };
  outputs = inputs:
    inputs.ghc-wasm-meta.inputs.flake-utils.lib.eachDefaultSystem (system:
      let pkgs = inputs.ghc-wasm-meta.inputs.nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          packages = [ inputs.ghc-wasm-meta.packages.${system}.all_9_10 ];
        };
      });
}

これで一応設定はされているらしい? どうにもうまく行っている気はしないが、問題は出てきてから対応しよう。

いちいちnix developするのはやってられないし、テキストエディター環境のセットアップがそれでは面倒すぎるので、 nix-direnv を使います。

これ自体をNix Flakesで管理することは出来ません。 Flakesに入るためのソフトウェアをFlakesで管理することは出来ません。

後々はNixOSかhome managerで管理しますが、今は雑にnix profileでインストールしてごまかします。

以下のようにしてnix-direnvのインストールと設定を行います。 direnvは元々入ってました。これもnix profileでインストール出来ます。 Ubuntu 22.04の場合direnvのバージョンが古いらしい(自分は24.04を使っているのでよくわからない)ので、 direnv自体もnix profileでインストールした方が良いかもしれません。

nix profile install 'nixpkgs#nix-direnv'
mkdir -p $HOME/.config/direnv/
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' >> $HOME/.config/direnv/direnvrc

Emacsの設定は以下のようにしておけばディレクトリを訪れた時に自動で環境を読み込んでくれます。

(leaf envrc
  :ensure t
  :global-minor-mode envrc-global-mode
  :custom (envrc-none-lighter . nil))

それでプロジェクトの.envrcは以下のようにしておきます。

use flake . --accept-flake-config
dotenv_if_exists .env.local

--accept-flake-configは後に設定するキャッシュサーバを読み込ませるために必要です。

dotenv_if_exists .env.local.env.localを読み込んでいるのは、 .envrcをプロジェクトにコミットして管理する都合上、 AWS_PROFILEなどを個人が設定しておきたい場合どうすれば良いのかと言われたから追加しました。 .gitignore.env.localは無視します。

is marked as broken, refusing to evaluate.とエラーになる

ドキュメントを参考にcallCabal2nixベースで構築したのですが、一部のパッケージはnixが壊れていると警告してきます。 GitHubの最新を参照すると壊れていないのだけど。

export NIXPKGS_ALLOW_BROKEN=1とか、 { allowBroken = true; }とかで無理矢理通すことは出来るみたいですが、壊れているパッケージ全体を通してしまうのは嫌ですね。一部のパッケージだけ問題ないとマークしたい。

Haskell - NixOS Wiki を読んで回避方法を知りましたが、これの方法はパッケージ指定でhaskell.lib.markUnbrokenしているから、 Cabal側で書いてcallCabal2nixしている私の状況では同じ方法は使えない気がします。

今回はcallCabal2nixは使わないべきなのでしょうか。 nix側でガチガチに管理したいわけではないし。

ビルドする時のネイティブツールさえ管理してくれれば今回は満足出来ます。

nixの細かい機能が必要になるまで使わないでおきましょうか?

それでcallCabal2nixをやめたら、

error while loading shared libraries: libzstd.so.1: cannot open shared object file: No such file or directory

というエラーが出るようになってしまいました。 zstdは依存関係に追加しているのですが…

LD_LIBRARY_PATHを設定すれば多分解決するんでしょうが、自動で継承してくれるようにしたいですよね。

LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (buildInputs);

を追加してライブラリは読み込まれるようになったのですが、代わりに以下のエラーになってしまいました。

Configuring library for zlib-0.7.1.0...
Preprocessing library for zlib-0.7.1.0...
running dist/build/Codec/Compression/Zlib/Stream_hsc_make failed (exit code -6)
rsp file was: "dist/build/Codec/Compression/Zlib/hsc2hscall78862-3.rsp"
output file:"dist/build/Codec/Compression/Zlib/Stream.hs"
command was: dist/build/Codec/Compression/Zlib/Stream_hsc_make  >dist/build/Codec/Compression/Zlib/Stream.hs
error: *** stack smashing detected ***: terminated

ちょっとエラーメッセージがプリミティブすぎて解決方法が分からないです。

callCabal2nixに戻る

やはりcallCabal2nixに戻るべきですかね?

とりあえずこれで良いか…

pkgs = import nixpkgs { inherit system; };
hspkgs = pkgs.haskell.packages.${ghcVersion}.override {
  overrides = self: super: {
    haxl = pkgs.haskell.lib.markUnbroken super.haxl;
  };
};

と思ったら、

Error: Setup: Encountered missing or private dependencies:

と言うエラーメッセージが出てきて全く分からない。

仕方がないのでとりあえずは全部許可して解決するか考えます。

外部にまだ公開しないリポジトリなのでビルドの一貫性とかそんなに気になりませんし。

許可。

pkgs = import nixpkgs {
  inherit system;
  config = {
    allowBroken = true;
  };
};

とすることでビルドは始まるようになったんですが、それでもhaxlとかは、 Error: Setup: Encountered missing or private dependencies:とエラーになってしまいますね。

haxl以外のライブラリもだめですね。よくわからない。 sandboxの都合かなあ。

haskell.nixを使用してうまくいく

仕方がないので敬遠していた、 Alternative Haskell Infrastructure for Nixpkgs を試してみます。

flakeを使うので、 Getting started with Flakes - Alternative Haskell Infrastructure for Nixpkgs にしたがってセットアップ。 hixじゃない方で良いか。 Scaffoldingのsectionに書いてる方を注入。

キャッシュの設定をしないと高確率でGHCをクライアント側でビルドしてしまいます。 nix.confに書く方法だとデフォルトのキャッシュサーバの設定はどうなるのでしょうか。他のキャッシュが効かなくなったりしませんかね?

flake.nixに書く方式だと問題ないらしい? もし問題になったとしても他のプロジェクトに影響も出しませんし。

追加とかも考えましたが、シンプルに以下のような感じで良いでしょう。

nixConfig = {
  extra-substituters = [
    "https://cache.nixos.org/"
    "https://cache.iog.io"
  ];
  extra-trusted-public-keys = [ "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" ];
  allow-import-from-derivation = "true";
};

GitHub ActionsとかのCIでの実行だとコマンドラインの方に--accept-flake-configを設定する必要がありますが。

CIもdirenvを使う場合、以下のような設定が効きます。

use flake . --accept-flake-config

ただそれでもマイナーなGHCのバージョンを選んでしまったりすると、 ghc-lib-parserとかは信頼できるキャッシュが存在しないのかビルドしてしまうこともあるみたいですね。

CabalにGHC添付ライブラリに依存することを許可させる

foo.cabalbuild-dependstemplate-haskellが入っていると、再インストールは出来ないからGHCの同梱のものを使えとエラーになる。

しかし入っていないとそれはそれでTemplateHaskell言語拡張の影響かエラーになる。

ghc-9.8.2: unknown unit: template-haskell

仕方がないのでとりあえずはプラグマを明示的に書くことにします。

いや仕方がないで済ませて良い問題ではなくですね。依存関係の依存関係でtemplate-haskellが依存関係に入ったら即座に詰んでしまいます。

以下のオプションをcabal.projectなどに記述するとtemplate-haskellなどの同梱パッケージを依存関係に使えます。

allow-boot-library-installs: True

全然知りませんでした。これまでStackに甘やかされてきたのを感じます。 Stack登場直後に飛びついてずっと使ってましたからね。

色々と楽を出来るので、まだまだStackは必要だと感じます。頑張ってwasm対応させたい。

zlibが結局エラーになったのでGHCのバージョンを下げる

しかし結局以下のエラーになります。

Failed to build zlib-0.7.1.0.
Build log (
/home/ncaq/.cabal/logs/ghc-9.8.2/zlib-0.7.1.0-f2f2751218583b2cdba4367b3e961fd0234b1424e67a6be0c851a5da3fd9cf22.log
):
Configuring library for zlib-0.7.1.0...
Preprocessing library for zlib-0.7.1.0...
running dist/build/Codec/Compression/Zlib/Stream_hsc_make failed (exit code -6)
rsp file was: "dist/build/Codec/Compression/Zlib/hsc2hscall66820-3.rsp"
output file:"dist/build/Codec/Compression/Zlib/Stream.hs"
command was: dist/build/Codec/Compression/Zlib/Stream_hsc_make  >dist/build/Codec/Compression/Zlib/Stream.hs
error: *** stack smashing detected ***: terminated

Error: [Cabal-7125]

GHCのバージョンをもっと下げてみて、 GHC 9.6系のghc-9.6.6にしてみます。

そしたらビルド出来ました。そこまで新しいバージョンの機能を欲しているわけではないので、とりあえずこれで妥協します。後で解決します。

Cabalの報告を見ているとCコンパイラやリンカーのバージョンととかも関係してそう。 Stack smashing when building some packages · Issue #7456 · haskell/cabal 今度pureにしてNix外部のツールを参照しないようにして再現性を高めて調査してみます。

haskell-language-server

nixでインストールしたhaskell-language-serverが以下のようなエラーで動かない。

ff025dd72d08f4bf ghc -v0 -- --print-libdir
  Environment Variables
    HIE_BIOS_GHC: /nix/store/d0gwrmmfgnaaandpzp31a5m69wzdn9rs-ghc-9.6.6/lib/ghc-9.6.6/bin/ghc-9.6.6
    HIE_BIOS_GHC_ARGS: -B/nix/store/aja0d6l3iqd0m8p89cz3lnk4ban7i7hb-ghc-shell-for-packages-ghc-9.6.6-env/lib/ghc-9.6.6/lib
ghc-pkg-9.6.6: cannot find package ghc-9.6.6
ghc-pkg-9.6.6: cannot find package template-haskell-2.20.0.0
GHC ABIs don't match!

Expected: ghc-9.6.6:df4ea993369c5512dd5f9d42c29a5af1 template-haskell-2.20.0.0:54f98474fb3c6416e430906af50b8378
Got:      ghc-9.6.6:template-haskell-2.20.0.0:
Content-Length: 203

{"jsonrpc":"2.0", "method":"window/showMessage", "params": {"type": 1, "message": "Couldn't find a working/matching GHC installation. Visit https://nixos.org/manual/nixpkgs/unstable/#haskell-language-server to learn how to correctly install a matching hls for your ghc with nix."}}%

Nix外部のghcupでインストールしたGHCなどをmv ~/.ghcup ~/Downloadsして隔離しても動かない。

色々とコマンドを試してみたところ、 haskell-language-server-wrapperに問題がある気がしてきて、以下のissueに辿り着きました。

Provide haskell-language-server-wrapper to help direnv · Issue #1776 · input-output-hk/haskell.nix

このissueにhaskell-language-server-wrapperをダミーにして、 haskell-language-serverを直接参照させてしまうワークアラウンドが書き込まれていました。

それを少しアレンジして、 buildInputsに以下を追加しました。

buildInputs = with pkgs; [
  (pkgs.writeScriptBin "haskell-language-server-wrapper" ''
    #!${pkgs.stdenv.shell}
    exec haskell-language-server "$@"
  '')
];

根本的には元々のhaskell-language-server-wrapperのハッシュ照合か環境の探索方法を根本的に治すか、それぞれのlsp-clientの方を治す必要がありそうです。

しかし今はこのワークアラウンドで行く。

こういう汚いワークアラウンドはどういうシステムでも必要になることはありますが、そのようなものを環境を汚染せずに全体で共有して一回だけやれば良いのがNix Flakesの良いところでしょう。他のマネージャと違ってそれぞれ個人の環境(例えばLinuxとmacOSが違うとか)向けの条件分岐などをかなりマシな言語で書けて、なおかつ単純なシェルスクリプトなどでのセットアップと違って宣言的で再現性が高い。

WASM向け開発環境

2025-01-15T15:31:08 [✖  INT 130] ⬢ [Systemd] ❯ wasm32-wasi-ghci
/nix/store/v6za3dqk7yz7i12g1hafcz42n3mbdfzg-ghc-wasm/bin/wasm32-wasi-ghci: line 10: /nix/store/lynwgz2p1rrly4z07q7c91g8lxbw6xs4-wasm32-wasi-ghc-9.10/lib/wasm32-wasi-ghc-9.10.1.20241209/bin/./wasm32-wasi-ghci-9.10.1.20241209: No such file or directory
2025-01-15T15:31:27 [✖  INT 130] ⬢ [Systemd] ❯ wasm32-wasi-ghc --interactive
GHCi, version 9.10.1.20241209: https://www.haskell.org/ghc/  :? for help

wasm32-wasi-ghciが動かないのに、 wasm32-wasi-ghc --interactiveだと動くことを発見しました。

最初wasm32-wasi-ghciリンク先のファイルが動かない問題として報告しようかと思っていましたが、報告用に新規に環境を開いたらghciコマンド自体が動いて困惑しました。

GHC 9.12からは動くそうです。

何も考えずにthreadsなどを有効にしてたけどwasmだと使えなくて当然ですね。コンパイルオプションの-threadedとかを削除したらビルド出来ました。

cabal runはプロセス起動なので使えないけどwasmtimeなら実行できる。以下のように書けば気分的にはcabal runです。

wasm32-wasi-cabal build && wasmtime $(wasm32-wasi-cabal list-bin exec-wasm)

haskell-language-serverをWASMターゲットで使おうと思いましたが止められました

Serverless Haskell - GHCのWASMバックエンドで Haskell を Cloudflare Workers に載せるにはIDEについてHLSは重武装したghciだからghciを実装していないwasmターゲットでは使えないと書いてありますが、今Nix経由でインストールしたGHC 9.13バージョンのWASM GHCだと普通にghciが存在しています。では逆に今ならアップデートされていてHLSが使えるのでは? そう簡単な話ではないとは思いますが。

HLSはghciを使うのでwasmでghciをサポートし始めたGHC 9.12が必要。 HLSの公式リリースでサポートしているGHCは9.10が最後。なので自分でビルドしてみる。

以下のようにflake.nixに定義を追加。

buildInputs = [
  (inputs.nixpkgs.legacyPackages.${system}.haskell-language-server.override {
    supportedGhcVersions = [ "912" ];
  })
];

これだけだとhappyがビルドエラーになります。

error: builder for '/nix/store/nxl4wf41y3r7i149gy4fn0i9pdrd029z-happy-1.20.1.1.drv' failed with exit code 1;
       last 25 log lines:
       >
       > make: *** [Makefile:68: shift01.gc.hs] Error 1
       > ../dist/build/happy/happy --strict -ag shift01.y -o shift01.ag.hs
       > happy: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.IOException:
       >
       > data//HappyTemplate-arrays-ghc: openFile: does not exist (No such file or directory)
       >
       > HasCallStack backtrace:
       >   ioError, called at libraries/ghc-internal/src/GHC/Internal/Foreign/C/Error.hs:291:5 in ghc-internal:GHC.Internal.Foreign.C.Error
       >
       > make: *** [Makefile:71: shift01.ag.hs] Error 1
       > ../dist/build/happy/happy --strict -agc shift01.y -o shift01.agc.hs
       > happy: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.IOException:
       >
       > data//HappyTemplate-arrays-coerce: openFile: does not exist (No such file or directory)
       >
       > HasCallStack backtrace:
       >   ioError, called at libraries/ghc-internal/src/GHC/Internal/Foreign/C/Error.hs:291:5 in ghc-internal:GHC.Internal.Foreign.C.Error
       >
       > make: *** [Makefile:74: shift01.agc.hs] Error 1
       > make: Target 'all' not remade because of errors.
       > make: Leaving directory '/build/happy-1.20.1.1/tests'
       > Test suite tests: FAIL
       > Test suite logged to: dist/test/happy-1.20.1.1-tests.log
       > 0 of 1 test suites (0 of 1 test cases) passed.
       For full logs, run 'nix log /nix/store/nxl4wf41y3r7i149gy4fn0i9pdrd029z-happy-1.20.1.1.drv'.

全体ログを見てもよくわからない。 happy自体が9.12でビルド出来るのか、 wasm抜きで確かめる必要がありそう。

とりあえずgit cloneしてきてGHC 9.12でビルドしてみたところ、この場合ちゃんとcabal buildは出来るようです。 cabal testも通る。

nix profile install 'nixpkgs#haskellPackages.happy'自体は正常に完了します。でもこれ一瞬で終わるからキャッシュを入れてるだけ?

wasmが入ると話がややこしくなるので、単純な例で既存のサポートされているバージョンを試してみます。

{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          buildInputs = [
            (nixpkgs.legacyPackages.${system}.haskell-language-server.override {
              supportedGhcVersions = [ "910" ];
            })
          ];
        };
      });
}

これは通ります。 supportedGhcVersions912にした場合を試します。同じエラーがhappyで起きて通らなくなります。

GHC 9.12を使っているだけなら通るが、それをNixでビルドすると通らないようです。

問題を整理して調べ直すと既にissueが存在していました。 Regression: Cabal-3.14.1.0: v1-test, Setup.hs test: test suites of alex-3.4.0.1 and happy-1.20.1.1 unable to find data files · Issue #10717 · haskell/cabal

テストに失敗することが問題なので、インストール時のテストを無効化すればワークアラウンドになりそうです。

parsecbaseのバージョンを厳しく指定しているので依存関係の解決に失敗します。既にPRはマージされている。 Cleanup .cabal file, allow base-4.21 by phadej · Pull Request #191 · haskell/parsec ならGitHubの方を参照させれば良いですね。

sha256の値を入れなくても実行自体は出来たのだが、入れないと書き換え出来ないらしい。わかりやすくエラーになって欲しいです。

hie-compat に関しては修正されたバージョンが存在しない。そのうち修正リクエストを投げるかもしれませんが、どうせbaseのバージョン違いなので制約を無視してしまいます。

色々とnix flake上で依存関係の上書きを試みてみましたが、こういうのはやはり素直にforkしたほうが良さそうです。

Hackageで見る文には問題ないはずの依存関係でコケているなと思ったら、 cabal.projectindex-stateが固定されていたので、古いリビジョンを隠れて参照したりしました。

各種パッケージの修正

かなりのパッケージがGHC 9.12を許容していないので書き換える必要がある。

いくつかはまだHackageにリリースされていいないだけで、 GitHubのHEADでは修正されていることがあるが、ない場合はforkしてそれを参照する必要がある。もちろんPRは出しておくが、取り込まれる前に検証しなければ。

fourmoluの修正

pathをHackageにリリースされていないGitHubを見るのはすぐ終わりましたが、 ghc-lib-parserを9.12に対応させるのは大変そう。

!13511: EPA: Remove AnnKeywordId · Merge requests · Glasgow Haskell Compiler / GHC · GitLab に書いてあるようにAnnKeywordId系が削除されて新しいAPIに切り替わったので、それのマイグレーションをする必要がある。

意外とそこまで大変でもない? fourmoluのfork元のormoluのPRを参考にできそう。 ghc-lib-parser 9.12 by amesgen · Pull Request #1140 · tweag/ormolu

そもそもmergeしろって書いてた。

作った。

feat: allow GHC 9.12 by ncaq · Pull Request #454 · fourmolu/fourmolu

unicode-syntaxと言う演算子のUnicodeシンボル変換機能だけ動かない。かなり真面目に読まないと難しそう。でもとりあえずhaskell-language-serverを動かすのには支障がないし、趣味ならともかく仕事なのでとりあえずこの段階でビルドを試してみる。

GHC自体のバグに突き当たった

haskell-language-server -> cabal-add -> cabal-install-parsers -> binary-instancesと依存関係を辿っていって、以下既に開かれていたPRにぶつかった。

GHC-9.12 support blocked on gitlab.haskell.org/ghc/ghc/-/issues/25653

Support GHC-9.12, tagged-0.8.9 by phadej · Pull Request #32 · haskellari/binary-instances

GHC自体のバグかあ、難易度はともかくリリース周りで時間取りそうだしHLSこの方針で動かすのは無理だったか?

いやcabal-addはプラグインだからまだ必須ではない。なのでとりあえずHLSをビルドする時は取り除ける。

止められた

既存のネイティブバイナリ向けにhaskell-language-serverを動かしてビルドだけWASMにする方法があるのだからそちらを使えと滅茶苦茶怒られました。

プロジェクトがC FFIのみで実行できるようになりました

というわけで先人の方法を使おうとしました。

最初はJS FFI必要だと思ってたのでGHC 9.12でHLSを動かしたり、ダミーの関数を読み込ませることを検討していたけど、よくよく聞き直してみるとプロジェクトの状況変化でC FFIのみでいけることが分かったので無理に対応する必要がなくなった。

JS FFIが不要なら新しいGHCはそんなに必要ありません。

haskell.nixとwasmビルドの共存

haskell.nixを使わずにやるのは色々と面倒だと分かってきたので、 WASM向けの環境もこれの上に構築したいです。ただ、 GHC 9.6, Wasm, and GHCJS · Issue #2024 · input-output-hk/haskell.nix がまだ解決されていません。

今のwasmのサポートはasteriusベースになっているみたい? カスタムコンパイラとかを指定できないかな?

いやビルド自体も基本的にはネイティブのものを使う方針で良いはずか。最終的にビルドする時だけwasmが使われればそれで良い。

JS FFI使わないならGHC 9.6で良いかと思ったけど、 GHC 9.6もGHC 9.8もダイナミックインターフェイスファイルを何故か作らない問題があるので、 GHC 9.10の上に構築することになりました。

一部のパッケージはGHC 9.10に対応していなかったので、無理矢理対応していると上書きしました。

cabal-fmt = pkgs.fetchFromGitHub {
  owner = "jhrcek";
  repo = "cabal-fmt";
  rev = "d94f0bef9ee3f606cb9b812b231bfd750a0abd3e";
  sha256 = "1kf6y6102q1inbz646gs1cz3w0ir86np0rs6fpc6yp95prqc191i";
};
hlint = pkgs.fetchFromGitHub {
  owner = "ndmitchell";
  repo = "hlint";
  rev = "7dfba720eaf6fa9bd0b23ae269334559aa722847";
  sha256 = "06sqja2n9glj8f58hkcpbkjf1h70x22jv74h9pzdlsp459sq28cy";
};
stylish-haskell = pkgs.fetchFromGitHub {
  owner = "jhrcek";
  repo = "stylish-haskell";
  rev = "85895fc861e46781a5f9474288aee191b06b4be2";
  sha256 = "10wannws1dvcayx36qq9sy50j1v1w5bk20dxfr7w5a1d4hg26xj3";
};

これらをhaskell.nixtoolsではなくbuildInputsの方に追加して行きます。完全にテストケースまだ通ってないものもあるらしいですがとりあえずは動いています。 sha256nix-prefetch-git https://github.com/jhrcek/stylish-haskell 85895fc861e46781a5f9474288aee191b06b4be2のように取得します。

ネイティブコードだろうがWASMコードだろうがnix buildで管理したいです。しかし方法が分かりませんでしたでした。ソフトウェアを読み込ませたつもりでも、 cabalがhttpsにアクセスできないとか出てしまう。

nix buildで構築したかったけど時間切れ。素直にwasm32-wasi-cabal buildを手で実行します。本当にやるならhaskell.nixを対応させた方が真面目な方法なのでしょう。

GitHub Actions

GitHub ActionsでのCIもNix Flakesベースで行います。開発者と環境が一致するので再現しやすいし手元で実行しやすいですからね。

セルフホストランナーへのNixのインストール

GitHub Actionsのランナーがセルフホストだったりすると多少注意する必要があります。

例えば事前にいくつかのパッケージをインストールしておく必要があったりとか。

- run: sudo apt-get update
- run: >
    sudo apt-get install -y
    xz-utils
    zstd

Nixのインストールには以下のactionを使わないと、セルフホストランナーだとうまくいきませんでした。 cachix/install-nix-action: Installs Nix on GitHub Actions for the supported platforms: Linux and macOS.

もちろん他にもいくつかの方法があると思いますが。 Nixのcacheを復元する時の時間のかかりかたに比べれば全ては誤差です。

cabal global cacheの方法がよくわからない

GitHub Actionsで行うcabal buildのglobal cacheで以下の警告が出ます。

Post job cleanup.
Warning: Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.

rm -rf ~/.cabalしてnix develop -iした環境でcabal updateしてcabal buildしたあとにcabal pathを実行して気がついたのですが、 XDG準拠の~/.cache/cabal以下にディレクトリが作られることもあるらしく、 Nixで新規にインストールした場合はそうなるらしい。 cache-home: /home/ncaq/.cache/cabal のように配置されます。

しかし、 GitHub Actionsのランナー上でcabal pathを実行させたら、実行結果はXDGを指しているのに、それを指定してもやはり空と出てしまいますね。 Nix側、それもhaskell.nixがキャッシュを管理してるのでしょうかね。

buildjet/cacheは無駄でした

nixのキャッシュはかなりサイズが大きくなります。元々Haskellのビルドのキャッシュサイズは大きかったですが、 Node.jsとかその他のツールをドシドシ追加出来ますし、したくなりますからね。

GitHub Actionのactions/cacheはセルフホストランナーを使うと異様に通信が遅くなることが知られています。

runnerを自前で用意すると、actionsが用意しているキャッシュをそのまま使うと異常に遅い。

GitHub Actionsをなるべく安く使う - k.dev

cache-nix-actionbackend引数でbuildjetを指定するだけでbuildjetのキャッシュが使えるらしいです。

Buildjetいわく、セルフホストランナーだとGitHubのcacheより3倍ぐらい速いらしい。

Launching BuildJet Cache for GitHub Actions | BuildJet for GitHub Actions

しかし私の環境ではむしろ遅くなりました。 GitHubのcacheだと3.8MBs/secなのが、 Buildjetのcacheを使うと0.6MBs/secに落ち込んでしまいました。

無料のサービスを使って3倍早くなるなんて都合の良い話はあんまりなかったということですね。社内にキャッシュサーバを建てるといった地道な作業が必要になりそうです。

wasmサブディレクトリでGitHub Actionsを実行

今回wasmの環境はwasmサブディレクトリに構築したので使えないactionsが出てきました。

defaults:
  run:
    working-directory: ./wasm

でのグローバルサブディレクトリはactionsには効かないのでrunで自分で制御する必要が出てきます。

nix-community/cache-nix-action は雑に自前で書き換えられます。こちらの方がシンプルでわかりやすいまでありますね。何ならnixがビルドを行う時はパッケージの大多数は既にキャッシュされてるので最悪なくても良いぐらいです。

- uses: actions/cache@v4
  with:
    path: |
      /nix
      ~/.cache/nix
      ~root/.cache/nix
    key: wasm-haskell-${{ runner.os }}-nix-${{ hashFiles('wasm/*.nix') }}-${{ hashFiles('wasm/flake.lock') }}

と思ったのですが… 大量にtarがCannot open: File existsNo such file or directoryのどっちかのエラーを吐き出すようになってしまいました。

DeterminateSystems/magic-nix-cache-action: Save 30-50%+ of CI time without any effort or cost. Use Magic Nix Cache, a totally free and zero-configuration binary cache for Nix on GitHub Actions. の方を試してみます。

nixのインストールはcachix/install-nix-actionのまま。 DeterminateSystems/nix-installer-actionだとセルフホストランナーだとタイムアウトエラーになってしまう。

時々GitHub API error: API error (429 Too Many Requests)になりますが、それでもこちらの方が全体がいきなりcache無効になったりしなくて良さそうですね。

aldoborrero/direnv-nix-action の方はディレクトリ指定出来ないし、簡単に書き換えられないしどうしようかなと思ったけれど、 CIでやる分には別にdirenvにこだわらなくても良いですね。

nix develop --accept-flake-config . -c wasm32-wasi-cabal build all --ghc-options="-Werror"

のようにすればnixの環境で実行してくれる。しばらくnix shellでやろうとしてドツボに入っていました。

--accept-flake-configがないとGHCをビルドし始めるので注意です。

もしかして最初にbashの代わりにnixに入ってしまえば良いのかと思いましたが、それはnixがプレインストールされているランナーでのみ使える手法ですね。

Nixを今後も使いたい

Nixを使うと全ての環境をプロジェクトごとに閉じ込めることが出来て、他の開発環境をimportしてこれて楽を出来ることが分かりました。

Nix使ってなかったこれまでは縛りプレイだと思うレベルです。ありとあらゆるプロジェクトのリポジトリをNixで管理したい。

キャッシュサイズが大きくなることだけが問題ですね。これはGitHub Actionsのキャッシュサーバをセルフホストすることで解決したいです。

後はビルド時間が長くなりがちでもありますが、やはり私はコンピュータに働かせて楽をしたいと考えがちな人間なので、ビルド時間が長くなっても自分の手を動かす時間が減るならそれで良いと思います。