Open7

Nix Package Manager の学習ログ

Nix とはなにか?

  • 純粋関数型パッケージマネージャ
  • 純粋関数型言語でパッケージを記述する
  • パッケージのビルドには暗黙的な入出力が存在しない
    • システムのCコンパイラを使ったりしない
    • ネットワーク上からファイルを落としたりしない
  • 再現性の高いビルドを可能にする

インストール

普通の Linux システムなら sudo 可能なユーザで

curl -L https://nixos.org/nix/install | sh

最近の macOS ではルートファイルシステムが read-only になってるので手間がいろいろかかる。割愛。

Nix Store

Nix のパッケージとかは Nix Store に保存される。Nix Store は通常 /nix/store である。Nix をもりもり使うようになるとここには大量のディレクトリが生成されるようになる。

手元のマシンで ls /nix/store | rg nodejs してみた。その結果がこちらである。

257nngj1zpq2q3mnri4gr04givg2asrd-nodejs-10.19.0
2zgcxd0x487a98avbmnxmj1w8dplbhdr-nodejs-12.21.0
487v2h4zrqd12p9igc0cvm4gadkdx12i-nodejs-14.16.0
4ag2rpqjhyw3ycgf0dqca41abw3r6pq3-nodejs-15.11.0.drv
8fjdlhvajz4xbm9l3dn0xylga3ypsg6b-nodejs-14.15.1
d9gj8yajh49mhbfw1r27vajnw4bhnvh8-nodejs-15.12.0
dnk5123ynnz3x1cfgjdf661s26lr2qim-nodejs-15.11.0
gdskfb8af4689gcfj1c8i4vx0rgxcfkn-nodejs-15.12.0.drv
gwd2jf747lyjs3s02nd0g7llj4dg5qmy-nodejs-12.21.0.drv
j0n10j33k39n1sp78rmbcxhybpk5apwb-nodejs-14.15.3
j779rh2xzm8pb63g128w3a9ap9q6crv0-nodejs-14.16.0.drv
k7apd59j662ph6ijz2xv72h92k12kd5h-nodejs-14.15.1.drv
kir7s4ygwx4ff75a6pflvzxi8r6k2hf5-nodejs-14.16.0.drv
qyj7rg46f14jq37jbsvx63px4vmfxi0b-nodejs-10.19.0.drv
rwh3c0v1irhnclzlxyyb784agjgnxfmy-nodejs-14.15.3.drv
xcnr6wn2b3ndgfys3xwjdcd4cgchq6v5-nodejs-14.16.0

これらのディレクトリの先頭にあるサイバー感のある文字列は、パッケージの依存関係グラフを暗号学的ハッシュ関数に突っ込んだものらしい。よく見てみると、Node.js 14.16.0 の中でも 487v2h4zrqd12p9igc0cvm4gadkdx12i-nodejs-14.16.0xcnr6wn2b3ndgfys3xwjdcd4cgchq6v5-nodejs-14.16.0 という種類があることがわかる。これはつまり、何らかの依存関係が異なるということ。依存関係が異なるものを別々に管理することで、依存関係が複雑化して地獄が生まれるのを避けられる。

確かに自分は Node.js をよく使っているけれども、流石にこんな大量の Node.js を管理する趣味はない。それに何よりこんなに同一パッケージのかぶりが存在するとストレージが圧迫されてしまう。なので nix-collect-garbage というコマンドが用意されている。これをするとどのパッケージからも使われていないパッケージを自動で削除できる。これも依存関係がすべて明示的に記述されていることの利点だろう。

GCC でコンパイルする

Cでプログラムを書いてコンパイルしてみる。コードはなんの変哲もない Hello world。

main.c
#include<stdio.h>

int main (void) {
  printf("Hello, Nix!\n");
  return 0;
}

ビルドするための Nix Derivation は default.nix というファイル名にすることが多い。

default.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation {
  name = "hello-nix";
  builder = ./builder.sh;
  src = ./main.c;
}

このファイルは1つの関数である。引数と本体を分けるのは :{ pkgs ? import <nixpkgs> } は JavaScript では ({ pkgs = require("nixpkgs") }) に相当する。Set―JS ならオブジェクトと呼ばれる―をパターンマッチングで分解し、pkgs のデフォルト値として <nixpkgs> をインポートしたものを使う。ちなみに import は専用の構文ではなく組み込み関数。さすが純粋関数型パッケージマネージャというべきか。

pkgs.stdenv.mkDerivation はパッケージ (derivation) を作る関数。引数として Set を渡している。Set のキーと値は = で区切り、; が必要。.builder.sh./main.c はパスらしい、文字列ではないみたいで、パッケージを扱う言語ならではかも。

パッケージをビルドする際には builder.sh が実行される。

builder.sh
source $stdenv/setup

gcc $src -o hello-nix

mkdir -p $out/bin
cp ./hello-nix $out/bin

このスクリプトはシステムから隔離された環境で実行される。最初に source しているのは基本的な環境変数。$src とか $out みたいなものが入っているはず。そして gcc でコンパイルしたファイルを出力ディレクトリにコピーして終わり。stdenv.mkDerivation でビルドする場合はビルドする環境に gcc が自動的に用意される。

ビルドするにはこれらのファイルが入ったディレクトリで

nix-build

する。ファイル名を指定しないと default.nix を実行する規約になっている。ビルドされたら result/bin/hello-nix を実行してみよう。Hello, Nix! と出力されるはず。result はビルドされたパッケージが入っている Nix Store へのシンボリックリンクになっている。readlink でたどると /nix/store/f0nbi18xpz8vbc86ba8kks6rb8r156xi-hello-nix であることがわかった。

nix-shell

nix-shell という特殊な Bash が使える。これはそして新しいソフトを試したりするのに便利。nix-shell -p nodejs-15_x とすると Node.js 15 のパスが通ったシェルが起動する。

たとえば突然さっきの nix-hello を gcc ではなく Clang でコンパイルしたい衝動に駆られたとしよう。そんなときは

nix-shell -p clang

を実行すれば Clang が使えるシェルに入れる。このシェルは隔離されていないのでシステムに入っているソフトウェアや main.c などのファイルが問題なく使える。そのシェルでコンパイル

clang main.c -o hello-nix

すればいい。この作業で Clang はシステムにはインストールされていない。exit して元のシェルに戻って clang を実行することを試みてもエラーになるはずだ。なんだ…夢か。

TODO

  • Node.js でなんかやる
    • 一番得意なので
  • F#/.NET でなんかやる
    • やったことないので
  • Rust でなんかやる
    • Rustup との兼ね合いが気になるので
  • 何かしら環境構築にめんどくささを持つソフトウェアが求められる
  • Nix Expression Language の文法のちゃんと突っ込んだ説明をする
  • Docker をやる
    • VS Code Dev Container が動かなそうだけど大丈夫かな
  • GitHub Actions
    • CI/CD がバズワードなので
  • niv
  • Cachix
    • ノートPCだとビルドしまくるのが困る
ログインするとコメントできます