Closed14

Nixとその他パッケージマネージャーの比較

AsahiAsahi

パッケージマネージャーとは何か

  • パッケージマネージャーは複雑なソフトウェア
    • あまりに使い慣れすぎて多くの人がこの事実を忘れている
  • パッケージマネージャーは複合的なソフトウェア
    • ビルド,インストール,etc...
    • モダンパッケージマネージャーとレガシーパッケージマネージャーでは機能が大きく異なる
      • 時代に合わせてパッケージマネージャーが担う機能が増えている
      • 例: ロックファイル
  • 「パッケージマネージャー」の構成要素を分解しながらNixと比較していく
AsahiAsahi

ユーザー権限領域とシステム権限領域

  • インストール時にsudoが必要か否か
    • 以降,便宜的にユーザー権限領域システム権限領域と呼ぶ

ユーザー権限領域

  • npm
  • cargo
  • その他言語固有のパッケージマネージャー

システム権限領域

  • apt (Ubuntu)
  • pacman (ArchLinux)
  • dnf (Fedora)
  • その他OS固有のパッケージマネージャー

Nixはユーザー権限領域

  • sudoは不要
  • OSを問わず使える(ただしUNIX系に限る)
  • システム権限領域には手を出せない
    • カーネルのインストールとかデバイスドライバの変更とかはできない
    • じゃあNixOSはどうなっているの?
      • Nixでシステム領域領域の操作ができるよう専用に作られたディストロ
      • NixOSについては後述.混乱を避けるためにも今は一旦忘れたほうがいい.
AsahiAsahi

《パッケージのインストール》の流れをみる

こんなインストールコマンドがあったとする.

package-manager install package-x
  1. パッケージレジストリからpackage-xを探す
  2. package-xのメタ情報やソースコードをダウンロード
  3. メタ情報に従いpackage-xをビルド
  4. package-xのビルド成果物を適切なディレクトリに配置,付随する適切な設定ファイルの配置または書き込みなど
  • 大抵のパッケージマネージャーはこういった流れで動く
  • 時短のため2, 3を飛ばし直接ビルド成果物をインストールすることも多い
    • aptやpacmanなど

段階に分ける

  1. レジストリ
  2. 依存解決フェーズ
  3. ビルドフェーズ
  4. インストールフェーズ
AsahiAsahi

1. レジストリ

  • 中央集権型分散型

中央集権型

  • パッケージレジストリは1つだけ
  • オプショナルで別のレジストリを追加できることが多い
    • 例: AUR (Arch User Repository)

例:

分散型

  • 中央レジストリは存在しない
  • 各々が勝手にパッケージを公開できる
  • GitHubリポジトリなどを介してパッケージを管理する

例:

  • Go言語
    • go.modを見るとパッケージをGitリポジトリのURLで指定していることが分かる

Nixのパッケージレジストリは分散型

  • Gitリポジトリを1つのレジストリと見なす
    • このリポジトリをFlakeと呼ぶ
    • リポジトリのルートにflake.nixが配置されたもの
  • このレジストリ管理機構はFlakesと呼ばれる
    • Flakes自体色んな機能と役割を持っているが今回はレジストリ管理に絞って論じる
  • Flakesは依存するFlakeをロックする
    • Gitのコミットハッシュを使って依存をロックする
      • flake.lock
    • Goとほぼ同じ
      • flake.nix = go.mod, flake.lock = go.sum
  • 昔は中央集権型だった
    • nixpkgsが中央レジストリだった
      • Flakesでは数多のレジストリの内の1つという扱い
    • nix-channelsと呼ばれる仕組みで管理していた
    • 公式にはnix-channelsはまだ非推奨ではないが,もう使わない機能と言ってよい
AsahiAsahi

2. 依存解決フェーズ

  • パッケージの依存関係を解決する
  • パッケージマネージャーの質を定める重要なポイントである

依存関係とは

  • パッケージの開発・ビルド・動作に必要な外部パッケージ
  • 基本的に同じパッケージマネージャーを使って依存関係をインストールする
    • npmでパッケージを作るなら,npmでその依存をインストールする(当たり前)

依存関係の指定の例

  • npm (packages.json)
    • 動作用: dependencies
    • 開発・ビルド用: devDependencies
  • Cargo (Cargo.toml)
    • 動作用: [depandencies]
    • 開発用: [dev.dependencies]
    • ビルド用: [build.depandencies]
  • ArchLinuxのパッケージ (PKGBUILD)
    • 動作用: depends
    • ビルド用: makedepends

依存解決の課題

  • 大抵のパッケージマネージャーは,重大だが馬鹿馬鹿しい問題を容易に引き起こす
  • これらの問題を回避するためにパッケージマネージャーは進化してきた
    • しかし,完全に回避することは難しい

暗黙的依存

  • こんなパッケージメタ情報があったとする
  • ビルドにpackage-Aをコマンドとして使うため依存関係に指定している
正しいメタ情報
{
  "依存関係": ["package-A", "package-B"],
  "ビルドスクリプト": "package-A build"
}
  • ここでpackage-Aを指定しなかった場合を考える
  • 「必要なものを指定していないのだから失敗するはずだ!」
誤ったメタ情報
{
  "依存関係": ["package-B"],
  "ビルドスクリプト": "package-A build"
}
  • 成功する可能性がある

    • package-Aは勿論インストールされないが,ビルド環境に既にpackage-Aがグローバルインストールされていた場合,ビルドが通ってしまう
    • 一方,package-Aが存在しない環境では失敗する
    • つまり,ビルドの再現性が損なわれる
  • 余談

    • npmパッケージでrm -rf相当の操作を行うスクリプトを書く場合,Windowsではrm -rfが使えないので,rimrafというnpmパッケージを使う
    • Windows環境でも再現性を保つための工夫

依存関係地獄

  • Dependency Hell
  • WindowsではDLL地獄として名を馳せた
  • 全Developerが一度は経験して地獄を見ている(個人調べ)ので省略

現代では解決しているのか?

  • Dockerの登場
    • 全部コンテナイメージに押し込めばいいじゃん!
    • バカみたいだが楽だし実用的
    • しかし,結局内部でaptやらpkgやらを使うので問題は起こる
  • 機械的に上記の問題を解決する方法がない
    • こっちが本題
    • そこでNix!(つづく)
AsahiAsahi

Nixの依存解決

  • Nixは依存関係の問題を回避するために,特別な依存管理システムとビルドシステムを持っている
  • Nixでは暗黙的依存と依存関係地獄がほとんど発生しない
    • 特にビルド用の依存については絶対に発生しない
      • これについてはビルドシステムの仕組みが関わってくるので後述

Derivation

  • Nixにおけるパッケージ単位
    • .drv拡張子のテキストファイル
    • パッケージのアーカイブではない
  • 「パッケージの完全再現レシピ」が記述されている
    • 依存関係
    • ビルドスクリプト
    • インストールスクリプト
    • etc...
  • Human writableではないので,専用のNix言語を用いて生成する
  • NixはDerivationを見てパッケージを依存解決・ビルド・インストールを行う
    • Nix言語 → (コンパイル) → .drv → (Nixが依存解決・ビルド) → ビルド成果物 → (Nixがインストール処理実行) → 完了

Derivationの依存解決

  • Derivationにはハッシュが記述されている
    • Derivationに記述されたパッケージを同定する要素(依存関係,ビルドスクリプトなど)を全てハッシュ関数ブチ込んだもの
    • 依存解決とビルドに使う
      • <ハッシュ>-<パッケージ名>-<バージョン>という名前のディレクトリにビルド成果物を格納する
        • Gitの場合: zcpq1fivdd131gshg58m4cyq7fwqr3j8-git-2.42.0/
      • このディレクトリはNixストア/nix/store)に配置される
        • ハッシュで区別しているので単一階層で管理
  • Nixにとってパッケージのバージョンに意味はない
    • ハッシュが異なる場合,Nixはそれを全く別のパッケージとして扱う
      • バージョンが異なる→ハッシュ関数の入力が異なる→ハッシュが異なる→異なるパッケージ
    • バージョンアップデートによる上書きが発生しない
  • 依存関係を指定する時,ハッシュを含めて指定する
    • Derivation自体がlockfileのようなもの
    • Derivationの依存解決を辿れば,「パッケージの完全な依存関係ツリー」が得られる
    • この時点で依存関係地獄は発生しなくなった

暗黙的依存はどうするか?

  • Nixのビルドシステムで解決している(つづく)
AsahiAsahi

3. ビルドフェーズ

  • パッケージマネージャーはビルドシステムを内包している
  • ビルドシステムだけを取り出して使えるものもある
    • npm, Cargoなどの言語固有パッケージマネージャー
      • アプリケーションのビルド(非パッケージ)のビルドに使う
      • ビルドだけして,パッケージ化は他のパッケージマネージャーで行う
    • 一方,aptはaptパッケージをビルドすることだけに使う
  • 汎用的なビルドシステムは問題を引き起こしがち
    • CargoやGo
      • cargo build, go buildの実行
      • 言語内での利用に閉じている
      • あまり問題は起きない
    • PKGBUILDなど
      • シェルスクリプトの実行
      • 正気の沙汰ではない
      • 暗黙的依存発生し放題
  • いずれにせよ機械的に暗黙的依存を防ぐ手立てが無い

Nixのビルドシステム

  • Nixはビルドシステムだけを取り出して使える
    • Nixは言語固有パッケージマネージャーに近い特性をたくさん持っている
  • Nixはパッケージのビルドを純粋関数に見立てる
    • Nixが関数型パッケージマネージャーと呼ばれる由縁
AsahiAsahi

[前提知識] 純粋関数

  • Nixを知る上で非常に重要
  • 「関数型」といっても,モナドみたいな高度な抽象数学概念は登場しない
    • 参照透過性副作用が分かっていれば問題ない
    • 一応Nix言語は関数型言語だが,DSLなので高度な機能はない
  • 余談
    • こんなの絶対に関数型言語製だろ!と思った人へ,NixはC++製です(驚愕の事実)

プログラミング言語の関数と数学の関数

  • すぐ慣れてしまうが,プログラミング言語と数学の関数は全く異なる
  • プログラミング言語における関数のほとんどは「手続きの集まり」である
    • 故に「function」ではなく「procedure」と呼ぶことがある
  • 一方,関数型言語は関数を数学の関数と同じものとして扱う
    • 特に数学っぽく扱うものを純粋関数型言語と呼ぶ(らしい)
  • プログラミング言語の文脈で,特に数学的関数としての性質を満たしている関数を純粋関数と呼んでいる

参照透過性

  • 同じ引数を与えられたら必ず同じ出力が返ってくる
    • 中学校では「xの値が決まると、yの値が1つに決まる関係」が関数だと教わる
      • かなり大事な概念だったことに後から気づく
  • ハッシュ関数は参照透過(衝突性は一旦無視)
    • 同じ入力があったら同じハッシュ値が返る
    • 1bitでも入力が異なれば異なるハッシュ値が返る

副作用

  • 関数の「外側」にアクセスすること
    • スコープの外側にアクセス
    • 引数として与えられていない値を読み取ったり変更したりする
  • 日本語だと副作用と呼ぶことが多いけど,英語では普通に「Effect」というので注意
    • フロントエンドエンジニアに馴染み深いもの: useEffect
AsahiAsahi

Nixの純粋関数的ビルド

  • 「入力」が同じなら同じビルド成果物が出力される
    • 入力=依存関係,環境変数,ビルドスクリプト,etc...
    • 依存関係が一意になることはDerivationが保証している
      • 次はビルド実行の再現性を保証する
  • 副作用がない
    • 副作用=暗黙的依存
      • 指定した依存関係が関数に与えられた「引数」なら,その外にある暗黙的依存は副作用といえる

おおまかな流れ

  1. Derivationで指定されたシェル(基本的にbash)を起動
  2. 1.の環境に依存関係,環境変数などをDerivationで指定されたものだけ導入
  3. Derivationに記述されたビルドスクリプトを実行

特徴

  • サンドボックス環境
    • Derivationに記述されていないものはビルド環境に存在できない
      • 指定しない限りcoreutilsなども導入されないので,lscpすら使えなくなる
      • 暗黙的依存が確実に排除される
    • ネットワークアクセス不可
      • ネット越しのリソースはビルド環境の外側にある
        • リソースの冪等性を保証できない
      • もちろんこれでは困るので脱出口がある

面倒...

  • 純粋なbashにcoreutils等々をイチからセットアップするのは非常に面倒
  • なので,ビルド用のユーティリティが公式パッケージリポジトリnixpkgsに内包されている

  • stdenv
    • 最も広く使われている
    • coreutilsなどの基本ツールやgccが内包された環境を作る
    • gcc無し版やclang版もある
    • 基本的にstdenvか,stdenvを言語ごとに拡張した ビルドユーティリティが使われる
  • crate2nix
    • RustのcrateをNixパッケージ化するユーティリティ
    • ビルド環境はネットワークアクセス不可能なので,Cargoの代わりにFetcherで依存crateをダウンロードする
  • crane
    • 同じくRust用ユーティリティ
    • こちらはコミュニティ版
      • Nixの分散型パッケージレジストリを活かして,各々がパッケージを公開している
AsahiAsahi

ビルド vs バイナリダウンロード

  • Nixは常にビルドする
    • Nix言語→(コンパイル)→Derivation→ビルド実行
    • 遅くなりそう...
      • 解決している

ハッシュとバイナリキャッシュ

  1. パッケージAをビルド(長時間のビルドが走る)
  2. パッケージAのビルド成果物をDerivationのハッシュを識別子としてキャッシュ
  3. パッケージAをビルド
  4. パッケージAのDerivationのハッシュをキャッシュに照会
  5. ヒットしたのでビルドをスキップして直接ビルド成果物をダウンロード

  1. 同じ入力なら同じハッシュが出力される(Derivationのハッシュ)
  2. 同じ入力なら同じビルド成果物が出力される(純粋関数的ビルドシステム)

よって,ハッシュが同じなら同じビルド成果物が得られることが保証されているので,ビルド手順をスキップできる(天才)

実用例

  • Hydra
    • nixpkgsで運用されているCIシステム
      • 各種パッケージのビルド成果物をキャッシュしている
    • nixpkgsの場合,キャッシュが利用可能だとcache.nixor.orgからダウンロードが走る
  • Cachix
    • Nixのバイナリキャッシュのホスティングサービス
    • コミュニティのパッケージがよく利用している
  • Attic
    • セルフホスト可能なRust製Nixバイナリキャッシュサーバー
    • S3互換ストレージで利用できる
このスクラップは2024/04/02にクローズされました