Development shells with Nix: four quick examples

Michael Stapelberg

Nixの開発シェル:4つの手軽な例

原文は Michael Stapelberg により に公開されました。 このブログを購読する

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

これは、私がよく使ういくつかのNixコマンドを紹介するのにちょうどいい事例に思えました。手軽な対話的で使い捨ての開発シェルから、完全に宣言的でHermetic、再現性があり共有可能な開発シェルまでをカバーします。

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

比較のために:Debian流のやり方

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

GoCVの関数、たとえばgocv.NewMat()を使う最小限の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コンテナから使えるようにボリュームマウントを指定する必要があり、コンテナ内のプログラムがホスト側でグラフィカルなウィンドウを開けるように環境変数を設定する必要もあります……

では、Nixがこの問題をどう解決してくれるか見てみましょう!

セットアップ:DebianでNixを使う(あるいはArchなどで)

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)

Nixにどのバージョンのnixpkgsを使うかを教えるため、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を恒久的にインストールすることなく利用可能にできるのです。

指定したパッケージが利用可能なbashシェルを起動するには、nix-shell(1)を実行します。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を作成できます(どのディレクトリでも構いませんが、通常はプロジェクトのルートに置きます)。nix-shell-pフラグなし)がこれを読み込みます。

{
  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.mkShellnix-shellで使うための便利なヘルパーです。
  • with pkgs;の部分により、pkgs.opencvではなくopencvと書けるようになります。

ちなみに、nixd language serverを使えば、LSPサポートのあるエディタで、パッケージが解決されるバージョンを表示したり、スペルミスを指摘したり、「定義へジャンプ」のような機能を提供したりできます。

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

Emacsでopencvの定義へジャンプした後にopencv/4.x.nixを表示している様子

例3:Hermeticでバージョン固定されたdevShell:Nix Flakes

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

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

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

{
  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.nixx86_64-linuxをハードコードしているため、たとえばaarch64-linux(ARM)マシンやx86_64-darwin(Mac)では使えません。

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

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

{
  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-shellnix developで開発シェルを起動したときだけ利用できるのです。

まとめ

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

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

個人的な一度きりの実験には、nix-shellを使っています。

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

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

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

細かい粗さはあるものの、Nixがもたらしてくれる再現性と制御には感謝しています!

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント