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.
自分のdotfilesをhome-managerベースの仕組みに置き換えたいとも考えています。
ソフトウェアの設定自動定義が魅力的すぎる。
Nixパッケージマネージャの導入
WSL2のUbuntuの上にNixパッケージマネージャだけを導入してみましょう。
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.cabal
のbuild-depends
にtemplate-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" ];
})
];
};
});
}
これは通ります。
supportedGhcVersions
を912
にした場合を試します。同じエラーが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
テストに失敗することが問題なので、インストール時のテストを無効化すればワークアラウンドになりそうです。
parsec
がbase
のバージョンを厳しく指定しているので依存関係の解決に失敗します。既に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.project
でindex-state
が固定されていたので、古いリビジョンを隠れて参照したりしました。
各種パッケージの修正
かなりのパッケージがGHC 9.12を許容していないので書き換える必要がある。
いくつかはまだHackageにリリースされていいないだけで、 GitHubのHEADでは修正されていることがあるが、ない場合はforkしてそれを参照する必要がある。もちろんPRは出しておくが、取り込まれる前に検証しなければ。
- fix: allow base version from GHC 9.12 by ncaq · Pull Request #79 · wz1000/HieDb
- fix: allow base version from GHC 9.12 by ncaq · Pull Request #16 · maoe/ghc-trace-events
- fix: allow GHC 9.12 by ncaq · Pull Request #449 · haskell/hie-bios
- fix: allow time-1.14 by ncaq · Pull Request #15 · haskell-pkg-janitors/unix-compat
- fix: allow GHC 9.12 by ncaq · Pull Request #582 · kowainik/stan
- fix: allow GHC 9.12 of compiler and library versions by ncaq · Pull Request #36 · mitchellwrosen/tasty-hspec
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.nix
のtools
ではなくbuildInputs
の方に追加して行きます。完全にテストケースまだ通ってないものもあるらしいですがとりあえずは動いています。
sha256
はnix-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が用意しているキャッシュをそのまま使うと異常に遅い。
cache-nix-action
はbackend
引数で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 exists
かNo such file or directory
のどっちかのエラーを吐き出すようになってしまいました。
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のキャッシュサーバをセルフホストすることで解決したいです。
後はビルド時間が長くなりがちでもありますが、やはり私はコンピュータに働かせて楽をしたいと考えがちな人間なので、ビルド時間が長くなっても自分の手を動かす時間が減るならそれで良いと思います。