はじめに
最近、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.nix
とmain.rs
です。
{
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;
};
}
);
}
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のビルド時にはclang
とpkg-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
で実行する
参考リンク
- 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
を作成するまでが大変なことも多いですが、
一度作成してしまえば、どこでもすぐに動かせるので重宝しています。