valhalla 0.6.26

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",
    };
    // Unity build speeds up compilation but can complicate debugging and profiling.
    // Disable with `UNITY_BUILD=OFF`, e.g. `UNITY_BUILD=OFF cargo bench`
    let unity_build = !matches!(std::env::var("UNITY_BUILD").as_deref(), Ok("OFF"));
    // LTO between Rust and C++ requires LLD:
    // https://doc.rust-lang.org/beta/rustc/linker-plugin-lto.html
    // Disable LTO for Debug builds to have reasonable compile times.
    let lto = build_type != "Debug" && has_lld();

    // 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
        .define(
            "CMAKE_INTERPROCEDURAL_OPTIMIZATION",
            if lto { "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_GEOTIFF", "OFF")
        .define("ENABLE_SINGLE_FILES_WERROR", "OFF")
        .define("CMAKE_UNITY_BUILD", if unity_build { "ON" } else { "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");

    // Include paths to the dependencies are tricky, so let cmake do its job while we can read the result.
    let valhalla_includes = extract_includes(
        &dst.join("build/compile_commands.json"),
        if unity_build {
            "/valhalla.dir/Unity/unity_0_cxx.cxx"
        } else {
            "config.cc"
        },
    );

    // Linking order is important, so valhalla-cxxbridge must come first.
    cxx_build::bridges(["src/actor.rs", "src/config.rs", "src/lib.rs"])
        .file("src/libvalhalla.cpp")
        .std("c++20")
        .includes(valhalla_includes)
        // Should be defined to have consistent behavior with valhalla tile ref definition.
        .define("ENABLE_THREAD_SAFE_TILE_REF_COUNT", None)
        .flags(if lto { vec!["-flto=thin"] } else { vec![] })
        .compile("libvalhalla-cxxbridge");

    // pkg_config resolves all dependencies based on the libvalhalla.pc file (generated by cmake)
    // and emits the correct cargo link directives.
    let dst = dst.display().to_string();
    println!("cargo:rustc-link-search=native={dst}/build/src/");
    if let Err(err) = pkg_config::Config::new()
        .arg("--with-path")
        .arg(format!("{dst}/build"))
        .probe("libvalhalla")
    {
        // pkg_config error messages are not very descriptive, so we add some context here.
        let pc_file = format!("{}/build/libvalhalla.pc", dst);
        let pc_content = fs::read_to_string(&pc_file)
            .unwrap_or_else(|_| "Could not read libvalhalla.pc file".to_string());
        panic!("Failed to link libvalhalla: {err}\nlibvalhalla.pc:\n{pc_content}");
    }

    println!("cargo:rerun-if-changed=src/actor.hpp");
    println!("cargo:rerun-if-changed=src/config.hpp");
    println!("cargo:rerun-if-changed=src/costing.hpp");
    println!("cargo:rerun-if-changed=src/libvalhalla.cpp");
    println!("cargo:rerun-if-changed=src/libvalhalla.hpp");
    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
}

/// Check whether LLD is being used as the linker.
fn has_lld() -> bool {
    if std::env::var("TARGET").is_ok_and(|t| t.contains("apple-darwin")) {
        return true;
    }
    if std::env::var("CARGO_ENCODED_RUSTFLAGS").is_ok_and(|f| f.contains("-fuse-ld=lld")) {
        return true;
    }
    false
}