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 23, 2025
1 parent c43f377 commit c9944df
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 88 deletions.
185 changes: 128 additions & 57 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -169,61 +167,131 @@ 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);
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<PathBuf> {
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<PathBuf> {
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<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()],
);

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;
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<PathBuf> {
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<PathBuf> {
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<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;
}
}
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<Vec<String>> {
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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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})");
}

Expand Down Expand Up @@ -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() && header::get_config_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: {:?})",
Expand All @@ -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()
Expand All @@ -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()?;

Expand Down
4 changes: 2 additions & 2 deletions build/binding-generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,7 +27,7 @@ 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 = 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();
Expand Down
45 changes: 38 additions & 7 deletions build/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -149,12 +149,19 @@ impl Linkage {
pub struct Library {
pub include_paths: Vec<PathBuf>,
pub version: Version,
pub enabled_features: Vec<String>,
pub cargo_metadata: Vec<String>,
}

impl Library {
fn version_from_include_paths(include_paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Option<Version> {
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<Item = impl AsRef<Path>>) -> Option<Vec<String>> {
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<EnvList<'a>>, sys_list: Vec<T>) -> Vec<T> {
Expand Down Expand Up @@ -231,14 +238,18 @@ 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![]));

Ok(Self {
include_paths,
version,
enabled_features,
cargo_metadata,
})
} else {
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -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()) {
Expand All @@ -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,
})
}
Expand Down
Loading

0 comments on commit c9944df

Please sign in to comment.