valhalla 0.3.1

Rust bindings for Valhalla routing engine
Documentation
use miniserde::{Deserialize, json};
use std::fs;
use std::path::Path;

fn main() {
    let build_type = if matches!(std::env::var("PROFILE"), Ok(profile) if profile == "debug") {
        "Debug"
    } else {
        "Release"
    };

    // Copy valhalla source to OUT_DIR to avoid modifying the original source that is happening in
    // `valhalla/third_party/tz/leapseconds`
    let out_dir = std::env::var("OUT_DIR").unwrap();
    let valhalla_src = Path::new(&out_dir).join("valhalla-src");
    if valhalla_src.exists() {
        fs::remove_dir_all(&valhalla_src).expect("Failed to remove existing valhalla source");
    }
    copy_dir("valhalla", &valhalla_src).expect("Failed to copy valhalla source");

    // Build & link required Valhalla libraries
    let dst = cmake::Config::new(&valhalla_src)
        .define("CMAKE_BUILD_TYPE", build_type)
        .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") // Required to extract include paths
        // // Enable link-time optimization only in Release configuration to have reasonable compile times in Debug
        // .define(
        //     "CMAKE_INTERPROCEDURAL_OPTIMIZATION",
        //     if build_type == "Release" { "ON" } else { "OFF" },
        // )
        // Disable everything we don't need to reduce number of system dependencies and speed up compilation
        .define("ENABLE_TOOLS", "OFF")
        .define("ENABLE_DATA_TOOLS", "OFF")
        .define("ENABLE_SERVICES", "OFF")
        .define("ENABLE_HTTP", "OFF")
        .define("ENABLE_PYTHON_BINDINGS", "OFF")
        .define("ENABLE_TESTS", "OFF")
        .define("ENABLE_GDAL", "OFF")
        // Switch `graph_tile_ptr` to `std::shared_ptr` that together with cxx `SharedPtr` allows to use `GraphTile` in Rust
        .define("ENABLE_THREAD_SAFE_TILE_REF_COUNT", "ON")
        .build_target("valhalla")
        .build();

    let valhalla_includes = extract_includes(&dst.join("build/compile_commands.json"), "config.cc");

    // Link Valhalla and its requirements via libvalhalla.pc file, generated by cmake
    let dst = dst.display().to_string();
    println!("cargo:rustc-link-search={dst}/build/src/");
    pkg_config::Config::new()
        .arg("--with-path")
        .arg(format!("{dst}/build"))
        .probe("libvalhalla")
        .unwrap();

    // bindings
    cxx_build::bridge("src/lib.rs")
        .file("src/libvalhalla.cpp")
        .std("c++17")
        .includes(valhalla_includes)
        // Should be defined to have consistent behavior with valhalla tile ref definition
        .define("ENABLE_THREAD_SAFE_TILE_REF_COUNT", None)
        .compile("libvalhalla-cxxbridge");
    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rerun-if-changed=src/libvalhalla.hpp");
    println!("cargo:rerun-if-changed=src/libvalhalla.cpp");
    println!("cargo:rerun-if-changed=valhalla");
}

/// Recursively copy directory from src to dst, handling special files gracefully
fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
    let src = src.as_ref();
    let dst = dst.as_ref();

    fs::create_dir_all(dst)?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let src_path = entry.path();
        let dst_path = dst.join(entry.file_name());

        let file_type = entry.file_type()?;

        if file_type.is_dir() {
            copy_dir(&src_path, &dst_path)?;
        } else if file_type.is_file() {
            fs::copy(&src_path, &dst_path)?;
        } else if file_type.is_symlink() {
            // Handle symlinks by copying the target or skipping if broken
            match fs::read_link(&src_path) {
                Ok(target) => {
                    // Try to create the symlink, fall back to copying the target if possible
                    if std::os::unix::fs::symlink(&target, &dst_path).is_err() {
                        // If symlinking fails, try to copy the target file/dir
                        if let Ok(resolved) = src_path.canonicalize() {
                            if resolved.is_dir() {
                                copy_dir(&resolved, &dst_path)?;
                            } else if resolved.is_file() {
                                fs::copy(&resolved, &dst_path)?;
                            }
                        }
                        // If all else fails, just skip this file
                    }
                }
                Err(_) => {
                    // Broken symlink, skip it
                    eprintln!("Warning: Skipping broken symlink: {src_path:?}");
                }
            }
        }
        // Skip other special files (devices, sockets, etc.)
    }

    Ok(())
}

/// https://clang.llvm.org/docs/JSONCompilationDatabase.html
#[derive(Deserialize)]
struct CompileCommand {
    command: String,
    file: String,
}

fn extract_includes(compile_commands: &Path, cpp_source: &str) -> Vec<String> {
    assert!(compile_commands.exists(), "compile_commands.json not found");

    let content =
        std::fs::read_to_string(compile_commands).expect("Failed to read compile_commands.json");
    let commands: Vec<CompileCommand> =
        json::from_str(&content).expect("Failed to parse compile_commands.json");

    let command = commands
        .into_iter()
        .find(|cmd| cmd.file.ends_with(cpp_source))
        .expect("Failed to find reference cpp source file");

    // Parse -I/path/to/include and -isystem /path/to/include
    let args: Vec<&str> = command.command.split_whitespace().collect();
    let mut includes = Vec::new();

    for i in 0..args.len() {
        if args[i].starts_with("-I") {
            // Handle -I/path/to/include
            includes.push(args[i][2..].to_string());
        } else if args[i] == "-isystem" && i + 1 < args.len() {
            // Handle -isystem /path/to/include
            includes.push(args[i + 1].to_string());
        }
    }
    includes
}