libicsneo-sys 0.2.0

Rust system level crate for libicsneo
Documentation
use cmake::Config;
use path_clean::{clean, PathClean};
use std::{env, path::PathBuf};

fn libicsneo_path() -> PathBuf {
    // Get the path of libicsneo
    let path = std::env::var("LIBICSNEO_PATH")
        .unwrap_or(format!("{}/src/libicsneo", env!("CARGO_MANIFEST_DIR")));
    let libicsneo_path = std::path::PathBuf::from(clean(&path));

    libicsneo_path
}

fn libicsneo_include_path() -> PathBuf {
    let path = libicsneo_path().join("include");
    path.clean()
}

fn libicsneo_header_path() -> PathBuf {
    let path = libicsneo_include_path().join("icsneo").join("icsneoc.h");
    path.clean()
}

// Detects the cargo build profile, true = release, otherwise false
fn is_release_build() -> bool {
    let profile = std::env::var("PROFILE").unwrap();
    match profile.as_str() {
        "debug" => return false,
        "release" => return true,
        _ => return false,
    }
}

// returns the cmake build string that is normally passed to -DCMAKE_BUILD_TYPE=
fn cmake_build_config_type() -> String {
    let build_config_type = if is_release_build() {
        "Release"
    } else {
        if cfg!(target_os = "windows") {
            // Rust runtime is linked with /MD on windows MSVC... MSVC takes Debug and forces /MDd
            // https://www.reddit.com/r/rust/comments/dvmzo2/cargo_external_c_library_windows_debugrelease_hell/
            "RelWithDebInfo"
        } else {
            "Debug"
        }
    };
    build_config_type.to_string()
}

// Build libicsneo through cmake, returns the build directory
fn build_libicsneo() -> PathBuf {
    let libicsneo_path = libicsneo_path();
    // Check to make sure CMakeLists.txt exists
    if !libicsneo_path.join("CMakeLists.txt").exists() {
        panic!("CMakeLists.txt not found at {}", libicsneo_path.display());
    }
    let build_config_type = cmake_build_config_type();
    // Run cmake on libicsneo
    let mut config = Config::new(libicsneo_path.clone());
    let config = config
        .build_target("ALL_BUILD")
        .define("LIBICSNEO_BUILD_ICSNEOC_STATIC:BOOL", "ON")
        .define("LIBICSNEO_BUILD_EXAMPLES:BOOL", "OFF")
        .define("LIBICSNEO_BUILD_ICSNEOLEGACY:BOOL", "OFF")
        .profile(&build_config_type);
    // Lets use ninja if it exists
    let config = match which::which("ninja") {
        Ok(_) => config.generator("Ninja Multi-Config").build_target("all"),
        Err(_e) => config,
    };
    config.build()
}

fn setup_linker_libs(build_path: &PathBuf) {
    let build_config_type = cmake_build_config_type();
    // output for lib path
    println!(
        "cargo:warning=build search path: {:?}",
        build_path
            .join(format!("build/{build_config_type}"))
            .display()
    );
    // icsneo lib/dll linker search path
    println!(
        "cargo:rustc-link-search=native={}",
        build_path
            .join(format!("build/{build_config_type}"))
            .display()
    );
    // fatfs linker search path and addition
    println!(
        "cargo:rustc-link-search=native={}/build/third-party/fatfs/{build_config_type}",
        build_path.display()
    );
    // libicsneo libraries
    println!("cargo:rustc-link-lib=fatfs");
    println!("cargo:rustc-link-lib=static=icsneocpp");
    if cfg!(feature = "static") {
        println!("cargo:rustc-link-lib=static=icsneoc-static");
    } else {
        println!("cargo:rustc-link-lib=dylib=icsneoc");
    }
    // Platform specific libraries
    match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() {
        "windows" => {
            // FTD3xx linker search path and addition
            println!(
                "cargo:rustc-link-search=native={}/build/_deps/ftdi3xx-src",
                build_path.display()
            );
            println!("cargo:rustc-link-lib=FTD3XX");
        }
        "linux" => {}
        "macos" => {
            println!("cargo:rustc-link-lib=static=icsneoc-static");
            println!("cargo:rustc-link-lib=framework=IOKit");
            println!("cargo:rustc-link-lib=framework=CoreFoundation");
        }
        target_os => panic!("Target OS not supported: {target_os}"),
    }
}

fn prepare_git_submodule() {
    // We don't need to checkout the submodule if we are using a custom libicsneo path
    if std::env::var("LIBICSNEO_PATH").is_ok() {
        println!("cargo:warning=Using custom LIBICSNEO_PATH, skipping checking out submodules");
        return;
    }
    let libicsneo_path = libicsneo_path();
    // This seems to not be needed when including this as a dependency? Why?
    // checkout the submodule if needed
    let output = std::process::Command::new("git")
        .args(["submodule", "update", "--init"])
        .current_dir(libicsneo_path)
        .output()
        .expect("Failed to fetch git submodules!");
    // Make sure git was successful!
    if !output.status.success() {
        println!("cargo:warning=git return code: {}", output.status);
        let stdout = std::str::from_utf8(&output.stdout).unwrap();
        for line in stdout.split("\n") {
            println!("cargo:warning=git stdout: {}", line);
        }
        let stderr = std::str::from_utf8(&output.stderr).unwrap();
        for line in stderr.split("\n") {
            println!("cargo:warning=git stderr: {}", line);
        }
    }
}

fn generate_bindings() {
    let header = libicsneo_header_path();
    let bindings = bindgen::Builder::default()
        .header(header.to_str().unwrap())
        .default_enum_style(bindgen::EnumVariation::Rust {
            non_exhaustive: false,
        })
        .clang_args(&[format!("-I{}", libicsneo_include_path().display()).as_str()])
        .allowlist_function("icsneo_.*")
        .allowlist_type("neodevice_t")
        .allowlist_type("neonetid_t")
        .allowlist_type("neomessage_.*")
        .allowlist_type("neoversion_t")
        .allowlist_type("neoevent_t")
        .formatter(bindgen::Formatter::Rustfmt)
        .derive_default(true)
        .derive_debug(true)
        .derive_partialeq(true)
        .derive_copy(true)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        //.clang_args(clang_args())
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    println!("cargo:warning=out_path: {:?}", out_path.display());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings");

    let out_path = std::path::PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

fn main() {
    let header = libicsneo_header_path();
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed={}", header.to_str().unwrap());
    println!("cargo:rerun-if-env-changed=LIBMSVC_PATH");

    prepare_git_submodule();
    generate_bindings();
    // We can skip building if its for docs.rs
    if std::env::var("DOCS_RS").is_err() {
        let build_directory = build_libicsneo();
        setup_linker_libs(&build_directory);
    }
}