use anyhow::{bail, ensure, Result};
use itertools::Itertools;
use once_cell::sync::Lazy;
use std::{
env::{self, VarError},
fs,
path::{Path, PathBuf},
};
static OUT_DIR: Lazy<PathBuf> = Lazy::new(|| {
let path = env::var_os("OUT_DIR").expect("The OUT_DIR environment variable is not set");
PathBuf::from(path)
});
enum SrcMethod {
Cmake(PathBuf),
RawStatic(PathBuf),
PkgConfig,
PkgConfigThenStatic(PathBuf),
}
fn get_source_method() -> Result<SrcMethod> {
use SrcMethod as M;
let src = env::var_os("APRILTAG_SRC")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("apriltag-src"));
let value = match env::var("APRILTAG_SYS_METHOD") {
Ok(value) => value,
Err(VarError::NotUnicode(_)) => {
bail!("If set, APRILTAG_SYS_METHOD environment variable must be UTF-8 string.")
}
Err(VarError::NotPresent) => return Ok(M::PkgConfigThenStatic(src)), };
let method = match value.as_str() {
"pkg-config" => M::PkgConfig,
"pkg-config-then-static" => M::PkgConfigThenStatic(src),
"raw,static" => M::RawStatic(src),
"cmake,dynamic" => M::Cmake(src),
value => {
bail!(
r#"The APRILTAG_SYS_METHOD value "{value}" was not recognized. See README.md of the \
apriltag-sys crate for a description of this environment variable."#,
);
}
};
Ok(method)
}
fn main() -> Result<()> {
use SrcMethod as M;
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-env-changed=APRILTAG_SRC");
println!("cargo:rerun-if-env-changed=APRILTAG_SYS_METHOD");
#[cfg(target_os = "windows")]
{
println!("cargo:rerun-if-env-changed=APRILTAG_SYS_WINDOWS_PTHREAD_INCLUDE_DIR");
println!("cargo:rerun-if-env-changed=APRILTAG_SYS_WINDOWS_PTHREAD_STATIC_LIB");
println!("cargo:rerun-if-env-changed=APRILTAG_SYS_WINDOWS_NO_WINMM");
}
#[allow(unused_variables)]
let clang_args = {
let mut args = match get_source_method()? {
M::Cmake(src_path) => build_cmake(src_path)?,
M::RawStatic(sdk_path) => build_raw_static(sdk_path)?,
other => {
match pkg_config::probe_library("apriltag") {
Ok(_) => vec![],
Err(e) => {
if let M::PkgConfigThenStatic(sdk_path) = other {
build_raw_static(sdk_path)?
} else {
bail!("pkg-config failed: {e}");
}
}
}
}
};
args.push("-fretain-comments-from-system-headers".into());
#[cfg(target_os = "windows")]
{
let path = env::var_os("APRILTAG_SYS_WINDOWS_PTHREAD_INCLUDE_DIR").map(PathBuf::from);
if let Some(path) = path {
args.push(format!("-I{}", path.display()));
}
}
args
};
#[cfg(feature = "buildtime-bindgen")]
{
use anyhow::{self, Context};
let bindgen_builder = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate_comments(true)
.opaque_type("pthread_mutex_t")
.allowlist_type("apriltag_.*")
.allowlist_type("image_u8_.*")
.allowlist_type("image_u8x3_.*")
.allowlist_type("image_u8x4_.*")
.allowlist_type("zarray_.*")
.allowlist_type("matd_.*")
.allowlist_function("apriltag_.*")
.allowlist_function("estimate_.*")
.allowlist_function("tag16h5_.*")
.allowlist_function("tag25h9_.*")
.allowlist_function("tag36h11_.*")
.allowlist_function("tagCircle21h7_.*")
.allowlist_function("tagCircle49h12_.*")
.allowlist_function("tagCustom48h12_.*")
.allowlist_function("tagStandard41h12_.*")
.allowlist_function("tagStandard52h13_.*")
.allowlist_function("image_u8_.*")
.allowlist_function("image_u8x3_.*")
.allowlist_function("image_u8x4_.*")
.allowlist_function("zarray_.*")
.allowlist_function("matd_.*");
let bindgen_builder = bindgen_builder.clang_args(clang_args);
let bindings = bindgen_builder
.generate()
.context("Unable to generate bindings")?;
let bindings_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("bindings")
.join("bindings.rs");
fs::create_dir_all(bindings_path.parent().unwrap())?;
bindings
.write_to_file(bindings_path)
.context("Couldn't write bindings!")?;
}
#[cfg(target_os = "windows")]
{
match env::var_os("APRILTAG_SYS_WINDOWS_PTHREAD_STATIC_LIB").map(PathBuf::from) {
Some(pthread_static_lib) if pthread_static_lib.is_file() => {
if let Some(pthread_static_lib_dir) = pthread_static_lib.parent() {
println!(
"cargo:rustc-link-search={}",
pthread_static_lib_dir.display()
);
}
let lib_name = pthread_static_lib
.file_stem()
.ok_or_else(|| anyhow::anyhow!("file_stem() returns None"))?
.to_str()
.ok_or_else(|| anyhow::anyhow!("to_str() returns None"))?;
println!("cargo:rustc-link-lib={}", lib_name);
if env::var_os("APRILTAG_SYS_WINDOWS_NO_WINMM").is_none() {
println!("cargo:rustc-link-lib=winmm");
}
}
Some(pthread_static_lib) => {
bail!("cargo:warning=The given path for the static library of pthread '{}' specified by APRILTAG_SYS_WINDOWS_PTHREAD_STATIC_LIB is not a valid file", pthread_static_lib.display());
}
None => {
println!("cargo:warning=Under Microsoft Windows, apriltags' dependency 'pthread' is not available by default. Consequently, link.exe is likely to fail with LNK2019 in the next step. Consider installing a shim and specify APRILTAG_SYS_WINDOWS_PTHREAD_STATIC_LIB as path to a static library like 'pthreadVC3.lib'.");
}
}
}
Ok(())
}
fn build_cmake<P>(src_path: P) -> Result<Vec<String>>
where
P: AsRef<Path>,
{
println!("cargo:rustc-link-lib=apriltag");
let build_dir = OUT_DIR.join("cmake_build");
fs::create_dir_all(&build_dir)?;
let dst = cmake::Config::new(src_path).out_dir(build_dir).build();
let include_dir = dst.join("include");
let lib_dir = dst.join("lib");
println!("cargo:rustc-link-search=native={}", lib_dir.display());
Ok(vec![
format!("-I{}", include_dir.display()),
format!("-L{}", lib_dir.display()),
])
}
fn build_raw_static(sdk_path: PathBuf) -> Result<Vec<String>> {
let inc_dir = &sdk_path;
let mut compiler = cc::Build::new();
let src_dirs = [&sdk_path, &sdk_path.join("common")];
let count = src_dirs
.into_iter()
.map(fs::read_dir)
.flatten_ok()
.map(|entry| anyhow::Ok(entry??.path()))
.filter_ok(|path| matches!(path.extension(), Some(ext) if ext == "c"))
.filter_ok(|path| !path.ends_with("apriltag_pywrap.c"))
.try_fold(0, |count, path| -> Result<_> {
compiler.file(path?);
Ok(count + 1)
})?;
ensure!(
count > 0,
"No source files found at {}. Hint: do 'git submodule update --init'.",
sdk_path.display()
);
compiler.include(inc_dir);
compiler.extra_warnings(false);
#[cfg(target_os = "windows")]
{
match env::var_os("APRILTAG_SYS_WINDOWS_PTHREAD_INCLUDE_DIR").map(PathBuf::from) {
Some(pthread_include_dir) if pthread_include_dir.is_dir() => {
compiler.include(pthread_include_dir);
}
Some(pthread_include_dir) => {
bail!("The given include directory for pthread '{}' specified by APRILTAG_SYS_WINDOWS_PTHREAD_INCLUDE_DIR is not a valid directory in the file system.", pthread_include_dir.display());
}
None => {
println!("cargo:warning=Under Microsoft Windows, apriltags' dependency 'pthread' is not available by default. Consequently, cl.exe is likely to fail in the next step. Consider installing a shim and specify APRILTAG_SYS_WINDOWS_PTHREAD_INCLUDE_DIR accordingly.");
}
}
}
compiler.compile("apriltags");
Ok(vec![])
}