Development shells with Nix: four quick examples

Michael Stapelberg

Nixによる開発シェル:すぐ試せる4つの例

プロジェクトの一つでGoCVを使いたいと思いました(大きなスキャン画像の中から紙の書類を見つけて抽出するためです)。ただし、OpenCVをシステムに恒久的に入れたくはありません。

これは、私がよく使うNixコマンドをいくつか紹介するのに適したユースケースだと思いました。手早く対話的に使う単発の開発シェルから、完全に宣言的で、隔離され、再現可能かつ共有可能な開発シェルまでを扱います。

特筆すべき点として、これらのコマンドを実行するのにNixOSを使う必要はありません。Nixパスを設定するか、Flakesを使えば(セットアップを参照)、DebianやArchなど、どのLinuxシステムにもNixをインストールして利用できます。

比較のために:Debian流

Nixを見る前に、DebianでGoCVを動かす方法を示します。

まず、プログラムをコンパイルできることを確認するためだけに、gocv.NewMat()のようなGoCV関数を使う最小限のGoプログラムを作ります。

package main

import "gocv.io/x/gocv"

func main() {
  gocv.NewMat()
}

Debianシステムでこれをビルドしようとすると、次のようになります。

debian % mkdir -p /tmp/minimal
debian % cd /tmp/minimal

debian % cat > minimal.go <<'EOT'
package main
import "gocv.io/x/gocv"
func main() { gocv.NewMat(); }
EOT

debian % go mod init minimal
go: creating new go.mod: module minimal
go: to add module requirements and sums:
	go mod tidy

debian % go mod tidy
go: finding module for package gocv.io/x/gocv
go: downloading gocv.io/x/gocv v0.41.0
go: found gocv.io/x/gocv in gocv.io/x/gocv v0.41.0

debian % go build
# gocv.io/x/gocv
# [pkg-config --cflags  -- opencv4]
Package opencv4 was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv4.pc'
to the PKG_CONFIG_PATH environment variable
Package 'opencv4', required by 'virtual:world', not found

Debianでは、次のようにOpenCVをインストールできます。

debian % sudo apt install libopencv-dev

[…]

Summary:
  Upgrading: 7, Installing: 512, Removing: 0, Not Upgrading: 27
  Download size: 367 MB
  Space needed: 1590 MB / 281 GB available

Continue? [Y/n]

このプロンプトで「yes」と答えると、500個を超えるパッケージがダウンロード・インストールされます(数分かかります)。

これでビルドは成功します。

debian % go build
debian % file minimal
minimal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), […]

……しかし、これから永遠に更新し続ける必要がある500個以上の余分なパッケージがシステムに入ってしまいました。そのため、この単発の実験は普段使っているシステムから分離したいのです。

DockerでDebianコンテナを起動し、その中で作業することもできます。しかし、タスクによっては、別環境であること自体が扱いにくさにつながります。この例では、入力ファイルをDockerコンテナから使えるようにボリュームマウントを指定する必要があります。また、Dockerコンテナ内のプログラムがホスト上でグラフィカルウィンドウを開けるように、環境変数も設定しなければなりません……

Nixがどう役立つのか見ていきましょう。

セットアップ:Debian上のNix(あるいはArch上のNix、その他)

NixOSの利用者は、この節を飛ばせます。NixOSにはすぐに使えるNixが含まれているためです。

自分のコンピューターで例を試すには、次の3ステップを完了する必要があります。

  1. Nixをインストールする
  2. Flakesを有効にする
  3. Nixパスを設定する

ステップ1:Nixをインストールする

Debian、Arch、Fedora、その他のLinuxシステムの利用者は、まずNixをインストールする必要があります。幸い、Nixは多くの主要Linuxディストリビューションで利用できます。

ステップ2:Flakesを有効にする

Nix flakesは、「Nixアーティファクトをパッケージ化する汎用的な方法」です。

例3と例4では依存関係を固定するためにNix flakesを使うので、Nix flakesを有効化する必要があります。

ステップ3:Nixパスを設定する

例1と例2では、Nix式のimport <nixpkgs>を使います。

NixOSでは、この式はシステムのバージョンに追従します。つまり、NixOS 25.05のインストール環境でimport <nixpkgs>を使うと、nixos-25.05バージョンのnixpkgsを参照します。

他のLinuxシステムでは、次のようなエラーメッセージが表示されます。

debian-server % nix-shell -p pkg-config opencv
error: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)

       at «string»:1:25:

            1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (pkg-config) (opencv) ]; } ""
             |                         ^
(use '--show-trace' to show detailed location information)

使うnixpkgsのバージョンをNixに伝えるため、Nix検索パスを設定する必要があります。

debian-server % export NIX_PATH=nixpkgs=channel:nixos-25.05
debian-server % nix-shell -p pkg-config opencv
[nix-shell:/tmp/opencv]#

よし、これでセットアップは完了です。最初の例に進みましょう。

例1:対話的な単発作業:nix-shell

Nixは、システムにOpenCVをインストールすること(上の例のapt install)と、別のDockerコンテナにOpenCVをインストールすることの中間を提供します。Nixなら、OpenCVを恒久的にインストールせずに利用可能にできます。

nix-shell(1)を実行すると、指定したパッケージが利用できるbashシェルを起動できます。GoCVを使うGoコードを正しくビルドするには、OpenCVを利用可能にする必要があります。

% nix-shell -p pkg-config opencv
these 194 paths will be fetched (175.80 MiB download, 764.10 MiB unpacked):
  /nix/store/ig2nk0hsha9xaailhaj69yv677nv95q4-abseil-cpp-20210324.2
  /nix/store/yw5xqn8lqinrifm9ij80nrmf0i6fdcbx-alsa-lib-1.2.13
[…]

[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4

気になったかもしれませんが、はい、このnix-shellコマンドではpkg-configを明示的に指定する必要があります。そうしないと、pkg-configを実行した際にホスト側のバージョン(開発シェルの外側)が実行され、opencv4.pcを見つけられません。

例2:nix-shellの設定ファイル:shell.nix

プロジェクトで動作するパッケージの組み合わせ(この例ではpkg-configopencvだけ)が分かったら、shell.nixを作成できます。任意のディレクトリに置けますが、通常はプロジェクトのルートに置きます。このファイルは、-pフラグなしのnix-shellが読み込みます。

{
  pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
  packages = with pkgs; [
    # Explicitly list pkg-config so that mkShell will arrange
    # for the PKG_CONFIG_PATH to find the .pc files.
    pkg-config
    opencv
  ];
}

あとはnix-shellを実行するだけです。

% nix-shell
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4

パッケージ一覧の前後にあるボイラープレートが気になる方のために、関連ドキュメントをいくつか挙げておきます。

  • 1〜3行目は、引数セットを持つ関数を宣言しています。nix-shellshell.nixファイルを呼び出せるようにするために必要な構造です。
  • pkgs.mkShellは、nix-shellで使うための便利なヘルパーです。
  • with pkgs;の部分により、pkgs.opencvではなくopencvと書けます。

ちなみに、nixd言語サーバーを使うと、LSPをサポートするエディターでは、パッケージが解決されるバージョンを表示したり、スペルミスを指摘したり、「定義へ移動」のような機能を利用したりできます。

たとえばこのスクリーンショットでは、Emacsでshell.nixを編集中に、opencvパッケージのNixソースがどのようなものか気になりました。opencv上に「ポイント」を置いてM-.xref-find-definitions)を押すと、ローカルのNixストアにあるopencv/4.x.nixへ移動できました。

opencvの定義へ移動した後にopencv/4.x.nixを表示するEmacs

例3:隔離され、バージョンを固定したdevShell:Nix Flakes

これまでの例では、システム上の(またはNixパスにある)nixpkgsを使っていました。つまり、システムをアップグレードしても.nixファイルを変更する必要はありません。ユースケース次第ですが、私はこの挙動を便利だと感じることもあれば、恐ろしいと感じることもあります。

周囲のOSがどのバージョンであっても、.nixファイルがまったく同じ方法でビルドされることが重要なユースケースでは、Nix Flakesを使い、flake.lockファイルで依存関係のバージョンを固定した隔離的なビルドを行えます。

flake.nixには上記と同じmkShell式が含まれますが、その周囲に構造を宣言します。mkShell式はoutputs.devShells.x86_64-linux.default属性に入り、inputs属性には、このビルドで利用できるFlake参照が含まれます。

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";

  outputs =
    { self, nixpkgs }:
    {
      devShells.x86_64-linux.default =
        let
          pkgs = nixpkgs.legacyPackages.x86_64-linux;
        in
        pkgs.mkShell {
          packages = with pkgs; [
            # Explicitly list pkg-config so that mkShell will arrange
            # for the PKG_CONFIG_PATH to find the .pc files.
            pkg-config
            opencv
          ];
        };
    };
}

ちなみに、名前に反してnixpkgs.legacyPackagesを使うのがベストプラクティスです。これは概念的には単一のimport nixpkgs結果を提供します(効率のためです)。

これで、nix developを使えばOpenCVを使えるシェルを取得できます。

% nix develop
michael@midna$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4

最初のnix develop実行時にflake.lockファイルが作成されるため、それ以降にnix developを実行すると、まったく同じ環境を取得できます。より新しいバージョンへ更新するには、nix flake updateを使います。

ヒント:シェルの代わりに、nix develop --command=emacsも便利な使い方です。

例4:Flakeをシステム非依存にする

残念ながら、上記のflake.nixではx86_64-linuxがハードコードされています。そのため、たとえばaarch64-linux(ARM)コンピューターやx86_64-darwin(Mac)では使えません。

デフォルトでsystemを明示的に指定しなければならないことは、Nix Flakesに対する長年の批判です。

回避策はいくつかあります。たとえば、numtide/flake-utilsを使い、その便利なeachDefaultSystem関数を使うようにflake.nixをリファクタリングできます。

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      flake-utils,
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        formatter = pkgs.nixfmt-tree;
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            # Explicitly list pkg-config so that mkShell will arrange
            # for the PKG_CONFIG_PATH to find the .pc files.
            pkg-config
            opencv
          ];
        };
      }
    );
}

あるいは、その精神的後継であるnumtide/blueprintを使うこともできます。

LucPerkinsのdev-templatesでは、この手法のバージョンが実質的にインライン化されています

Nix本体の一部ではないものの、Nixに隣接する解決策としては、devenvがあります。これはNix上に構築された別ツールであり(もはやCppNix実装ではなく、実際にはtvixを使っています)、独自の.nixファイルを持ちます。

ヒント:パッケージを保持する

flake.lockが変わっていないにもかかわらず、nix developなどのコマンドがパッケージをフェッチしていることに気付いたら、Flakeをプロファイルにインストールし、Nixのgcrootとして宣言できます。

% nix profile install .#devShells.x86_64-linux.default

でも待ってください。これはDebian流と同じ状態に戻ることになりませんか? いいえ。FlakeをプロファイルにインストールすればOpenCVは無期限に利用可能なままですが、それでも分離の層はあります。システム内でOpenCVを利用できるわけではなく、nix-shellまたはnix developで開発シェルを起動したときにだけ利用できます。

まとめ

上記4つの例は、どのように比較できるでしょうか。概要は次のとおりです。

ボイラープレート固定済み?システム依存?
例1nix-shell -p …😊いいえいいえ
例2shell.nix🙂いいえいいえ
例3flake.nix😲はいはい
例4:システム非依存のflake.nix🤨はいいいえ

個人的な単発の実験には、nix-shellを使っています。

実験がうまくいったら、たいてい依存関係を固定したくなるので、flake.nixを使います。

単にバージョン管理されるだけでなく、公開される(あるいは複数の人やシステムで作業する)ソフトウェアであれば、システム非依存のflake.nixにする手間をかけます。

将来的には、システム非依存のflakeをより簡単に書けるようになることを願っています。

粗削りな部分はあるものの、Nixがもたらす再現性と制御性を高く評価しています。

原文は Michael Stapelberg により に公開されました。

この記事は「gpt-5.6-terra」を使用して翻訳されました。