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"
};
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");
let dst = cmake::Config::new(&valhalla_src)
.define("CMAKE_BUILD_TYPE", build_type)
.define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") .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_THREAD_SAFE_TILE_REF_COUNT", "ON")
.build_target("valhalla")
.build();
let valhalla_includes = extract_includes(&dst.join("build/compile_commands.json"), "config.cc");
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();
cxx_build::bridge("src/lib.rs")
.file("src/libvalhalla.cpp")
.std("c++17")
.includes(valhalla_includes)
.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");
}
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() {
match fs::read_link(&src_path) {
Ok(target) => {
if std::os::unix::fs::symlink(&target, &dst_path).is_err() {
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)?;
}
}
}
}
Err(_) => {
eprintln!("Warning: Skipping broken symlink: {src_path:?}");
}
}
}
}
Ok(())
}
#[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");
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") {
includes.push(args[i][2..].to_string());
} else if args[i] == "-isystem" && i + 1 < args.len() {
includes.push(args[i + 1].to_string());
}
}
includes
}