• 作成:
  • 更新:

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 to 1 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"

https://github.com/NixOS/nixpkgs/blob/38a4e2c62e1ff81de752037e2f301535a75ca176/pkgs/by-name/po/postgresqlTestHook/postgresql-test-hook.sh#L62

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ドメインソケット自体を無効化することで解決するのは、なにか違うなと言う気はします。