Skip to content

Commit

Permalink
Include additional core headers conditionally
Browse files Browse the repository at this point in the history
  • Loading branch information
twistedfall committed Jan 24, 2025
1 parent 60b1abc commit d01d34e
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 215 deletions.
12 changes: 11 additions & 1 deletion .github/workflows/opencv-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,19 @@ jobs:
- macos-14
vcpkg-version:
- 2025.01.13 # https://github.com/microsoft/vcpkg/releases
vcpkg-features-cache-key:
- full
vcpkg-features:
- contrib,nonfree,ade,opencl
include:
- os-image: windows-2022
vcpkg-version: 2025.01.13
vcpkg-features-cache-key: min
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:
Expand All @@ -80,7 +90,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-cache-key }}

- name: Install dependencies
run: ci/install.sh
Expand Down
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
90 changes: 20 additions & 70 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
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;

use binding_generator::handle_running_binding_generator;
use docs::handle_running_in_docsrs;
use generator::BindingGenerator;
use header::IncludePath;
use library::Library;
use once_cell::sync::Lazy;
use semver::{Version, VersionReq};
Expand All @@ -21,6 +20,8 @@ pub mod cmake_probe;
mod docs;
#[path = "build/generator.rs"]
mod generator;
#[path = "build/header.rs"]
mod header;
#[path = "build/library.rs"]
pub mod library;

Expand Down Expand Up @@ -144,6 +145,8 @@ static SUPPORTED_MODULES: [&str; 73] = [
"xstereo",
];

static SUPPORTED_INHERENT_FEATURES: [&str; 2] = ["hfloat", "opencl"];

/// The contents of these vars will be present in the debug log, but will not cause the source rebuild
static DEBUG_ENV_VARS: [&str; 1] = ["PATH"];

Expand All @@ -169,64 +172,6 @@ fn files_with_extension<'e>(dir: &Path, extension: impl AsRef<OsStr> + 'e) -> Re
})
}

fn get_module_header_dir(header_dir: &Path) -> Option<PathBuf> {
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);
}
None
}

fn get_version_header(header_dir: &Path) -> Option<PathBuf> {
get_module_header_dir(header_dir)
.map(|dir| dir.join("core/version.hpp"))
.filter(|hdr| hdr.is_file())
}

fn get_version_from_headers(header_dir: &Path) -> Option<Version> {
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;
}
}
line.clear();
}
if let (Some(major), Some(minor), Some(revision)) = (major, minor, revision) {
Some(Version::new(major, minor, revision))
} else {
None
}
}

fn make_modules_and_alises(
opencv_dir: &Path,
opencv_version: &Version,
Expand Down Expand Up @@ -264,13 +209,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 SUPPORTED_INHERENT_FEATURES.contains(&feature.as_str()) {
println!("cargo::rustc-cfg=ocvrs_has_inherent_feature_{feature}");
}
}
}

fn make_compiler(opencv: &Library, ffi_export_suffix: &str) -> cc::Build {
Expand Down Expand Up @@ -374,9 +324,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 SUPPORTED_INHERENT_FEATURES {
println!("cargo::rustc-check-cfg=cfg(ocvrs_has_inherent_feature_{inherent_feature})");
}

Expand Down Expand Up @@ -419,10 +367,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| p.get_version_header().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) = opencv_header_dir.find_version() {
if header_version != opencv.version {
panic!(
"OpenCV version from the headers: {header_version} (at {}) must match version of the OpenCV library: {} (include paths: {:?})",
Expand All @@ -442,7 +390,9 @@ 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 = opencv_header_dir
.get_module_header_dir()
.expect("Can't find OpenCV module header dir");
eprintln!(
"=== Detected OpenCV module header dir at: {}",
opencv_module_header_dir.display()
Expand All @@ -452,7 +402,7 @@ fn main() -> Result<()> {
println!("cargo::rustc-cfg=ocvrs_has_module_{module}");
}

emit_inherent_features(&opencv.version);
emit_inherent_features(&opencv);

setup_rerun()?;

Expand Down
6 changes: 4 additions & 2 deletions build/binding-generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ 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::{GenerateFullBindings, Result};
use crate::header::IncludePath;

/// 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,
Expand All @@ -27,7 +28,8 @@ pub fn run(mut args: impl Iterator<Item = OsString>) -> 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 = opencv_header_dir
.find_version()
.ok_or("Can't find the version in the headers")?
.to_string();
let arg_additional_include_dirs = args.next();
Expand Down
139 changes: 139 additions & 0 deletions build/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::Command;

use semver::Version;

pub trait IncludePath {
fn get_module_header_dir(&self) -> Option<PathBuf>;
fn get_version_header(&self) -> Option<PathBuf>;
fn get_config_header(&self) -> Option<PathBuf>;
fn find_version(&self) -> Option<Version>;
fn find_enabled_features(&self) -> Option<Vec<String>>;
}

impl IncludePath for Path {
fn get_module_header_dir(&self) -> Option<PathBuf> {
let mut out = self.join("opencv2.framework/Headers");
if out.is_dir() {
return Some(out);
}
out = self.join("opencv2");
if out.is_dir() {
return Some(out);
}
None
}

fn get_version_header(&self) -> Option<PathBuf> {
self
.get_module_header_dir()
.map(|dir| dir.join("core/version.hpp"))
.filter(|hdr| hdr.is_file())
}

fn get_config_header(&self) -> Option<PathBuf> {
self
.get_module_header_dir()
.map(|dir| dir.join("cvconfig.h"))
.filter(|hdr| hdr.is_file())
}

fn find_version(&self) -> Option<Version> {
let version_hpp = self.get_version_header()?;
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;
}
}
line.clear();
}
if let (Some(major), Some(minor), Some(revision)) = (major, minor, revision) {
Some(Version::new(major, minor, revision))
} else {
None
}
}

fn find_enabled_features(&self) -> Option<Vec<String>> {
let config_h = self.get_config_header()?;
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();
}
Some(out)
}
}

/// Something like `/usr/include/x86_64-linux-gnu/opencv4/` on newer Debian-derived distros
///
/// https://wiki.debian.org/Multiarch/Implementation
pub fn get_multiarch_header_dir() -> Option<PathBuf> {
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()],
);

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);
}
}
None
}
Loading

0 comments on commit d01d34e

Please sign in to comment.