NixでRust + OpenCVのプロジェクトをビルドする

# はじめに

最近、Nix(Nix flakes)を使い始めました。  
`flake.nix`を適切に作成すれば、Nix以外に何も入れていなくても、`nix build`コマンドを叩くだけで色々なものをビルド出来て非常に便利です。  
今回は、「[opencv-rust](https://github.com/twistedfall/opencv-rust)を使用したRustプロジェクト」をビルドするための`flake.nix`を作成したので紹介します。  

opencv-rustを使用すると、RustからOpenCVを利用することが出来ますが、  
単にRustをインストールしただけではビルド出来ず、別途OpenCV自体やClang等のインストールも必要でやや面倒です。  
まさにNixが活きる場面だと思い使ってみましたが、いくつかハマりどころがあったので解説します。

# 前提

NixでRustプロジェクトをビルドする方法については、[『Nix入門: ハンズオン編』の`§2. Rustプロジェクトのビルド`](https://zenn.dev/asa1984/books/nix-hands-on/viewer/ch04-02-rust-project) で詳しく解説されています。  
NixでRustプロジェクトをビルドしたことが無い方は、本記事の前に上記ハンズオンを読むことをおススメします。  

本記事では、上記ハンズオンの内容や、Nix flakesの基本的な使い方は理解していることを前提として、  
opencv-rustを使用する際の特有の内容のみを解説します。

# 環境

- OS: Manjaro Linux
- Nix: 2.24.9

# ソースコード

リポジトリは以下で公開しています。

https://github.com/okmt765/nix-opencv-rust-example

今回メインとなるのは以下の`flake.nix`と`main.rs`です。

```nix:flake.nix
{
  description = "Nix opencv-rust example.";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs =
    inputs@{ self, ... }:
    inputs.flake-utils.lib.eachDefaultSystem (
      system:
      let
        overlays = [ (import inputs.rust-overlay) ];
        pkgs = import (inputs.nixpkgs) { inherit system overlays; };

        nativeBuildInputs = with pkgs; [
          clang
          pkg-config
          rustPlatform.bindgenHook
          makeWrapper
        ];

        buildInputs = with pkgs; [
          (opencv4.override { enableGtk3 = true; })
        ];

        rustPlatform = pkgs.makeRustPlatform {
          cargo = pkgs.rust-bin.stable.latest.minimal;
          rustc = pkgs.rust-bin.stable.latest.minimal;
        };

        GST_PLUGIN_SYSTEM_PATH_1_0 =
          with pkgs.gst_all_1;
          "${gstreamer.out}/lib/gstreamer-1.0:${gst-plugins-base}/lib/gstreamer-1.0:${gst-plugins-good}/lib/gstreamer-1.0";
      in
      {
        packages.default = rustPlatform.buildRustPackage rec {
          inherit buildInputs nativeBuildInputs;

          name = "nix-opencv-rust-example";
          src = ./.;
          version = "0.0.1";
          meta.mainProgram = name;

          cargoLock = {
            lockFile = ./Cargo.lock;
            allowBuiltinFetchGit = true;
          };

          postFixup = ''
            wrapProgram $out/bin/${name} \
              --prefix GST_PLUGIN_SYSTEM_PATH_1_0 : ${GST_PLUGIN_SYSTEM_PATH_1_0}
          '';
        };

        devShells.default = pkgs.mkShell {
          inherit nativeBuildInputs;
          inherit GST_PLUGIN_SYSTEM_PATH_1_0;

          buildInputs =
            buildInputs
            ++ (with pkgs.rust-bin; [
              (stable.latest.minimal.override {
                extensions = [
                  "clippy"
                  "rust-src"
                ];
              })
              nightly.latest.rustfmt
              nightly.latest.rust-analyzer
            ]);

          RUST_BACKTRACE = 1;
        };
      }
    );
}
```

```rust:main.rs
use anyhow::{bail, Result};
use opencv::{core, highgui, prelude::*, videoio};

fn main() -> Result<()> {
    let args: Vec<String> = std::env::args().collect();
    if args.len() != 2 {
        bail!("USAGE: nix-opencv-rust-example <PATH>");
    }

    let path = &args[1];
    let mut cap = match path.parse() {
        # カメラ番号(0などの数値)の場合
        Ok(index) => videoio::VideoCapture::new(index, videoio::CAP_ANY)?,
        # それ以外(ファイルパスやGStreamerのパイプライン)の場合
        Err(_) => videoio::VideoCapture::from_file(path, videoio::CAP_ANY)?,
    };

    if !videoio::VideoCapture::is_opened(&cap)? {
        bail!("Failed to open VideoCapture.");
    }

    let mut img = core::Mat::default();
    loop {
        if !cap.read(&mut img)? {
            break;
        }
        dbg!(&img);

        let _ = highgui::imshow("image", &img);
        let _ = highgui::wait_key(1);
    }

    Ok(())
}
```

# 解説

コマンドライン引数で指定した値を`VideoCapture`で開き、`imshow()`で映像を表示するだけの簡単なサンプルプログラムです。  
以下のような引数に対応しています。  

- カメラ番号(`0`など)
- ファイルパス(`hoge.mp4`など)
- GStreamerのパイプライン(`'videotestsrc ! appsink'`など)[^gst]

`main.rs`については特筆する点は無いので、`flake.nix`について解説していきます。

## 依存関係の記述

```nix
nativeBuildInputs = with pkgs; [
  clang
  pkg-config
  rustPlatform.bindgenHook
  makeWrapper
];
```

ビルド時にのみ必要な依存は`nativeBuildInputs`に記載します。  
opencv-rustのビルド時には`clang`と`pkg-config`が必要なので含めています。  
また、そのままではopencv-rust[^bindgenhook]のビルド時に`libclang.so`を上手く見つけられずエラーになるのですが、  
`rustPlatform.bindgenHook`を含めておけばビルドできるようになります。  
`makeWrapper`は後述の`wrapProgram`の使用に必要なので含めています。

```nix
buildInputs = with pkgs; [
  (opencv4.override { enableGtk3 = true; })
];
```

実行時にも必要な依存は`buildInputs`に記載します。  
OpenCVの`imshow()`でのウィンドウ表示にはGTKが必要なのですが、  
Nixpkgsの`opencv4`では[デフォルトで無効になっている](https://github.com/NixOS/nixpkgs/blob/24.11/pkgs/development/libraries/opencv/4.x.nix#L56)ため、overrideして有効化しています。[^gtk]  
GStreamerはデフォルトで有効になっており、`opencv4`の依存に含まれるためここでは記載不要です。

## GStreamerのプラグインパス指定

```nix
GST_PLUGIN_SYSTEM_PATH_1_0 =
  with pkgs.gst_all_1;
  "${gstreamer.out}/lib/gstreamer-1.0:${gst-plugins-base}/lib/gstreamer-1.0:${gst-plugins-good}/lib/gstreamer-1.0";
```

```nix
postFixup = ''
  wrapProgram $out/bin/${name} \
    --prefix GST_PLUGIN_SYSTEM_PATH_1_0 : ${GST_PLUGIN_SYSTEM_PATH_1_0}
'';
```

Nixで入れたGStreamerのプラグインは、通常とは異なる場所(`/nix/store/`以下)に配置されるため、  
実行時に環境変数で場所を指定しなければ見つけることが出来ません。  
そのため、[`wrapProgram`](https://nixos.org/manual/nixpkgs/unstable/#fun-wrapProgram)を使用して、`nix run`時や`nix build`の結果ファイルの実行時に、  
環境変数を設定するシェルスクリプト経由で実行されるようにしています。

<details><summary>シェルスクリプトの例</summary>

通常は、`nix build`すると、ビルド結果のバイナリが`result/bin/nix-opencv-rust-example`に配置されますが、  
`wrapProgram`を使用すると、代わりに以下のようなシェルスクリプトが配置されるようになります。  
(この場合、本来のビルド結果のバイナリは`result/bin/.nix-opencv-rust-example-wrapped`に配置されています)

```bash
#! /nix/store/p6k7xp1lsfmbdd731mlglrdj2d66mr82-bash-5.2p37/bin/bash -e
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0:+':'$GST_PLUGIN_SYSTEM_PATH_1_0':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0/':''/nix/store/3znjs3il3w3pbv454ayvxc27dyy20ghh-gst-plugins-good-1.24.7/lib/gstreamer-1.0'':'/':'}
GST_PLUGIN_SYSTEM_PATH_1_0='/nix/store/3znjs3il3w3pbv454ayvxc27dyy20ghh-gst-plugins-good-1.24.7/lib/gstreamer-1.0'$GST_PLUGIN_SYSTEM_PATH_1_0
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0#':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0%':'}
export GST_PLUGIN_SYSTEM_PATH_1_0
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0:+':'$GST_PLUGIN_SYSTEM_PATH_1_0':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0/':''/nix/store/i8r5f1y4kjbigmmlji2hyipm6f8gfaz1-gst-plugins-base-1.24.7/lib/gstreamer-1.0'':'/':'}
GST_PLUGIN_SYSTEM_PATH_1_0='/nix/store/i8r5f1y4kjbigmmlji2hyipm6f8gfaz1-gst-plugins-base-1.24.7/lib/gstreamer-1.0'$GST_PLUGIN_SYSTEM_PATH_1_0
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0#':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0%':'}
export GST_PLUGIN_SYSTEM_PATH_1_0
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0:+':'$GST_PLUGIN_SYSTEM_PATH_1_0':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0/':''/nix/store/jxhvcy7ckv5nzqqb1rxl72wv09dxa84x-gstreamer-1.24.7/lib/gstreamer-1.0'':'/':'}
GST_PLUGIN_SYSTEM_PATH_1_0='/nix/store/jxhvcy7ckv5nzqqb1rxl72wv09dxa84x-gstreamer-1.24.7/lib/gstreamer-1.0'$GST_PLUGIN_SYSTEM_PATH_1_0
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0#':'}
GST_PLUGIN_SYSTEM_PATH_1_0=${GST_PLUGIN_SYSTEM_PATH_1_0%':'}
export GST_PLUGIN_SYSTEM_PATH_1_0
exec -a "$0" "/nix/store/0kqql9vwzddqbkcsvij4fy5r6h12b76a-nix-opencv-rust-example/bin/.nix-opencv-rust-example-wrapped"  "$@" 
```

</details>

```nix
devShells.default = pkgs.mkShell {
  inherit nativeBuildInputs;
  inherit GST_PLUGIN_SYSTEM_PATH_1_0;
```

devShell内で直接`cargo run`した場合にも動作するよう、devShellの環境変数にも`GST_PLUGIN_SYSTEM_PATH_1_0`を追加しています。

# 実行例

以下のような方法で実行することが出来ます。

- `nix run . -- hoge.mp4`のように`nix run`で実行する
- `nix build`してから`result/bin/nix-opencv-rust-example hoge.mp4`のように実行する
- `nix develop`でdevShellに入り、`cargo run -- hoge.mp4`のように`cargo run`で実行する

# 参考リンク

- https://zenn.dev/asa1984/books/nix-introduction
- https://zenn.dev/asa1984/books/nix-hands-on
- https://nixos.org/manual/nixpkgs/unstable/#fun-wrapProgram
- https://github.com/twistedfall/opencv-rust

# おわりに

Nix歴数週間の初心者ですが、すでにNixが手放せなくなりつつあります。  
今回のように、上手くビルドできる`flake.nix`を作成するまでが大変なことも多いですが、  
一度作成してしまえば、どこでもすぐに動かせるので重宝しています。

[^gst]: OpenCVはGStreamerを有効化してビルドした場合、`VideoCapture`の入力ソースにパイプラインを指定できるようになります。
[^bindgenhook]: 正確には、opencv-rustが依存している`clang-sys`クレート
[^gtk]: これをしないと、「特にエラーは出ていないのに何故かウィンドウが表示されない」という現象に悩まされることになります(なりました)。