haskell.nixでのプロジェクト環境でnix flake checkでPostgreSQLへのアクセスを含むテストを実行する方法
背景
最近はNix Flakeで、 input-output-hk/haskell.nix: Alternative Haskell Infrastructure for Nixpkgs を使ってHaskellプロジェクトの環境構築を行っています。
問題
CIの実行が遅いし定義が複雑なことに悩まされています。
Cachix - Nix binary cache hosting は会社のお金で契約しているのですが、純粋にダウンロードが多すぎます。
なぜ多くなってしまうのかと言うと、
haskell.nixに限らずNixは必要なプログラムしかダウンロードしない仕組みになっているのですが、我々のGitHub ActionsのCI定義ではnix develop
経由でコマンドを実行してしまっています。
- name: cabal update
run: nix develop . -c cabal update
このnix develop
を実行するだけでほぼ全ての依存関係をダウンロードしてしまい、数分かかってしまいます。
特にhaskell-language-serverは巨大であり、今のところ人間のプログラマーにしか必要ないので、 CIではインストールしないようにしたいですよね。
まあ単純にnix develop
をやめるだけだと依存したままになってしまいがちなんですが、
nix develop
ベースでビルドを進めているとそういうのを別分離するのも難しくなってしまいます。
原因
じゃあなぜnix develop
を使ってcabal
を直接実行しているのでしょうか?
nix flake check
をすればテストは必要な依存関係だけで実行されるからそれで良いはずです。
何故そんなことになっているのかと言うと、アプリケーションのテストでPostgreSQLを読み書きするので、 Nixの純粋な空間では単純にはネットワーク通信でデータベースと通信出来ないからです。
CIが遅くて複雑というだけで致命的なバグにはなってなかったので、忙しかったというのもあって放置していました。
でも前調べたときと違ってClaude 4 Opusが出てきたので、 Claude Codeを使って探索させて、少しそれを手直ししました。
ソリューションはある
実はNix自体はこれへの解決策は用意しています。
postgresqlTestHook と言います。
これを以下のように設定すれば解決するそうです。
nativeCheckInputs = [
postgresql
postgresqlTestHook
];
しかしhaskell.nixにはnativeCheckInputs
は存在しないので、この解決方法はそのまま使えません。
インテグレーション
modules
でうまく接続する必要がありました。
modules = [
(
{ pkgs, ... }:
let
postgresqlTestConfig = {
preCheck = ''
source ${pkgs.postgresqlTestHook}/nix-support/setup-hook
export PGUSER="postgres" # テスト内部でデータベースを作成するためにsuper userを指定。
postgresqlStart
'';
};
in
{
packages = {
foo.components.tests.foo-test = postgresqlTestConfig;
};
}
)
];
ちょっと罠なのはpostgresqlStart
は必要なのに、
postgresqlStop
は必要ないことです。むしろ入れると二重解放でエラーになります。既にhookが処理しているということなのでしょうね。
もし処理してなかったとしてもNixの領域にゴミが積み立てられるだけなので、ディスクがいっぱいになりそうになったらディスクを計測して問題のディレクトリを削除すれば良いでしょう。
GitHub Actionsで失敗する
ローカルでnix flake check
を実行するのは成功するようになったのですが、私が取り掛かっているプロジェクトではGitHub Actionsで実行すると失敗します。
以下のようにエラーログが表示されます。
> starting postgresql
> waiting for server to start....2025-07-11 05:40:18.086 UTC [17972] LOG: starting PostgreSQL 17.5 on x86_64-pc-linux-gnu, compiled by clang version 19.1.7, 64-bit
> 2025-07-11 05:40:18.086 UTC [17972] LOG: Unix-domain socket path "/home/runner/_work/_temp/nix-build-foo-test-foo-test-0.1.0.0-check.drv-0/run/postgresql/.s.PGSQL.5432" is too long (maximum 107 bytes)
> 2025-07-11 05:40:18.086 UTC [17972] WARNING: could not create Unix-domain socket in directory "/home/runner/_work/_temp/nix-build-foo-test-foo-test-0.1.0.0-check.drv-0/run/postgresql"
> 2025-07-11 05:40:18.086 UTC [17972] FATAL: could not create any Unix-domain sockets
> 2025-07-11 05:40:18.087 UTC [17972] LOG: database system is shut down
> stopped waiting
> pg_ctl: could not start server
PostgreSQLのUnixドメインソケットのパスは107文字までという制限があるようです。 nixのビルドディレクトリのパスが長すぎるんですね。これぐらいの長さは許容して欲しいと思うんですが。 FATの時代じゃあるまいし。
あとなんでローカルだと問題ないんだろう。そんなにローカルも短いパスになっているわけではないと思うんですが。もしかしたらNixOSとUbuntu上のNixで差異が存在するのかもしれません。
昔からDebianなどでも様々な人が困っているらしい。 Re: buildd path too long (postgresql-9.3 FTBFS)
なんでそんなわけのわからん制限があるのか分からない。パスの長さがハードコーディングされて動かすと何が起きるかわからないからとか?
hookを使うことではUNIXドメインソケットを無効化できない
単純にもっと短いパスを持つ一時ディレクトリにソケットを動かせば良さそうだ。多分無理。サンドボックスを破らずに$NIX_BUILD_TOP
を変更することは出来ない。
じゃあUNIXドメインソケットが問題になっているのでTCPを使う。
postgresqlEnableTCP
: set to1
to enable TCP listening. Flaky; not recommended.
こんなことが書かれてあるから使いたくないが、
$NIX_BUILD_TOP
が長いパスの場合制限を回避できないのだから仕方がない。
TCPを使う設定にしてもUNIXドメインソケットは有効なままで作られることが分かった。
UNIXドメインソケット機能自体を無効化してしまえば良いと思うんだけど、軽く調べた限り、その方法はunix_socket_directories
を空文字列にすることだけだ。
しかしそういうextraなことをする処理は以下のようになっている。
echo "$postgresqlExtraSettings" >>"$PGDATA/postgresql.conf" # Move the socket echo "unix_socket_directories = '$NIX_BUILD_TOP/run/postgresql'" >>"$PGDATA/postgresql.conf"
はunix_socket_directories
の設定の前の部分にあるんだけど、これで上書き出来るものなのかなあ。前に書いてあるものが優先されて後ろは捨てられるなら都合が良いんですが。
これでやってみる。
postgresqlTestConfig = {
preCheck = ''
# 変数やコマンドを読み込む。
source ${pkgs.postgresqlTestHook}/nix-support/setup-hook
# テスト内部でデータベースを作成するためにsuper userを指定。
export PGUSER="postgres"
# GitHub Actions環境などで、
# PostgreSQLのUnixドメインソケットパスが107文字制限を超える場合があるので、
# TCP接続のみで通信し、Unixドメインソケットを無効化する。
export PGHOST=localhost
export postgresqlEnableTCP=1
# postgresqlTestHookのデフォルト設定を上書きして、
# Unixドメインソケットを完全に無効化する。
export postgresqlExtraSettings="unix_socket_directories = ''''''"
# ソケットを作らなくてもlockファイルなどは作成されるのでディレクトリが必要。
mkdir -p $NIX_BUILD_TOP/run/postgresql
# PostgreSQLを起動。
postgresqlStart
'';
捨てられないらしい。無理。
postgresqlTestSetupCommands
が後ろでeval
してるけど、これはこれでpg_ctl start
の後に実行されるので今更効力が無い。流石に一度閉じて設定変えて再起動するのは嫌すぎる。
仕方がないから自分でいじる
車輪の再発明じみたことは私は相当嫌いなんだけど順序的に割り込みが難しいから仕方がない。トラブルシューティングのために仕組みを睨みつけていたら、やっている事自体はそこまで複雑ではないことが分かった。自分で実装してみよう。
# テスト時にPostgreSQLを必要とするパッケージにデータベース環境を提供する。
# 本当は`postgresqlTestHook`を使いたかったのですが、
# GitHub Actionsの環境ではパスが長すぎてUNIXドメインソケットを作成するのがエラーになり、
# `postgresqlTestHook`のUNIXドメインソケットを無効化する方法が見つからなかったので、
# 自分で実装しています。
_:
let
postgresqlTestConfig = {
preCheck = ''
PGDATA="$NIX_BUILD_TOP/postgresql"
export PGDATA
# TCP接続を使用するため、PGHOSTをlocalhostに設定。
PGHOST="localhost"
export PGHOST
# テスト内部でデータベースを作成するためにsuper userを指定。
PGUSER="postgres"
export PGUSER
PGDATABASE="foo"
export PGDATABASE
# システムデータベースの初期化。
initdb -U postgres
# UNIXドメインソケットを無効化し、TCP接続を有効化
echo "unix_socket_directories = ''\'''\'" >> "$PGDATA/postgresql.conf"
# データベースの起動。
pg_ctl start
# ユーザが使うデフォルトデータベースの作成。
createdb "$PGDATABASE"
'';
};
ClaudeはNixのエスケープ文法が分からなくて大変に混乱してあちこちのコードを消し始めたので自分で実装した。
クリーンアップ
ポートの競合が起きてしまいます。 hookを使わない場合は終了するときのクリーンアップは自分でやる必要があるようだ。もしかしたらローカルでは自動的に抹消されたのと、 hookをGitHub Actionsで使った時は先にUNIXドメインソケットの問題でエラーになっただけで、本来どこでもやるべきなのかもしれません。
単に停止するだけで良いでしょう。
postCheck = ''
# データベースの停止。
pg_ctl stop
'';
それでも競合しますね。
リソースの競合
複数のテストが実行されてもこれまでUNIXドメインソケットや固定の独立したコンテナなら問題なかったけど、 Nixが好き勝手にPostgreSQLコンテナを立ち上げまくるので競合するということなんでしょうね。
プロセスIDをベースに雑にポートを割り当ててしまいましょう。軽量プロセスだったりランダムでテスト失敗したり他のポートと被ったらその時対処を考えます。
# 並列テスト実行時の衝突を避けるため、ランダムなポートを使用。
# プロセスIDから範囲指定で決定。
PGPORT=$((5433 + ($$ % 192)))
export PGPORT
やっとCIが通るようになりました。
PRを出すべきでしょうか
これを実現するオプションをnixpkgsにPRとして出しておくべきでしょうか。しかし割とそんなに実装するのは難しくなかったのと、 UNIXドメインソケットのパスの長さ問題をUNIXドメインソケット自体を無効化することで解決するのは、なにか違うなと言う気はします。