NixでZenn CLIをビルドする

5 min読了の目安(約5000字TECH技術記事

Nixの紹介

Nixはソフトウェアの再現性を重視したパッケージマネージャです.
ソフトウェアに再現性が高いと何がうれしいのかというと,一度動くことを確認したソフトウェアが環境を移行しても動き続けるということが保証できるため,システムの信頼性に繋がるからです.
例えば,ソフトウェアの依存するライブラリがバージョンごとの相性問題で動かなかったり,開発環境と本番環境でソフトウェアの挙動が予期しない形で違ったりという問題の解決に繋がります.

Nixではソフトウェアの再現性を高めるために関数プログラミングの考え方を活用しています.
関数プログラミングでは入力に対して関数を順々に適用していくことで最終的な出力を得るのでした.
これをパッケージマネージャの文脈に当てはめると,ソフトウェアの依存関係が入力としてビルドすることで(関数適用)出力として成果物のソフトウェアができるということになります.
Nixでパッケージの記述に使うNix言語は純粋関数プログラミング言語として設計されているため,入力が同じならば出力も同じとなります.
これによってビルドの再現性を担保するという形です.

ところで,近年のプログラミング言語にはパッケージマネージャも組み込まれていることが多いです.
例えばRustにはcargoがありますし,Node.jsではnpm/yarnが広く使われています.
これら言語組み込みのパッケージマネージャとNixの違いは何なのでしょうか.
一つにはNixは言語外の依存も追跡できる,つまりNixの普遍性が挙げられるでしょう.
ソフトウェアの依存関係には言語内の依存関係だけではなく,言語外への依存,特にCライブラリへの依存が含まれます.
cargoやnpmといった言語組み込みのパッケージマネージャではこのような外側への依存はアドホックなやり方で扱われることが主なように思われます.
一方Nixでは,CでもRustでもNode.jsでも全てNix言語によってパッケージの依存やビルド方法を記述することができるのです.世界統一!!!

node2nixを用いたnpmパッケージのビルド

言語ごとのパッケージ記述をNixに移植するのは面倒なものです.
そこで,Nixではそれぞれの言語のパッケージをNixへと自動で翻訳するためのツールが揃っています.
この記事ではそのツールの一つであるnode2nixをZenn CLIを例に紹介します.

node2nixはnpmパッケージをNixに変換するためのツールです.
node2nixを使うためには,まずNixをインストールしてからnix-shellコマンドを以下のように実行します.

$ nix-shell -p nodePackages.node2nix

nix-shellコマンドは指定したパッケージをインストールしたシェルを起動するコマンドです.
この場合は新しいシェルでnode2nixが使えるようになります.
nix-shellはパッケージをグローバルにインストールするわけではないので,プロジェクトごとに必要なソフトウェアだけ選んで別々のnix-shellを利用することができます.

これでnode2nixを使うことができるようになりました.
node2nixでnpmバイナリパッケージを変換するには次のように起動します.

$ echo '["zenn-cli"]' >> node-packages.json # 利用するバイナリパッケージを選択
$ node2nix -i node-packages.json # Nixパッケージを生成

すると,カレントディレクトリに

  • node-packages.nix
  • node-env.nix
  • default.nix

といったファイルが生成されます.これらがZenn CLIをビルドするためのNixパッケージ記述に対応します.

では早速ビルドしてみましょう!

$ nix-build -A zenn-cli
...略...
builder for '/nix/store/y8dqazp84ldzhkq59l818y2d1pl757rj-node_zenn-cli-0.1.61.drv' failed with exit code 1
error: build of '/nix/store/y8dqazp84ldzhkq59l818y2d1pl757rj-node_zenn-cli-0.1.61.drv' failed

おおっと,失敗してしまいました.ログを眺めてみると,zenn-cliの依存にあるsharpのビルドに失敗しているようです.

> sharp@0.26.2 install /nix/store/4w8w9yflny7dp349wk947v1w470a57p9-node_zenn-cli-0.1.61/lib/node_modules/zenn-cli/node_modules/sharp
> (node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)

info sharp Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.10.0/libvips-8.10.0-linux-x64.tar.br
ERR! sharp getaddrinfo ENOTFOUND github.com
info sharp Attempting to build from source via node-gyp but this may fail due to the above error
info sharp Please see https://sharp.pixelplumbing.com/install for required dependencies
make: Entering directory '/nix/store/4w8w9yflny7dp349wk947v1w470a57p9-node_zenn-cli-0.1.61/lib/node_modules/zenn-cli/node_modules/sharp/build'
  CC(target) Release/obj.target/nothing/../node-addon-api/nothing.o
  AR(target) Release/obj.target/../node-addon-api/nothing.a
  COPY Release/nothing.a
  TOUCH Release/obj.target/libvips-cpp.stamp
  CXX(target) Release/obj.target/sharp/src/common.o
../src/common.cc:24:10: fatal error: vips/vips8: No such file or directory
   24 | #include <vips/vips8>
      |          ^~~~~~~~~~~~
compilation terminated.

どうやらsharpはlibvipsというCライブラリに依存しているようです.
ログにはsharpがWebからlibvipsを入手しようとして失敗している箇所があります.

Downloading https://github.com/lovell/sharp-libvips/releases/download/v8.10.0/libvips-8.10.0-linux-x64.tar.br

これはNixがビルドの再現性を確保するために,ビルドをネットワークから隔離したサンドボックスで行っているからです.
その結果としてsharpがlibvipsをコンパイルできず,全体のビルドが失敗してしまったというわけです.

sharpのビルド方法を眺めると,どうやらlibvipsを外部から供給してやるとうまくいきそうです.
そこで,次のzenn-cli.nixを作成します.

zenn-cli.nix
{pkgs ? import <nixpkgs> {
    inherit system;
}, system ? builtins.currentSystem}:

let
  nodePackages = import ./default.nix {
    inherit pkgs system;
  };
in
nodePackages // {
  zenn-cli = nodePackages.zenn-cli.override {
    buildInputs = [ pkgs.pkgconfig pkgs.vips ];
  };
}

これはNix言語のソースコードで,Zenn CLIをビルドする際にpkg-configlibvipsを依存関係として追加しています.
これを基にビルドしてみましょう.

$ nix-build zenn-cli.nix -A zenn-cli
...()...
/nix/store/7acrddff72515n8qdih893880zb205fx-node_zenn-cli-0.1.61

できました!
成果物はカレントディレクトリにあるresultという名のsimlinkからアクセスできます.

$ ./result/bin/zenn
👀Preview on http://localhost:8000

nix-shellからZenn CLIを起動する

nix-shellを使って開発環境にZenn CLIを追加することもできます.
次のshell.nixを作成しましょう.
zenn-cli.nixからZenn CLIのパッケージをロードして開発環境に追加しています.

shell.nix
{pkgs ? import <nixpkgs> {
    inherit system;
}, system ? builtins.currentSystem}:
let
  zenn-cli = import ./zenn-cli.nix {
    inherit pkgs system;
  };
in
  pkgs.mkShell {
    # 開発環境の依存関係としてZenn CLIを追加
    nativeBuildInputs = [
      zenn-cli.zenn-cli
    ];
  }

nix-shellを単独で実行すると,カレントディレクトリからdefault.nixまたはshell.nixの内容に応じて開発環境が構築されます.

$ nix-shell
$ zenn
👀Preview on http://localhost:8000

やったね!