opencv 0.94.0

Rust bindings for OpenCV
Documentation
use std::collections::HashMap;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
use std::{env, fs, thread};

use collector::Collector;
use opencv_binding_generator::{Generator, IteratorExt};

use super::docs::transfer_bindings_to_docs;
use super::{files_with_predicate, Library, Result, OUT_DIR, SRC_CPP_DIR, SRC_DIR};

#[path = "generator/collector.rs"]
mod collector;

pub struct BindingGenerator<'r> {
	build_script_path: &'r Path,
	modules: &'r [String],
	module_aliases: &'r HashMap<&'r str, &'r str>,
}

impl<'r> BindingGenerator<'r> {
	pub fn new(build_script_path: &'r Path, modules: &'r [String], module_aliases: &'r HashMap<&'r str, &'r str>) -> Self {
		Self {
			build_script_path,
			modules,
			module_aliases,
		}
	}

	pub fn generate_wrapper(&self, opencv_header_dir: &Path, opencv: &Library, ffi_export_suffix: &str) -> Result<()> {
		let target_docs_dir = env::var_os("OCVRS_DOCS_GENERATE_DIR").map(PathBuf::from);
		let target_module_dir = OUT_DIR.join("opencv");
		let manual_dir = SRC_DIR.join("manual");

		eprintln!("=== Generating code in: {}", OUT_DIR.display());
		eprintln!("=== Placing generated bindings into: {}", target_module_dir.display());
		if let Some(target_docs_dir) = target_docs_dir.as_ref() {
			eprintln!(
				"=== Placing static generated docs bindings into: {}",
				target_docs_dir.display()
			);
		}
		eprintln!("=== Using OpenCV headers from: {}", opencv_header_dir.display());

		let non_dll_files = files_with_predicate(&OUT_DIR, |p| {
			let extension_is_dll = p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("dll"));
			!extension_is_dll
		})?;
		for path in non_dll_files {
			let _ = fs::remove_file(path);
		}

		self.run(opencv_header_dir, opencv)?;

		Collector::new(
			self.modules,
			self.module_aliases,
			ffi_export_suffix,
			&target_module_dir,
			&manual_dir,
			&OUT_DIR,
		)
		.collect_bindings()?;

		if let Some(target_docs_dir) = target_docs_dir {
			if !target_docs_dir.exists() {
				fs::create_dir(&target_docs_dir)?;
			}
			transfer_bindings_to_docs(&OUT_DIR, &target_docs_dir);
		}

		Ok(())
	}

	fn run(&self, opencv_header_dir: &Path, opencv: &Library) -> Result<()> {
		let additional_include_dirs = opencv
			.include_paths
			.iter()
			.filter(|&include_path| include_path != opencv_header_dir)
			.map(|path| path.as_path())
			.collect::<Vec<_>>();

		let gen = Generator::new(opencv_header_dir, &additional_include_dirs, &SRC_CPP_DIR);
		if !gen.is_clang_loaded() {
			eprintln!("=== ERROR: Unable to load libclang library, check item #8 in https://github.com/twistedfall/opencv-rust/blob/master/README.md#troubleshooting");
			eprintln!(
				"=== Try enabling `clang-runtime` feature of the `opencv` crate, or alternatively disabling it if it's enabled"
			);
			return Err("a `libclang` shared library is not loaded on this thread".into());
		}
		eprintln!("=== Clang: {}", gen.clang_version());
		eprintln!("=== Clang command line args: {:#?}", gen.build_clang_command_line_args());

		let additional_include_dirs = additional_include_dirs
			.into_iter()
			.map(|p| p.to_str().expect("Can't convert additional include dir to UTF-8 string"))
			.join(",");
		let job_server = Jobserver::build()?;
		let start = Instant::now();
		eprintln!("=== Generating {} modules", self.modules.len());
		thread::scope(|scope| {
			let join_handles = self
				.modules
				.iter()
				.map(|module| {
					let token = job_server.acquire().expect("Can't acquire token from job server");
					thread::Builder::new()
						.name(format!("gen-{module}"))
						.spawn_scoped(scope, {
							let additional_include_dirs = additional_include_dirs.as_str();
							move || {
								let module_start = Instant::now();
								let mut bin_generator = Command::new(self.build_script_path);
								bin_generator
									.arg(opencv_header_dir)
									.arg(&*SRC_CPP_DIR)
									.arg(&*OUT_DIR)
									.arg(module)
									.arg(additional_include_dirs);
								eprintln!("=== Running: {bin_generator:?}");
								let res = bin_generator
									.status()
									.unwrap_or_else(|e| panic!("Can't run bindings generator for module: {module}, error: {e}"));
								if !res.success() {
									panic!("Failed to run the bindings generator for module: {module}");
								}
								eprintln!("=== Generated: {module} in {:?}", module_start.elapsed());
								drop(token); // needed to move the token to the thread
							}
						})
						.expect("Error spawning thread")
				})
				.collect::<Vec<_>>();
			for join_handle in join_handles {
				join_handle.join().expect("Generator process panicked");
			}
		});
		eprintln!("=== Total binding generation time: {:?}", start.elapsed());
		Ok(())
	}
}

pub struct Jobserver {
	client: jobserver::Client,
	reacquire_token_on_drop: bool,
}

impl Jobserver {
	pub fn build() -> Result<Self> {
		unsafe { jobserver::Client::from_env() }
			.and_then(|client| {
				let own_token_released = client.release_raw().is_ok();
				let available_jobs = client.available().unwrap_or(0);
				if available_jobs > 0 {
					eprintln!("=== Using environment job server with the the amount of available jobs: {available_jobs}");
					Some(Jobserver {
						client,
						reacquire_token_on_drop: own_token_released,
					})
				} else {
					if own_token_released {
						client.acquire_raw().expect("Can't reacquire build script thread token");
					}
					eprintln!(
						"=== Available jobs from the environment created jobserver is: {available_jobs} or there is an error reading that value"
					);
					None
				}
			})
			.or_else(|| {
				let num_jobs = env::var("NUM_JOBS")
					.ok()
					.and_then(|jobs| jobs.parse().ok())
					.or_else(|| thread::available_parallelism().map(|p| p.get()).ok())
					.unwrap_or(2)
					.max(1);
				eprintln!("=== Creating a new job server with num_jobs: {num_jobs}");
				jobserver::Client::new(num_jobs).ok().map(|client| Jobserver {
					client,
					reacquire_token_on_drop: false,
				})
			})
			.ok_or_else(|| "Can't create job server".into())
	}
}

impl Drop for Jobserver {
	fn drop(&mut self) {
		if self.reacquire_token_on_drop {
			self.client.acquire_raw().expect("Can't reacquire build script thread token");
		}
	}
}

impl Deref for Jobserver {
	type Target = jobserver::Client;

	fn deref(&self) -> &Self::Target {
		&self.client
	}
}