NixOSでフロントエンド開発するときのワークアラウンド集
これは何?
現代のフロントエンド開発ではNode.jsが必須だが、npmにはバイナリを直接ダウンロードしてくるような野蛮なパッケージがとても多い。一方、NixOSでは依存関係の問題を防ぐために独自のファイル構造を採用しており、Filesystem Hierarchy Standardに準拠していない。そのため、npmパッケージがNixを経由せずにダウンロードしてきた実行可能バイナリは共有ライブラリを見つけることができず、そのままだとNixOSでは動作しない。
このスクラップではそのような問題について、エッジケースごとに解決策を記録していく。
注意!
パッケージマネージャNixのみを使っている場合、以下に列挙した問題は発生しません。
NixOS以外のディストロやmacOSの上でNixを動かすだけなら問題は発生しない。あくまで、NixOSがFHSに準拠していないことで生じる問題であって、FHS準拠のOSの上でNix単体を動かすだけなら何も問題はない。なので、Nixを使ってみたいな〜という人は、NixOSではない普通のOSで使ってみるのがいいだろう。
それならフロントエンドでNix使う意味って何?と思う人もいるかもしれないが、aptやらhomebrewやらOSごとに異なるパッケージマネージャを使って疲弊したり、謎のNode.js用バージョン管理ツールで実行環境をバージョン管理したりするくらいなら、全部Nixでインストール・バージョン管理してしまった方が楽だ。それくらいの気楽さでよくて、Nixを使うからって別に強気になる必要はない。
結論
Nixは普通に便利、NixOSは変!
Sharp/Wrangler/その他
いきなり「その他」とかいうデカすぎる主語が出てきてしまった。というのも大体の問題はnix-ldで解決するからだ。
nix-ld
nix-ldはまさにこういった問題に対処するためのNixOS moduleで、ポン置きバイナリをNixストア内の共有ライブラリにいい感じにリンクしてくれる。
設定も超簡単で、nid-ldはNixOS本体でサポートされているので以下の設定を記述するだけでよい。
{
  programs.nix-ld.enable = true;
}
ポン置きバイナリのリンク先をlddで確認すると大体not foundになっているが、nix-ld有効化後に確認するとリンク先がNixストアに向いていることが分かるだろう。
Playwright
みんな大好きPlaywrightは、ブラウザの実行可能ファイルを直接落としてきて実行するヤバいパッケージである。ブラウザは依存関係マシマシなので、上で示したように単純にnix-ldを有効化するだけでは対処できない。
解決策は3つある。
1. buildFHSEnv
Nixpkgsのコミッターnatsukiumさんに教えてもらった方法(ありがとうございます!)。
buildFHSEnvとは、FHS互換の軽量サンドボックスを構築する関数である。依存関係激ヤバパッケージのビルドに使われているのをよく見る。これをNixシェルとして使う。
以下のようにdevShellを定義する。ほとんどNixOS Discourseで見つけたコードを参考にさせていただいた。
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default =
          (pkgs.buildFHSEnv {
            name = "playwright";
            targetPkgs =
              pkgs: with pkgs; [
                openssl
                systemd
                glibc
                glibc.dev
                glib
                cups.lib
                cups
                nss
                nssTools
                alsa-lib
                dbus
                at-spi2-core
                libdrm
                expat
                xorg.libX11
                xorg.libXcomposite
                xorg.libXdamage
                xorg.libXext
                xorg.libXfixes
                xorg.libXrandr
                xorg.libxcb
                mesa
                libxkbcommon
                pango
                cairo
                nspr
              ];
            profile = ''
              export LD_LIBRARY_PATH=/run/opengl-driver/lib:/run/opengl-driver-32/lib:/lib
              export FONTCONFIG_FILE=/etc/fonts/fonts.conf
            '';
            unshareUser = false;
            unshareIpc = false;
            unsharePid = false;
            unshareNet = false;
            unshareUts = false;
            unshareCgroup = false;
            dieWithParent = true;
          }).env;
      }
    );
}
通常、Nixシェルを宣言的に構築するときはmkShell関数を使うが、ここではbuildFHSEnvを使う。実はmkShell関数が提供するシェル環境はstdenvそのもので、上記のコードも同様にbuildFHSEnv関数が構築したFHS互換のサンドボックスをシェル環境として利用する。
基本的にNixOSの/usrディレクトリには/usr/binディレクトリしか存在せず、中身も/usr/bin/envというシンボリックリンクしかないが、上記のflake.nixをnix developしてdevShellに入った後にもう一度確認すると/usr/lib以下にたくさんのシンボリックリンクが貼られていることがわかる。よって、Playwrightがダウンロードしてきたブラウザのバイナリは無事FHS準拠のパスから共有ライブラリをロードして実行できるようになる。
2. nix-ldを使う
nix-ldを利用することもできる。nix-ldは環境変数を経由して、nix-ldに自動解決してほしい共有ライブラリを追加できるのでそれを使う。
$NIX_LD_LIBRARY_PATHにライブラリのパスを追加する。
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs =
    { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default = pkgs.mkShell {
          NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (
            with pkgs;
            [
              openssl
              systemd
              glibc
              glibc.dev
              glib
              cups.lib
              cups
              nss
              nssTools
              alsa-lib
              dbus
              at-spi2-core
              libdrm
              expat
              xorg.libX11
              xorg.libXcomposite
              xorg.libXdamage
              xorg.libXext
              xorg.libXfixes
              xorg.libXrandr
              xorg.libxcb
              mesa
              libxkbcommon
              pango
              cairo
              nspr
            ]
          );
        };
      }
    );
}
pkgs.lib.makeLibraryPath関数は、与えられたパッケージのリストを見ていき、<opensslのストアパス>/lib:<systemdのストアパス>/libという感じに、ライブラリのパスを連結した文字列を返す。buildFHSEnvの例で使ったものと同じライブラリのパスを環境変数に突っ込んでいる。
環境変数を宣言しているだけのdevShellだが、あとはnix-ldがよしなにやってくれる。欠点を挙げるとすれば、NixOSユーザーなら誰でも使えるbuildFHSEnvと違って、nix-ldを有効化しているユーザーしかこのdevShellを使えないことだろうか。
3. launchOptions.executablePathを指定する
実はNixpkgsはPlaywrightのbrowser driverを提供しており、そのストアパスをplaywright.config.tsのオプションで指定すれば動かせる。ただし、前述の2つの解決策と違ってコード本体にNixOSの事情が反映されてしまうので、それが許容できるかどうかで使いわけよう。
詳細なやり方は以下を参考にされたし。
上記のdevShellはchromiumにしか対応していないので注意
現在調査中
Prisma
みんな大好きPrismaは、Prisma EngineというRust実装のバイナリを使う。他と違うのは、Prisma CLIからバイナリをダウンロードしようとすると「NixOSはサポートしてないぜ!」と処理を失敗させる点だ。実は、Prismaは結構良心的なヤツで、エンジン等のバイナリのパスを環境変数から指定できるようになっており、Nixpkgsが提供しているprisma-enginesその他のパスを指定してやれば動く。
以下を参考にすればヨシ。
環境変数については以下のリファレンスを参照。