zyxdb-sys 0.1.28

Raw Driver ABI bindings for the ZYX embedded graph database
use std::{
    env,
    path::{Path, PathBuf},
};

#[cfg(feature = "bundled")]
use std::{fs, process::Command};

fn main() {
    println!("cargo:rerun-if-env-changed=ZYX_LIB_DIR");
    println!("cargo:rerun-if-env-changed=ZYXDB_SYS_USE_CONAN");
    println!("cargo:rerun-if-env-changed=CMAKE_TOOLCHAIN_FILE");
    println!("cargo:rerun-if-env-changed=CMAKE_GENERATOR");
    println!("cargo:rerun-if-env-changed=CMAKE_BUILD_TYPE");

    let bundled = env::var_os("CARGO_FEATURE_BUNDLED").is_some();
    let system = env::var_os("CARGO_FEATURE_SYSTEM").is_some();

    match (bundled, system) {
        (true, false) => build_bundled(),
        (false, true) => link_system(),
        (true, true) => panic!("features `bundled` and `system` are mutually exclusive"),
        (false, false) => panic!("enable either the `bundled` or `system` feature"),
    }
}

#[cfg(feature = "bundled")]
fn build_bundled() {
    let source_dir = zyx_src::source_dir();
    emit_source_reruns(&source_dir);

    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
    let generator = cmake_generator();
    let build_dir = out_dir.join(if generator.is_some() {
        "zyx-build-ninja"
    } else {
        "zyx-build"
    });
    let install_dir = out_dir.join("zyx-install");
    let profile = cmake_profile();

    fs::create_dir_all(&build_dir).expect("failed to create bundled ZYX build directory");
    fs::create_dir_all(&install_dir).expect("failed to create bundled ZYX install directory");

    let mut configure = Command::new("cmake");
    if let Some(generator) = generator {
        configure.arg("-G").arg(generator);
    }
    configure
        .arg("-S")
        .arg(&source_dir)
        .arg("-B")
        .arg(&build_dir)
        .arg(format!("-DCMAKE_BUILD_TYPE={profile}"))
        .arg(format!("-DCMAKE_INSTALL_PREFIX={}", install_dir.display()))
        .arg("-DZYX_BUILD_TESTS=OFF")
        .arg("-DZYX_BUILD_APPS=OFF")
        .arg("-DZYX_BUILD_PYTHON=OFF")
        .arg("-DZYX_BUILD_SHARED_LIBRARY=ON")
        .arg("-DZYX_BUILD_STATIC_LIBRARY=OFF")
        .arg("-DZYX_INSTALL_PKGCONFIG=OFF")
        .arg("-DBUILD_SHARED_LIBS=ON")
        .arg("-DCMAKE_POSITION_INDEPENDENT_CODE=ON");

    if let Some(toolchain) = bundled_toolchain_file(&source_dir, &build_dir, profile) {
        configure.arg(format!("-DCMAKE_TOOLCHAIN_FILE={}", toolchain.display()));
    }

    run(&mut configure, "configure bundled ZYX native library");

    let mut build = Command::new("cmake");
    build
        .arg("--build")
        .arg(&build_dir)
        .arg("--target")
        .arg("install")
        .arg("--config")
        .arg(profile);
    run(&mut build, "build bundled ZYX native library");

    link_bundled_install(&install_dir);
}

#[cfg(feature = "bundled")]
#[cfg(feature = "bundled")]
fn cmake_generator() -> Option<&'static str> {
    if env::var_os("CMAKE_GENERATOR").is_some() {
        return None;
    }

    Command::new("ninja")
        .arg("--version")
        .output()
        .ok()
        .filter(|output| output.status.success())
        .map(|_| "Ninja")
}

#[cfg(feature = "bundled")]
fn bundled_toolchain_file(source_dir: &Path, build_dir: &Path, profile: &str) -> Option<PathBuf> {
    if let Some(path) = env::var_os("CMAKE_TOOLCHAIN_FILE") {
        return Some(PathBuf::from(path));
    }

    if env_flag("ZYXDB_SYS_USE_CONAN") {
        let conan_dir = build_dir.join("conan");
        fs::create_dir_all(&conan_dir).expect("failed to create Conan output directory");

        let mut install = Command::new("conan");
        install
            .arg("install")
            .arg(source_dir)
            .arg(format!("--output-folder={}", conan_dir.display()))
            .arg("--build=missing")
            .arg("--build=antlr4-cppruntime/*")
            .arg("-s")
            .arg(format!("build_type={profile}"))
            .arg("-s")
            .arg("compiler.cppstd=20")
            .arg("-o")
            .arg("with_tests=False");
        run(&mut install, "install bundled ZYX dependencies with Conan");

        return Some(conan_dir.join("conan_toolchain.cmake"));
    }

    None
}

#[cfg(feature = "bundled")]
fn link_bundled_install(install_dir: &Path) {
    let lib_dir = install_dir.join("lib");
    let lib64_dir = install_dir.join("lib64");
    let bin_dir = install_dir.join("bin");

    for dir in [&lib_dir, &lib64_dir, &bin_dir] {
        if dir.exists() {
            println!("cargo:rustc-link-search=native={}", dir.display());
        }
    }
    if lib_dir.exists() {
        println!("cargo:LIB_DIR={}", lib_dir.display());
    } else if lib64_dir.exists() {
        println!("cargo:LIB_DIR={}", lib64_dir.display());
    }
    if bin_dir.exists() {
        println!("cargo:BIN_DIR={}", bin_dir.display());
    }

    if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
        for dir in [&lib_dir, &lib64_dir] {
            if dir.exists() {
                println!("cargo:rustc-link-arg=-Wl,-rpath,{}", dir.display());
            }
        }
    }

    if cfg!(target_os = "windows") {
        copy_windows_runtime_dll(&bin_dir);
    }

    println!("cargo:rustc-link-lib=dylib=zyx");
}

#[cfg(feature = "bundled")]
fn copy_windows_runtime_dll(bin_dir: &Path) {
    let dll = bin_dir.join("zyx.dll");
    if !dll.exists() {
        return;
    }

    for dir in cargo_target_runtime_dirs() {
        if fs::create_dir_all(&dir).is_ok() {
            let _ = fs::copy(&dll, dir.join("zyx.dll"));
        }
    }
}

#[cfg(feature = "bundled")]
fn cargo_target_runtime_dirs() -> Vec<PathBuf> {
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
    let Some(profile_dir) = out_dir.ancestors().nth(3) else {
        return Vec::new();
    };
    vec![profile_dir.to_path_buf(), profile_dir.join("deps")]
}

fn link_system() {
    let lib_dir = env::var_os("ZYX_LIB_DIR")
        .map(PathBuf::from)
        .or_else(discover_repo_lib_dir);
    if let Some(lib_dir) = lib_dir {
        println!("cargo:rustc-link-search=native={}", lib_dir.display());
        emit_runtime_path(&lib_dir);
    }

    println!("cargo:rustc-link-lib=dylib=zyx");
}

#[cfg(feature = "bundled")]
fn cmake_profile() -> &'static str {
    env::var("CMAKE_BUILD_TYPE")
        .ok()
        .filter(|value| !value.is_empty())
        .map(|value| {
            if value.eq_ignore_ascii_case("debug") {
                "Debug"
            } else {
                "Release"
            }
        })
        .unwrap_or_else(|| match env::var("PROFILE").as_deref() {
            Ok("debug") => "Debug",
            _ => "Release",
        })
}

#[cfg(feature = "bundled")]
fn env_flag(name: &str) -> bool {
    env::var(name)
        .map(|value| {
            matches!(
                value.as_str(),
                "1" | "true" | "TRUE" | "on" | "ON" | "yes" | "YES"
            )
        })
        .unwrap_or(false)
}

#[cfg(feature = "bundled")]
fn emit_source_reruns(source_dir: &Path) {
    for relative in [
        "CMakeLists.txt",
        "cmake",
        "include",
        "src",
        "lib/inputxx",
        "conanfile.py",
    ] {
        let path = source_dir.join(relative);
        if path.is_dir() {
            emit_dir_reruns(&path);
        } else if path.exists() {
            println!("cargo:rerun-if-changed={}", path.display());
        }
    }
}

#[cfg(feature = "bundled")]
fn emit_dir_reruns(dir: &Path) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            emit_dir_reruns(&path);
        } else {
            println!("cargo:rerun-if-changed={}", path.display());
        }
    }
}

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

#[cfg(not(feature = "bundled"))]
fn build_bundled() {
    panic!("the `bundled` feature is not enabled");
}

fn discover_repo_lib_dir() -> Option<PathBuf> {
    let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR")?);
    let repo_root = manifest_dir.ancestors().nth(3)?;

    [
        "buildDir",
        "build",
        "cmake-build-debug",
        "cmake-build-release",
        "out/build",
    ]
    .into_iter()
    .map(|dir| repo_root.join(dir))
    .find(|dir| has_zyx_library(dir))
}

fn has_zyx_library(dir: &Path) -> bool {
    [
        "libzyx.dylib",
        "libzyx.so",
        "zyx.dll",
        "zyx.lib",
        "Debug/zyx.dll",
        "Debug/zyx.lib",
        "Release/zyx.dll",
        "Release/zyx.lib",
    ]
    .into_iter()
    .any(|name| dir.join(name).is_file())
}

#[cfg(feature = "bundled")]
fn run(command: &mut Command, action: &str) {
    let status = command
        .status()
        .unwrap_or_else(|error| panic!("failed to {action}: {error}"));
    if !status.success() {
        panic!("failed to {action}: command exited with {status}");
    }
}