diff --git a/.github/workflows/opencv-rust.yml b/.github/workflows/opencv-rust.yml index 972bea6f8..7a957e6cb 100644 --- a/.github/workflows/opencv-rust.yml +++ b/.github/workflows/opencv-rust.yml @@ -68,9 +68,16 @@ jobs: - macos-14 vcpkg-version: - 2025.01.13 # https://github.com/microsoft/vcpkg/releases + vcpkg-features: + - contrib,nonfree,ade,opencl + include: + - os-image: windows-2022 + vcpkg-version: 2025.01.13 + vcpkg-features: contrib runs-on: ${{ matrix.os-image }} env: VCPKG_VERSION: ${{ matrix.vcpkg-version }} + VCPKG_FEATURES: ${{ matrix.vcpkg-features }} SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: @@ -80,7 +87,7 @@ jobs: - uses: actions/cache@v4 with: path: ~/build - key: vcpkg-${{ matrix.vcpkg-version }}-${{ matrix.os-image }} + key: vcpkg-${{ matrix.vcpkg-version }}-${{ matrix.os-image }}-${{ matrix.vcpkg-features }} - name: Install dependencies run: ci/install.sh diff --git a/INSTALL.md b/INSTALL.md index 10ae92c62..ef86b3752 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -56,7 +56,7 @@ Installing OpenCV is easy through the following sources: * from [vcpkg](https://docs.microsoft.com/en-us/cpp/build/vcpkg), also install `llvm` package, necessary for building: ```shell script - vcpkg install llvm opencv4[contrib,nonfree,opencl] + vcpkg install llvm opencv4[contrib,nonfree] ``` You most probably want to set environment variable `VCPKGRS_DYNAMIC` to "1" unless you're specifically targeting a static build. diff --git a/build.rs b/build.rs index 1f0ce2aef..6ddf0bdde 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::env; use std::ffi::OsStr; -use std::fs::File; -use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -169,61 +167,131 @@ fn files_with_extension<'e>(dir: &Path, extension: impl AsRef + 'e) -> Re }) } -fn get_module_header_dir(header_dir: &Path) -> Option { - let mut out = header_dir.join("opencv2.framework/Headers"); - if out.exists() { - return Some(out); - } - out = header_dir.join("opencv2"); - if out.exists() { - return Some(out); +mod header { + use std::fs::File; + use std::io::{BufRead, BufReader}; + use std::path::{Path, PathBuf}; + use std::process::Command; + + use semver::Version; + + pub fn get_module_header_dir(header_dir: &Path) -> Option { + let mut out = header_dir.join("opencv2.framework/Headers"); + if out.is_dir() { + return Some(out); + } + out = header_dir.join("opencv2"); + if out.is_dir() { + return Some(out); + } + None } - None -} -fn get_version_header(header_dir: &Path) -> Option { - get_module_header_dir(header_dir) - .map(|dir| dir.join("core/version.hpp")) - .filter(|hdr| hdr.is_file()) -} + /// Something like `/usr/include/x86_64-linux-gnu/opencv4/` on newer Debian-derived distros + pub fn get_multiarch_header_dir() -> Option { + let try_multiarch = Command::new("dpkg-architecture") + .args(["--query", "DEB_TARGET_MULTIARCH"]) + .output() + .inspect_err(|e| eprintln!("=== Failed to get DEB_TARGET_MULTIARCH: {e}")) + .ok() + .or_else(|| { + Command::new("cc") + .arg("-print-multiarch") + .output() + .inspect_err(|e| eprintln!("=== Failed to get -print-multiarch: {e}")) + .ok() + }) + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map_or_else( + || { + eprintln!("=== Failed to get DEB_TARGET_MULTIARCH, trying common multiarch paths"); + vec![ + "x86_64-linux-gnu".to_string(), + "aarch64-linux-gnu".to_string(), + "arm-linux-gnueabihf".to_string(), + ] + }, + |multiarch| vec![multiarch.trim().to_string()], + ); -fn get_version_from_headers(header_dir: &Path) -> Option { - let version_hpp = get_version_header(header_dir)?; - let mut major = None; - let mut minor = None; - let mut revision = None; - let mut line = String::with_capacity(256); - let mut reader = BufReader::new(File::open(version_hpp).ok()?); - while let Ok(bytes_read) = reader.read_line(&mut line) { - if bytes_read == 0 { - break; + eprintln!("=== Trying multiarch paths: {try_multiarch:?}"); + + for multiarch in try_multiarch { + let header_dir = PathBuf::from(format!("/usr/include/{multiarch}/opencv4")); + if header_dir.is_dir() { + return Some(header_dir); + } } - if let Some(line) = line.strip_prefix("#define CV_VERSION_") { - let mut parts = line.split_whitespace(); - if let (Some(ver_spec), Some(version)) = (parts.next(), parts.next()) { - match ver_spec { - "MAJOR" => { - major = Some(version.parse().ok()?); - } - "MINOR" => { - minor = Some(version.parse().ok()?); - } - "REVISION" => { - revision = Some(version.parse().ok()?); + None + } + + pub fn get_version_header(header_dir: &Path) -> Option { + get_module_header_dir(header_dir) + .map(|dir| dir.join("core/version.hpp")) + .filter(|hdr| hdr.is_file()) + } + + pub fn get_config_header(header_dir: &Path) -> Option { + get_module_header_dir(header_dir) + .map(|dir| dir.join("cvconfig.h")) + .filter(|hdr| hdr.is_file()) + } + + pub fn find_version(header_dir: &Path) -> Option { + let version_hpp = get_version_header(header_dir)?; + let mut major = None; + let mut minor = None; + let mut revision = None; + let mut line = String::with_capacity(256); + let mut reader = BufReader::new(File::open(version_hpp).ok()?); + while let Ok(bytes_read) = reader.read_line(&mut line) { + if bytes_read == 0 { + break; + } + if let Some(line) = line.strip_prefix("#define CV_VERSION_") { + let mut parts = line.split_whitespace(); + if let (Some(ver_spec), Some(version)) = (parts.next(), parts.next()) { + match ver_spec { + "MAJOR" => { + major = Some(version.parse().ok()?); + } + "MINOR" => { + minor = Some(version.parse().ok()?); + } + "REVISION" => { + revision = Some(version.parse().ok()?); + } + _ => {} } - _ => {} + } + if major.is_some() && minor.is_some() && revision.is_some() { + break; } } - if major.is_some() && minor.is_some() && revision.is_some() { + line.clear(); + } + if let (Some(major), Some(minor), Some(revision)) = (major, minor, revision) { + Some(Version::new(major, minor, revision)) + } else { + None + } + } + + pub fn find_enabled_features(header_dir: &Path) -> Option> { + let config_h = get_config_header(header_dir)?; + let mut out = Vec::with_capacity(64); + let mut line = String::with_capacity(256); + let mut reader = BufReader::new(File::open(config_h).ok()?); + while let Ok(bytes_read) = reader.read_line(&mut line) { + if bytes_read == 0 { break; } + if let Some(feature) = line.strip_prefix("#define HAVE_") { + out.push(feature.trim().to_lowercase()); + } + line.clear(); } - line.clear(); - } - if let (Some(major), Some(minor), Some(revision)) = (major, minor, revision) { - Some(Version::new(major, minor, revision)) - } else { - None + Some(out) } } @@ -264,13 +332,18 @@ fn make_modules_and_alises( Ok((modules, aliases)) } -fn emit_inherent_features(opencv_version: &Version) { +fn emit_inherent_features(opencv: &Library) { if VersionReq::parse(">=4.10") .expect("Static version requirement") - .matches(opencv_version) + .matches(&opencv.version) { println!("cargo::rustc-cfg=ocvrs_has_inherent_feature_hfloat"); } + for feature in &opencv.enabled_features { + if feature == "opencl" { + println!("cargo::rustc-cfg=ocvrs_has_inherent_feature_opencl"); + } + } } fn make_compiler(opencv: &Library, ffi_export_suffix: &str) -> cc::Build { @@ -374,9 +447,7 @@ fn main() -> Result<()> { for module in SUPPORTED_MODULES { println!("cargo::rustc-check-cfg=cfg(ocvrs_has_module_{module})"); } - // MSRV: switch to #[expect] when MSRV is 1.81 - #[allow(clippy::single_element_loop)] - for inherent_feature in ["hfloat"] { + for inherent_feature in ["hfloat", "opencl"] { println!("cargo::rustc-check-cfg=cfg(ocvrs_has_inherent_feature_{inherent_feature})"); } @@ -419,10 +490,10 @@ fn main() -> Result<()> { let opencv_header_dir = opencv .include_paths .iter() - .find(|p| get_version_header(p).is_some()) - .expect("Discovered OpenCV include paths is empty or contains non-existent paths"); + .find(|p| header::get_version_header(p).is_some()) + .expect("Discovered OpenCV include paths do not contain valid OpenCV headers"); - if let Some(header_version) = get_version_from_headers(opencv_header_dir) { + if let Some(header_version) = header::find_version(opencv_header_dir) { if header_version != opencv.version { panic!( "OpenCV version from the headers: {header_version} (at {}) must match version of the OpenCV library: {} (include paths: {:?})", @@ -442,7 +513,7 @@ fn main() -> Result<()> { ) } - let opencv_module_header_dir = get_module_header_dir(opencv_header_dir).expect("Can't find OpenCV module header dir"); + let opencv_module_header_dir = header::get_module_header_dir(opencv_header_dir).expect("Can't find OpenCV module header dir"); eprintln!( "=== Detected OpenCV module header dir at: {}", opencv_module_header_dir.display() @@ -452,7 +523,7 @@ fn main() -> Result<()> { println!("cargo::rustc-cfg=ocvrs_has_module_{module}"); } - emit_inherent_features(&opencv.version); + emit_inherent_features(&opencv); setup_rerun()?; diff --git a/build/binding-generator.rs b/build/binding-generator.rs index 74fe92a22..0e3c0c550 100644 --- a/build/binding-generator.rs +++ b/build/binding-generator.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use opencv_binding_generator::writer::RustNativeBindingWriter; use opencv_binding_generator::Generator; -use super::{get_version_from_headers, GenerateFullBindings, Result}; +use super::{header, GenerateFullBindings, Result}; /// Because clang can't be used from multiple threads we run the binding generator helper for each /// module as a separate process. Building an additional helper binary from the build script is problematic, @@ -27,7 +27,7 @@ pub fn run(mut args: impl Iterator) -> Result<()> { let out_dir = PathBuf::from(args.next().ok_or("3rd argument must be output dir")?); let module = args.next().ok_or("4th argument must be module name")?; let module = module.to_str().ok_or("Not a valid module name")?; - let version = get_version_from_headers(&opencv_header_dir) + let version = header::find_version(&opencv_header_dir) .ok_or("Can't find the version in the headers")? .to_string(); let arg_additional_include_dirs = args.next(); diff --git a/build/library.rs b/build/library.rs index 070fd1a93..e5769b676 100644 --- a/build/library.rs +++ b/build/library.rs @@ -8,7 +8,7 @@ use dunce::canonicalize; use semver::Version; use super::cmake_probe::{CmakeProbe, LinkLib, LinkSearch}; -use super::{get_version_from_headers, Result, MANIFEST_DIR, OUT_DIR, TARGET_VENDOR_APPLE}; +use super::{header, Result, MANIFEST_DIR, OUT_DIR, TARGET_VENDOR_APPLE}; struct PackageName; @@ -149,12 +149,19 @@ impl Linkage { pub struct Library { pub include_paths: Vec, pub version: Version, + pub enabled_features: Vec, pub cargo_metadata: Vec, } impl Library { fn version_from_include_paths(include_paths: impl IntoIterator>) -> Option { - include_paths.into_iter().find_map(|x| get_version_from_headers(x.as_ref())) + include_paths.into_iter().find_map(|x| header::find_version(x.as_ref())) + } + + fn enabled_features_from_include_paths(include_paths: impl IntoIterator>) -> Option> { + include_paths + .into_iter() + .find_map(|x| header::find_enabled_features(x.as_ref())) } fn process_env_var_list<'a, T: From<&'a str>>(env_list: Option>, sys_list: Vec) -> Vec { @@ -231,7 +238,10 @@ impl Library { let mut cargo_metadata = Vec::with_capacity(64); let include_paths: Vec<_> = include_paths.iter().map(PathBuf::from).collect(); - let version = Self::version_from_include_paths(&include_paths).ok_or("Could not OpenCV version from include_paths")?; + let version = + Self::version_from_include_paths(&include_paths).ok_or("Could not get OpenCV version from include_paths")?; + let enabled_features = + Self::enabled_features_from_include_paths(&include_paths).ok_or("Could not get OpenCV config from include_paths")?; cargo_metadata.extend(Self::process_link_paths(Some(link_paths), vec![])); cargo_metadata.extend(Self::process_link_libs(Some(link_libs), vec![])); @@ -239,6 +249,7 @@ impl Library { Ok(Self { include_paths, version, + enabled_features, cargo_metadata, }) } else { @@ -305,11 +316,17 @@ impl Library { )); } - let include_paths = Self::process_env_var_list(include_paths, opencv.include_paths); + let mut include_paths = Self::process_env_var_list(include_paths, opencv.include_paths); + if let Some(multiarch_include_path) = header::get_multiarch_header_dir() { + include_paths.push(multiarch_include_path); + } + let enabled_features = + Self::enabled_features_from_include_paths(&include_paths).ok_or("Could not get OpenCV config from include_paths")?; Ok(Self { include_paths, version: Version::parse(&opencv.version)?, + enabled_features, cargo_metadata, }) } @@ -358,9 +375,17 @@ impl Library { cargo_metadata.extend(Self::process_link_paths(link_paths, probe_result.link_paths)); cargo_metadata.extend(Self::process_link_libs(link_libs, probe_result.link_libs)); + let mut include_paths = Self::process_env_var_list(include_paths, probe_result.include_paths); + if let Some(multiarch_include_path) = header::get_multiarch_header_dir() { + include_paths.push(multiarch_include_path); + } + let enabled_features = + Self::enabled_features_from_include_paths(&include_paths).ok_or("Could not get OpenCV config from include_paths")?; + Ok(Self { - include_paths: Self::process_env_var_list(include_paths, probe_result.include_paths), + include_paths, version: probe_result.version.unwrap_or_else(|| Version::new(0, 0, 0)), + enabled_features, cargo_metadata, }) } @@ -400,8 +425,6 @@ impl Library { let version = Self::version_from_include_paths(&opencv_include_paths); - let include_paths = Self::process_env_var_list(include_paths, opencv_include_paths); - let mut cargo_metadata = opencv.cargo_metadata; if link_paths.as_ref().is_some_and(|lp| !lp.is_extend()) { @@ -414,9 +437,17 @@ impl Library { } cargo_metadata.extend(Self::process_link_libs(link_libs, vec![])); + let mut include_paths = Self::process_env_var_list(include_paths, opencv_include_paths); + if let Some(multiarch_include_path) = header::get_multiarch_header_dir() { + include_paths.push(multiarch_include_path); + } + let enabled_features = + Self::enabled_features_from_include_paths(&include_paths).ok_or("Could not get OpenCV config from include_paths")?; + Ok(Self { include_paths, version: version.unwrap_or_else(|| Version::new(0, 0, 0)), + enabled_features, cargo_metadata, }) } diff --git a/ci/install-macos-vcpkg.sh b/ci/install-macos-vcpkg.sh index 10f55d0f3..f5b9d01f2 100755 --- a/ci/install-macos-vcpkg.sh +++ b/ci/install-macos-vcpkg.sh @@ -28,8 +28,8 @@ export VCPKG_DEFAULT_TRIPLET=arm64-osx set +e which cmake cmake --version - if ! ./vcpkg install --clean-after-build --recurse "opencv[contrib,nonfree,ade,opencl]"; then - for log in "$VCPKG_ROOT/buildtrees"/**/*out.log; do + if ! ./vcpkg install --clean-after-build --recurse "opencv4[$VCPKG_FEATURES]"; then + for log in "$VCPKG_ROOT/buildtrees"/**/*-{out,err}.log; do echo "=== $log" cat "$log" done diff --git a/ci/install-ubuntu-vcpkg.sh b/ci/install-ubuntu-vcpkg.sh index 01e335575..bb748102a 100755 --- a/ci/install-ubuntu-vcpkg.sh +++ b/ci/install-ubuntu-vcpkg.sh @@ -60,7 +60,7 @@ export VCPKG_DEFAULT_TRIPLET=x64-linux set +e which cmake cmake --version - if ! ./vcpkg install --clean-after-build --recurse "opencv[contrib,nonfree,ade,opencl]"; then + if ! ./vcpkg install --clean-after-build --recurse "opencv4[$VCPKG_FEATURES]"; then for log in "$VCPKG_ROOT/buildtrees"/**/*-{out,err}.log; do echo "=== $log" cat "$log" diff --git a/ci/install-windows-vcpkg.sh b/ci/install-windows-vcpkg.sh index 402f87d35..9a154b178 100755 --- a/ci/install-windows-vcpkg.sh +++ b/ci/install-windows-vcpkg.sh @@ -33,8 +33,8 @@ export VCPKG_DEFAULT_TRIPLET=x64-windows set +e which cmake cmake --version - if ! ./vcpkg install --clean-after-build --recurse "opencv[contrib,nonfree,ade,opencl]"; then - for log in "$VCPKG_ROOT/buildtrees"/**/*out.log; do + if ! ./vcpkg install --clean-after-build --recurse "opencv4[$VCPKG_FEATURES]"; then + for log in "$VCPKG_ROOT/buildtrees"/**/*-{out,err}.log; do echo "=== $log" cat "$log" done diff --git a/examples/opencl.rs b/examples/opencl.rs index 3a8299069..ced02a138 100644 --- a/examples/opencl.rs +++ b/examples/opencl.rs @@ -1,19 +1,21 @@ -use std::{env, time}; +use opencv::Result; -use opencv::core::{Device, Size, UMat, Vector}; -use opencv::prelude::*; -use opencv::{core, imgcodecs, imgproc, Result}; +#[cfg(ocvrs_has_inherent_feature_opencl)] +fn main() -> Result<()> { + use std::{env, time}; -opencv::not_opencv_branch_34! { - use opencv::core::AccessFlag::ACCESS_READ; -} -opencv::opencv_branch_34! { - use opencv::core::ACCESS_READ; -} + use opencv::core::{Device, Size, UMat, Vector}; + use opencv::prelude::*; + use opencv::{core, imgcodecs, imgproc}; -const ITERATIONS: usize = 100; + opencv::not_opencv_branch_34! { + use opencv::core::AccessFlag::ACCESS_READ; + } + opencv::opencv_branch_34! { + use opencv::core::ACCESS_READ; + } -fn main() -> Result<()> { + const ITERATIONS: usize = 100; let img_file = env::args().nth(1).expect("Please supply image file name"); let opencl_have = core::have_opencl()?; if opencl_have { @@ -74,3 +76,10 @@ fn main() -> Result<()> { } Ok(()) } + +#[cfg(not(ocvrs_has_inherent_feature_opencl))] +fn main() -> Result<()> { + eprintln!("This example requires that OpenCV is build with OpenCL support:"); + eprintln!("{}", opencv::core::get_build_information()?); + Ok(()) +} diff --git a/src_cpp/core.hpp b/src_cpp/core.hpp index 3a5be24a2..ebb6f5d3e 100644 --- a/src_cpp/core.hpp +++ b/src_cpp/core.hpp @@ -1,11 +1,15 @@ #include "ocvrs_common.hpp" #include -#include -#include -#include +#ifdef HAVE_OPENCL + #include + // opengl.hpp, va_intel.hpp and directx.hpp unconditionally include ocl.hpp thus it needs to be within ifdef HAVE_OPENCL + #ifdef HAVE_OPENGL + #include + #endif + #include + #include +#endif #include -#include -#include #if (CV_VERSION_MAJOR == 3 && CV_VERSION_MINOR == 4 && CV_VERSION_REVISION >= 4) /* 3.4.4+ */ \ || (CV_VERSION_MAJOR == 4) /* 4.0+ */ \ || (CV_VERSION_MAJOR == 5) /* 5.0+ */ diff --git a/src_cpp/ocvrs_common.hpp b/src_cpp/ocvrs_common.hpp index fc47e45fe..c99f4ae87 100644 --- a/src_cpp/ocvrs_common.hpp +++ b/src_cpp/ocvrs_common.hpp @@ -1,10 +1,11 @@ #ifndef __OCVRS_COMMON_HPP__ #define __OCVRS_COMMON_HPP__ -#if defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__) \ - || defined(__CYGWIN__) || defined(__MINGW32__) || defined(__BORLANDC__) - #define OCVRS_TARGET_OS_WINDOWS -#endif +#include +#include +// defining HAVE_VA starts to rely on for VADisplay and VASurfaceID instead of OpenCV stubs, and we stop generating +// bindings for the functions that use them +#undef HAVE_VA #define CV_COLLECT_IMPL_DATA #ifdef OCVRS_PARSING_HEADERS @@ -15,9 +16,13 @@ #define OCVRS_FFI_EXPORT_SUFFIX #endif -#include #include +#if defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__) \ + || defined(__CYGWIN__) || defined(__MINGW32__) || defined(__BORLANDC__) + #define OCVRS_TARGET_OS_WINDOWS +#endif + #define OCVRS_ONLY_DEPENDENT_TYPES #define OCVRS_HANDLE(code, msg, return_name) Err(code, msg, return_name) diff --git a/tests/opencl.rs b/tests/opencl.rs index 5ead77b89..1eafa9bec 100644 --- a/tests/opencl.rs +++ b/tests/opencl.rs @@ -2,6 +2,7 @@ use opencv::{core, Result}; #[test] fn convert_type_str() -> Result<()> { + #[cfg(ocvrs_has_inherent_feature_opencl)] if core::have_opencl()? { // this function writes to buf argument and returns it let mut test = "test".to_string();