rslibcamlite 0.2.0

Rust wrapper for libcamlite - h264/rgb stream access to rpi cameras
use std::env;
use std::path::PathBuf;
use std::process::Command;

/// The rpicam-apps API libcamlite was ported to; tested with 1.13.0.
const RPICAM_APP_MIN_VERSION: [u32; 3] = [1, 9, 1];

/// Target-specific env var lookup, the same precedence the pkg-config crate
/// uses: NAME_<target>, NAME_<target_with_underscores>, NAME.
fn target_env(name: &str) -> Option<String> {
    let target = env::var("TARGET").unwrap_or_default();
    [
        format!("{name}_{target}"),
        format!("{name}_{}", target.replace('-', "_")),
        name.to_string(),
    ]
    .iter()
    .find_map(|var| {
        println!("cargo:rerun-if-env-changed={var}");
        env::var(var).ok()
    })
}

/// `pkg-config --modversion rpicam_app`. Unlike probing, this works even though
/// rpicam_app.pc's Requires.private can't be resolved.
fn rpicam_app_version() -> Option<String> {
    let mut cmd = Command::new(target_env("PKG_CONFIG").unwrap_or_else(|| "pkg-config".into()));
    for var in ["PKG_CONFIG_LIBDIR", "PKG_CONFIG_PATH", "PKG_CONFIG_SYSROOT_DIR"] {
        if let Some(value) = target_env(var) {
            cmd.env(var, value);
        }
    }
    let out = cmd.args(["--modversion", "rpicam_app"]).output().ok()?;
    out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}

fn parse_version(v: &str) -> [u32; 3] {
    let mut parts = v.split(|c: char| !c.is_ascii_digit()).filter(|p| !p.is_empty());
    let mut next = || parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
    [next(), next(), next()]
}

/// rpicam-apps changes its C++ API between releases (even patch releases)
/// without changing the library soname, so check the version explicitly.
fn check_rpicam_app_version() {
    let min = RPICAM_APP_MIN_VERSION.map(|n| n.to_string()).join(".");
    match rpicam_app_version() {
        Some(version) if parse_version(&version) < RPICAM_APP_MIN_VERSION => panic!(
            "rpicam-apps {version} is too old: libcamlite needs >= {min}. \
             Update it (sudo apt full-upgrade) or build rpicam-apps from source."
        ),
        Some(_) => {}
        None => println!(
            "cargo:warning=could not determine the rpicam-apps version (no rpicam_app.pc); \
             libcamlite needs rpicam-apps >= {min}"
        ),
    }
}

fn main() {
    check_rpicam_app_version();

    let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
    let libcamlite = manifest.join("submodules/libcamlite/src");

    // pkg-config emits the link flags for each library as it probes it.
    let probe = |name: &str| {
        pkg_config::Config::new()
            .probe(name)
            .unwrap_or_else(|err| panic!("pkg-config could not find {name}: {err}"))
    };
    let mut includes: Vec<PathBuf> = Vec::new();
    for lib in ["libcamera", "libavformat", "libavcodec", "libavutil"] {
        includes.extend(probe(lib).include_paths);
    }
    // rpicam_app.pc lists image libraries under Requires.private, which makes
    // pkg-config fail unless all their -dev packages are installed. We only need
    // the library and its headers ("core/..." under rpicam-apps/).
    match pkg_config::Config::new().probe("rpicam_app") {
        Ok(lib) => includes.extend(lib.include_paths),
        Err(_) => println!("cargo:rustc-link-lib=rpicam_app"),
    }
    let rpicam_inc: Vec<PathBuf> = includes
        .iter()
        .cloned()
        .chain(["/usr/include".into(), "/usr/local/include".into()])
        .map(|p| p.join("rpicam-apps"))
        .filter(|p| p.join("core/rpicam_app.hpp").exists())
        .take(1)
        .collect();
    assert!(!rpicam_inc.is_empty(), "rpicam-apps headers not found (install librpicam-app-dev)");
    println!("cargo:rustc-link-lib=boost_program_options");

    let sources = ["libcamlite.cpp", "yuv.cpp", "stream_info.cpp", "post_proc.cpp"];
    cxx_build::bridge("src/lib.rs")
        .file("cxx/wrap.cxx")
        .file("cxx/stream_info.c")
        .files(sources.iter().map(|s| libcamlite.join(s)))
        .includes(&includes)
        .includes(&rpicam_inc)
        .include(&libcamlite)
        .std("c++20")
        .flag_if_supported("-Wno-unused-parameter")
        .flag_if_supported("-Wno-sign-compare")
        .compile("rslibcamlite");

    println!("cargo:rerun-if-changed=cxx");
    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rerun-if-changed=submodules/libcamlite/src");
}