2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

前提

NixでRustプロジェクトをビルドする方法については、『Nix入門: ハンズオン編』の§2. Rustプロジェクトのビルド で詳しく解説されています。
NixでRustプロジェクトをビルドしたことが無い方は、本記事の前に上記ハンズオンを読むことをおススメします。

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

環境

  • OS: Manjaro Linux
  • Nix: 2.24.9

ソースコード

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

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

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;
        };
      }
    );
}
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'など)1

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

依存関係の記述

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

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

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

実行時にも必要な依存はbuildInputsに記載します。
OpenCVのimshow()でのウィンドウ表示にはGTKが必要なのですが、
Nixpkgsのopencv4ではデフォルトで無効になっているため、overrideして有効化しています。3
GStreamerはデフォルトで有効になっており、opencv4の依存に含まれるためここでは記載不要です。

GStreamerのプラグインパス指定

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";
postFixup = ''
  wrapProgram $out/bin/${name} \
    --prefix GST_PLUGIN_SYSTEM_PATH_1_0 : ${GST_PLUGIN_SYSTEM_PATH_1_0}
'';

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

シェルスクリプトの例

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

#! /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"  "$@" 
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で実行する

参考リンク

おわりに

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

  1. OpenCVはGStreamerを有効化してビルドした場合、VideoCaptureの入力ソースにパイプラインを指定できるようになります。

  2. 正確には、opencv-rustが依存しているclang-sysクレート

  3. これをしないと、「特にエラーは出ていないのに何故かウィンドウが表示されない」という現象に悩まされることになります(なりました)。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?