ogdf-sys 0.3.2

Unsafe Rust bindings to the OGDF graph drawing library built with autocxx.
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use autocxx_build::Builder as AutocxxBuilder;
use syntheon as syn;

const OGDF_TAG: &str = "foxglove-202510";
const PERF_FLAGS: &[&str] = &["-O3", "-DNDEBUG", "-g0", "-fomit-frame-pointer"];
const NATIVE_CPU_FLAGS: &[&str] = &["-march=native", "-mtune=native"];

struct OgdfLayout {
    _cache_lock: syn::LockedCacheDir,
    archive_path: PathBuf,
    source_dir: PathBuf,
    build_dir: PathBuf,
    install_dir: PathBuf,
}

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=CARGO_ENCODED_RUSTFLAGS");

    let ogdf_layout = ogdf_layout();
    println!(
        "cargo:rerun-if-changed={}",
        ogdf_layout.archive_path.display()
    );
    let install_dir = ensure_ogdf(&ogdf_layout);

    let cpp_include = PathBuf::from("cpp/include");
    let ogdf_include = install_dir.join("include");
    let ogdf_release_include = install_dir.join("include/ogdf-release");

    let mut build = AutocxxBuilder::new(
        "src/lib.rs",
        [
            cpp_include.as_path(),
            ogdf_include.as_path(),
            ogdf_release_include.as_path(),
        ],
    )
    .extra_clang_args(
        &clang_args()
            .iter()
            .map(String::as_str)
            .collect::<Vec<&str>>(),
    )
    .build()
    .expect("autocxx code generation failed");

    build
        .file("cpp/src/spqr.cpp")
        .file("cpp/src/mps.cpp")
        .flag("-std=c++17")
        .include(&cpp_include)
        .include(&ogdf_include)
        .include(&ogdf_release_include);

    apply_perf_flags(&mut build);
    build.compile("graphum_ffi");

    for lib_dir in lib_dirs(&install_dir) {
        println!("cargo:rustc-link-search=native={}", lib_dir.display());
    }
    println!("cargo:rustc-link-lib=static=OGDF");
    println!("cargo:rustc-link-lib=static=COIN");
    if env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("unix") {
        println!("cargo:rustc-link-lib=pthread");
    }

    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rerun-if-changed=cpp/include/types.hpp");
    println!("cargo:rerun-if-changed=cpp/include/mps.hpp");
    println!("cargo:rerun-if-changed=cpp/include/spqr.hpp");
    println!("cargo:rerun-if-changed=cpp/src/mps.cpp");
    println!("cargo:rerun-if-changed=cpp/src/spqr.cpp");
}

fn clang_args() -> Vec<String> {
    let mut args = vec!["-std=c++17".to_string()];
    for dir in syn::clang_system_include_dirs() {
        args.push("-isystem".to_string());
        args.push(dir.display().to_string());
    }
    args
}

fn ensure_ogdf_source(layout: &OgdfLayout) -> PathBuf {
    if layout.source_dir.join("CMakeLists.txt").exists() {
        return layout.source_dir.clone();
    }

    if let Some(parent) = layout.source_dir.parent() {
        fs::create_dir_all(parent).expect("failed to create ogdf source parent directory");
    }
    let root = layout
        .source_dir
        .parent()
        .unwrap_or_else(|| panic!("missing parent for {}", layout.source_dir.display()));
    syn::extract_tar_gz(&layout.archive_path, root);
    if layout.source_dir.join("CMakeLists.txt").exists() {
        return layout.source_dir.clone();
    }
    panic!(
        "OGDF source tree not found under {} after extraction",
        layout.source_dir.display()
    );
}

fn ensure_ogdf(layout: &OgdfLayout) -> PathBuf {
    if !available_lib_dirs(&layout.install_dir).is_empty() {
        return layout.install_dir.clone();
    }
    ensure_ogdf_source(layout);
    build_ogdf(layout)
}

fn build_ogdf(layout: &OgdfLayout) -> PathBuf {
    if !available_lib_dirs(&layout.install_dir).is_empty() {
        return layout.install_dir.clone();
    }

    let cmake = syn::CmakeRunner::new(&layout.build_dir);
    let jobs = cmake.jobs();
    let capabilities = cmake.capabilities();
    if jobs > 1 && !capabilities.build_parallel {
        println!(
            "cargo:warning=cmake --build --parallel requires cmake 3.12+; building with a single job"
        );
    }
    if jobs > 1 && !capabilities.install_parallel {
        println!(
            "cargo:warning=cmake --install --parallel requires cmake 3.31+; installing with a single job"
        );
    }

    fs::create_dir_all(&layout.build_dir).expect("failed to create ogdf build directory");
    let perf_flags = perf_flag_string();
    let mut configure = Command::new("cmake");
    configure
        .arg("-S")
        .arg(&layout.source_dir)
        .arg("-B")
        .arg(&layout.build_dir)
        .arg("-DCMAKE_BUILD_TYPE=Release")
        .arg(format!("-DCMAKE_CXX_FLAGS={perf_flags}"))
        .arg(format!("-DCMAKE_C_FLAGS={perf_flags}"))
        .arg(format!("-DCMAKE_CXX_FLAGS_RELEASE={perf_flags}"))
        .arg(format!("-DCMAKE_C_FLAGS_RELEASE={perf_flags}"))
        .arg("-DBUILD_SHARED_LIBS=OFF")
        .arg("-DOGDF_WARNING_ERRORS=OFF")
        .arg("-DOGDF_INCLUDE_CGAL=OFF")
        .arg("-DOGDF_DEBUG_MODE=NONE")
        .arg("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
        .arg(format!(
            "-DCMAKE_INSTALL_PREFIX={}",
            layout.install_dir.display()
        ));
    syn::run(&mut configure, "cmake configuration failed");

    cmake.build_target("OGDF", Some("Release"), "cmake build failed");
    cmake.install(Some("Release"), "cmake install failed");

    if available_lib_dirs(&layout.install_dir).is_empty() {
        panic!(
            "OGDF build did not produce a usable library under {}",
            layout.install_dir.display()
        );
    }
    layout.install_dir.clone()
}

fn ogdf_layout() -> OgdfLayout {
    let tag = OGDF_TAG.to_string();
    let archive_path = syn::vendor_dir().join(format!("ogdf-{tag}.tar.gz"));
    if !archive_path.is_file() {
        panic!(
            "missing vendored OGDF archive at {}",
            archive_path.display()
        );
    }

    let fingerprint = syn::CacheFingerprint::builder()
        .flag("native", syn::wants_native_cpu_flags())
        .build();
    let cache = syn::cache_dir(tag.as_str())
        .with_fingerprint_opt(fingerprint)
        .lock();
    let root = cache.path().to_path_buf();

    OgdfLayout {
        _cache_lock: cache,
        archive_path,
        source_dir: root.join(format!("ogdf-{tag}")),
        build_dir: root.join("build"),
        install_dir: root.join("install"),
    }
}

fn available_lib_dirs(root: &Path) -> Vec<PathBuf> {
    ["lib", "lib64"]
        .into_iter()
        .map(|dir| root.join(dir))
        .filter(|dir| dir.join("libOGDF.a").exists() && dir.join("libCOIN.a").exists())
        .collect()
}

fn lib_dirs(root: &Path) -> Vec<PathBuf> {
    let dirs = available_lib_dirs(root);
    if dirs.is_empty() {
        panic!(
            "OGDF libraries missing under {} (looked for lib/ and lib64/)",
            root.display()
        );
    }
    dirs
}

fn perf_flag_string() -> String {
    PERF_FLAGS
        .iter()
        .copied()
        .chain(native_cpu_flags().iter().copied())
        .collect::<Vec<_>>()
        .join(" ")
}

fn apply_perf_flags(build: &mut cc::Build) {
    for flag in PERF_FLAGS {
        build.flag(flag);
    }
    if syn::wants_native_cpu_flags() {
        for flag in NATIVE_CPU_FLAGS {
            build.flag(flag);
        }
    }
}

fn native_cpu_flags() -> &'static [&'static str] {
    if syn::wants_native_cpu_flags() {
        NATIVE_CPU_FLAGS
    } else {
        &[]
    }
}