valhalla 0.6.4

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

fn main() {
    let build_type = match (
        std::env::var("PROFILE").as_deref(),
        std::env::var("DEBUG").as_deref(),
    ) {
        (Ok("debug"), _) => "Debug",
        (Ok("release"), Ok("true")) => "RelWithDebInfo",
        _ => "Release",
    };

    // Build & link required Valhalla libraries
    let dst = cmake::Config::new("valhalla")
        .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")
        .define("ENABLE_SINGLE_FILES_WERROR", "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")
        .define("LOGGING_LEVEL", "WARN") // todo: Provide an API for setting custom loggers to Valhalla
        .build_target("valhalla")
        .build();
    // Clean up temporary created `valhalla/third_party/tz/leapseconds` to keep source tree clean.
    let _ = fs::remove_file("valhalla/third_party/tz/leapseconds");

    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::bridges(["src/lib.rs", "src/config.rs", "src/actor.rs"])
        .file("src/libvalhalla.cpp")
        // Hacky workaraound for linking issue because `get_formatted_date()` function is being called in header file
        // and somehow compiler is unable to resolve it when building bridge library.
        // ```
        // const date::local_seconds pivot_date_ = get_formatted_date(kPivotDate + "T00:00");
        // ```
        .file("valhalla/src/baldr/datetime.cc")
        .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/actor.hpp");
    println!("cargo:rerun-if-changed=src/config.hpp");
    println!("cargo:rerun-if-changed=src/libvalhalla.hpp");
    println!("cargo:rerun-if-changed=src/libvalhalla.cpp");
    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rerun-if-changed=valhalla");

    // protos
    let proto_files: Vec<_> = fs::read_dir("valhalla/proto")
        .expect("Failed to read valhalla/proto directory")
        .map(|entry| entry.expect("Bad fs entry").path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "proto"))
        .collect();
    prost_build::compile_protos(&proto_files, &["valhalla/proto/"])
        .expect("Failed to compile proto files");
}

/// 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 =
        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
}