flowbrigade 0.1.4

Rust bindings for FlowBrigade's C ABI.
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    let lib_dir = match env::var("FLOWBRIGADE_LIB_DIR") {
        Ok(value) => PathBuf::from(value),
        Err(_) => build_from_flowbrigade_source(),
    };

    println!("cargo:rustc-link-search=native={}", lib_dir.display());
    println!("cargo:rustc-link-lib=dylib=flowbrigade");

    if cfg!(any(target_os = "linux", target_os = "macos")) {
        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display());
    }

    println!("cargo:rerun-if-env-changed=FLOWBRIGADE_LIB_DIR");
    println!("cargo:rerun-if-env-changed=FLOWBRIGADE_SOURCE_DIR");
}

fn build_from_flowbrigade_source() -> PathBuf {
    let source_dir = env::var("FLOWBRIGADE_SOURCE_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| nimble_flowbrigade_path());

    let (source_file, import_path) = flowbrigade_source_layout(&source_dir);

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by Cargo"));
    let native_dir = out_dir.join("flowbrigade-native");
    std::fs::create_dir_all(&native_dir).expect("failed to create native build output directory");

    let lib_path = native_dir.join(platform_library_name());
    let status = Command::new("nim")
        .arg("c")
        .arg("--mm:arc")
        .arg("--app:lib")
        .arg(format!("-p:{}", import_path.display()))
        .arg(format!("--out:{}", lib_path.display()))
        .arg(&source_file)
        .status()
        .unwrap_or_else(|error| {
            panic!(
                "failed to run `nim`: {error}\n\
                 Install Nim and Nimble first, then run `nimble refresh` and \
                 `nimble install flowbrigade`.\n\
                 See https://nim-lang.org/install.html"
            )
        });

    if !status.success() {
        panic!(
            "failed to build FlowBrigade native library from '{}'.\n\
             Make sure Nim is installed and FlowBrigade is available via \
             `nimble refresh` and `nimble install flowbrigade`, or set \
             FLOWBRIGADE_LIB_DIR to an existing native library directory.",
            source_dir.display()
        );
    }

    native_dir
}

fn flowbrigade_source_layout(source_dir: &Path) -> (PathBuf, PathBuf) {
    let checkout_source = source_dir.join("src").join("flowbrigade_c.nim");
    if checkout_source.is_file() {
        return (checkout_source, source_dir.join("src"));
    }

    let nimble_source = source_dir.join("flowbrigade_c.nim");
    if nimble_source.is_file() {
        return (nimble_source, source_dir.to_path_buf());
    }

    panic!(
        "FlowBrigade source was found at '{}', but no C ABI source file was found.\n\
         Expected either '{}' for a source checkout or '{}' for a Nimble package.\n\
         Install FlowBrigade with `nimble refresh` and `nimble install flowbrigade`, \
         or set FLOWBRIGADE_SOURCE_DIR to a local FlowBrigade checkout.",
        source_dir.display(),
        checkout_source.display(),
        nimble_source.display()
    );
}

fn nimble_flowbrigade_path() -> PathBuf {
    let output = Command::new("nimble")
        .arg("path")
        .arg("flowbrigade")
        .output()
        .unwrap_or_else(|error| {
            panic!(
                "failed to run `nimble path flowbrigade`: {error}\n\
                 Install Nim/Nimble and then run `nimble refresh` and \
                 `nimble install flowbrigade`, or set FLOWBRIGADE_SOURCE_DIR \
                 to a local FlowBrigade checkout."
            )
        });

    if !output.status.success() {
        panic!(
            "`nimble path flowbrigade` could not find FlowBrigade.\n\
             Install it with `nimble refresh` and `nimble install flowbrigade`, or set \
             FLOWBRIGADE_SOURCE_DIR to a local FlowBrigade checkout.\n\
             stderr:\n{}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let path = stdout
        .lines()
        .map(str::trim)
        .find(|line| !line.is_empty())
        .expect("`nimble path flowbrigade` returned no path");

    Path::new(path).to_path_buf()
}

fn platform_library_name() -> &'static str {
    if cfg!(target_os = "windows") {
        "flowbrigade.dll"
    } else if cfg!(target_os = "macos") {
        "libflowbrigade.dylib"
    } else {
        "libflowbrigade.so"
    }
}