libindigo 0.3.2+INDIGO.2.0.300

Core API for developing INDIGO astronomy clients and devices.
use core::str;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use std::{env, io};

use once_cell::sync::Lazy;
use regex::Regex;

// TODO source INDIGO version and build information from INDIGO source code
// NOTE create aux crate for build related functionality shared with the sys crate

/// used for cloning the INDIGO git repository when source is retrieved from a crate
const INDIGO_GIT_REPOSITORY: &str = "https://github.com/indigo-astronomy/indigo";

/// used for building INDIGO from source when checked out
const INDIGO_GIT_SUBMODULE: &str = "sys/externals/indigo";

// used to detect Linux system libraries
// const LINUX_INDIGO_VERSION_HEADER: &str = "/usr/include/indigo/indigo_version.h";
// const LINUX_INCLUDE: &str = "/usr/include";
// const LINUX_LIB: &str = "/usr/lib";

// Regex to extract INDIGO constant definitions from headers
// Matches: #define CONSTANT_NAME "value"
// Example: #define INFO_PROPERTY_NAME "INFO"
static RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"^#define\s+(?<name>\w+)_NAME\s+"(?<value>.+)"\s*$"#).unwrap());
fn main() -> std::io::Result<()> {
    let submodule = Path::new(INDIGO_GIT_SUBMODULE);

    // Always try to extract constants if the submodule exists
    if submodule.exists() {
        if let Err(e) = extract_indigo_constants(&submodule) {
            eprintln!("Warning: Could not extract INDIGO constants: {}", e);
            eprintln!("Continuing with existing constants.rs");
        }
    } else {
        eprintln!("INDIGO submodule not found at {}", INDIGO_GIT_SUBMODULE);
        eprintln!("Run: git submodule update --init --recursive");
        eprintln!("Continuing with existing constants.rs");
    }

    // Only generate FFI bindings when building with FFI features
    let has_ffi = env::var("CARGO_FEATURE_FFI_STRATEGY").is_ok()
        || env::var("CARGO_FEATURE_SYS").is_ok()
        || env::var("CARGO_FEATURE_AUTO").is_ok();

    if !has_ffi {
        // For pure Rust builds, just create empty output files
        let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
        let names_path = out_dir.join("name.rs");
        let interface_path = out_dir.join("interface.rs");
        std::fs::write(names_path, "// No INDIGO names for pure Rust build\n")?;
        std::fs::write(
            interface_path,
            "// No INDIGO interface for pure Rust build\n",
        )?;
        return Ok(());
    }

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    if !submodule.exists() {
        init_indigo_submodule()?;
    }

    // Generate FFI bindings
    generate_ffi_bindings(&submodule, &out_dir)?;

    Ok(())
}

/// Extract INDIGO property and item name constants from headers.
///
/// This function parses the INDIGO C headers to extract all standard property
/// and item name definitions, generating Rust constants in `src/constants.rs`.
///
/// The constants are extracted from `indigo_names.h` which contains definitions like:
/// ```c
/// #define CONNECTION_PROPERTY_NAME "CONNECTION"
/// #define INFO_PROPERTY_NAME "INFO"
/// ```
///
/// These are converted to Rust constants without the `_NAME` suffix:
/// ```rust
/// pub const CONNECTION_PROPERTY: &str = "CONNECTION";
/// pub const INFO_PROPERTY: &str = "INFO";
/// ```
fn extract_indigo_constants(submodule: &Path) -> std::io::Result<()> {
    let mut names = BTreeMap::new();

    let include = submodule
        .join("indigo_libs/indigo/indigo_names.h")
        .canonicalize()?;

    // Parse the header file and extract constant definitions
    for line in read_lines(include)? {
        if let Some(cap) = RE.captures(&line?) {
            // Remove the _NAME suffix from the constant name
            let name = cap["name"].to_string();
            let value = cap["value"].to_string();
            names.insert(name, value);
        }
    }

    // Generate src/constants.rs with all extracted constants
    let constants_path = Path::new("src/constants.rs");
    let mut constants_file = File::create(constants_path)?;

    writeln!(
        constants_file,
        "// This file is automatically generated by build.rs"
    )?;
    writeln!(constants_file, "// DO NOT EDIT MANUALLY")?;
    writeln!(constants_file, "//")?;
    writeln!(constants_file, "// To regenerate this file:")?;
    writeln!(
        constants_file,
        "//   1. Update the INDIGO submodule: git submodule update --remote sys/externals/indigo"
    )?;
    writeln!(constants_file, "//   2. Run: cargo clean && cargo build")?;
    writeln!(constants_file, "//")?;
    writeln!(
        constants_file,
        "// Source: sys/externals/indigo/indigo_libs/indigo/indigo_names.h"
    )?;
    writeln!(constants_file)?;

    for (name, value) in names {
        writeln!(constants_file, r#"pub const {}: &str = "{}";"#, name, value)?;
    }

    println!("cargo:rerun-if-changed=sys/externals/indigo/indigo_libs/indigo/indigo_names.h");

    Ok(())
}

/// Generate FFI bindings for INDIGO device interfaces.
fn generate_ffi_bindings(submodule: &Path, out_dir: &Path) -> std::io::Result<()> {
    let include = submodule.join("indigo_libs").canonicalize()?;
    let bindings = bindgen::Builder::default()
        .clang_arg(format!("-I{}", include.to_str().expect("path not found")))
        .header(join_paths(&include, "indigo/indigo_bus.h"))
        .derive_debug(true)
        .allowlist_item("indigo_device_interface")
        .bitfield_enum("indigo_device_interface")
        .prepend_enum_name(false)
        .translate_enum_integer_types(true)
        .generate_cstr(true)
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    bindings
        .write_to_file(out_dir.join("interface.rs"))
        .expect("Couldn't write bindings!");

    Ok(())
}

fn join_paths(base: &Path, path: &str) -> String {
    let p = base.join(path);
    let s = p.to_str().expect("path not found");
    String::from(s)
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

fn init_indigo_submodule() -> std::io::Result<PathBuf> {
    // check if we are in a crate package or if this a git repository
    let outcome = if PathBuf::from(".git").exists() {
        std::process::Command::new("git")
            .arg("submodule")
            .arg("update")
            .arg("--init")
            .arg("--recursive")
            .status()
            .expect("could not spawn `git`")
    } else {
        std::process::Command::new("git")
            .arg("clone")
            .arg(INDIGO_GIT_REPOSITORY)
            .arg("externals/indigo")
            .status()
            .expect("could not spawn `git`")
    };

    if !outcome.success() {
        panic!("could not clone or checkout git submodule externals/indigo");
    }
    Path::new(INDIGO_GIT_SUBMODULE).canonicalize()
}