NixでZenn CLIをビルドする
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
を作成します.
{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-config
とlibvips
を依存関係として追加しています.
これを基にビルドしてみましょう.
$ 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のパッケージをロードして開発環境に追加しています.
{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
やったね!
Discussion