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",
};
let unity_build = !matches!(std::env::var("UNITY_BUILD").as_deref(), Ok("OFF"));
let lto = build_type != "Debug" && has_lld();
let dst = cmake::Config::new("valhalla")
.define("CMAKE_BUILD_TYPE", build_type)
.define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON") .define(
"CMAKE_INTERPROCEDURAL_OPTIMIZATION",
if lto { "ON" } else { "OFF" },
)
.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_LZ4", "OFF") .define("ENABLE_SINGLE_FILES_WERROR", "OFF")
.define("CMAKE_UNITY_BUILD", if unity_build { "ON" } else { "OFF" })
.define("ENABLE_THREAD_SAFE_TILE_REF_COUNT", "OFF")
.define("LOGGING_LEVEL", "WARN") .build_target("valhalla")
.build();
let _ = fs::remove_file("valhalla/third_party/tz/leapseconds");
let valhalla_includes = extract_includes(
&dst.join("build/compile_commands.json"),
if unity_build {
"/valhalla.dir/Unity/unity_0_cxx.cxx"
} else {
"config.cc"
},
);
cxx_build::bridges(["src/actor.rs", "src/config.rs", "src/lib.rs"])
.file("src/libvalhalla.cpp")
.std("c++20")
.includes(valhalla_includes)
.flags(if lto { vec!["-flto=thin"] } else { vec![] })
.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/costing.hpp");
println!("cargo:rerun-if-changed=src/libvalhalla.cpp");
println!("cargo:rerun-if-changed=src/libvalhalla.hpp");
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")
{
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}");
}
let proto_files: Vec<_> = fs::read_dir("valhalla/proto/descriptors")
.expect("Failed to read valhalla/proto/descriptors 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/descriptors/"])
.expect("Failed to compile proto files");
println!("cargo:rerun-if-changed=valhalla/proto/descriptors");
let script = fs::read_to_string("valhalla/scripts/valhalla_build_config")
.expect("Failed to read valhalla_build_config script");
let config = parse_valhalla_config(&script);
let help = parse_valhalla_help_text(&script);
let code = generate_config_code(&config, &help);
let out_dir = std::env::var("OUT_DIR").unwrap();
fs::write(Path::new(&out_dir).join("config_builder.rs"), &code)
.expect("Failed to write config_builder.rs");
println!("cargo:rerun-if-changed=valhalla/scripts/valhalla_build_config");
}
#[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");
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
}
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
}
#[derive(Debug, Clone)]
enum PyValue {
Dict(Vec<(String, PyValue)>),
List(Vec<PyValue>),
Str(String),
Int(i64),
Float(f64),
Bool(bool),
Optional(String),
}
struct PyParser {
chars: Vec<char>,
pos: usize,
}
impl PyParser {
fn new(input: &str) -> Self {
Self {
chars: input.chars().collect(),
pos: 0,
}
}
fn skip_ws(&mut self) {
while self.pos < self.chars.len() {
match self.chars[self.pos] {
' ' | '\t' | '\n' | '\r' => self.pos += 1,
'#' => {
while self.pos < self.chars.len() && self.chars[self.pos] != '\n' {
self.pos += 1;
}
}
_ => break,
}
}
}
fn peek(&self) -> char {
self.chars[self.pos]
}
fn advance(&mut self) -> char {
let c = self.chars[self.pos];
self.pos += 1;
c
}
fn expect(&mut self, expected: char) {
self.skip_ws();
let c = self.advance();
assert_eq!(c, expected, "Expected '{expected}', got '{c}'");
}
fn consume(&mut self, word: &str) {
for expected in word.chars() {
let c = self.advance();
assert_eq!(c, expected, "Expected '{expected}', got '{c}'");
}
}
fn parse_value(&mut self) -> PyValue {
self.skip_ws();
match self.peek() {
'{' => self.parse_dict(),
'[' => self.parse_list(),
'"' | '\'' => PyValue::Str(self.parse_string()),
'(' => {
self.advance();
let mut result = String::new();
loop {
self.skip_ws();
if self.peek() == ')' {
self.advance();
break;
}
result.push_str(&self.parse_string());
}
PyValue::Str(result)
}
'T' => {
self.consume("True");
PyValue::Bool(true)
}
'F' => {
self.consume("False");
PyValue::Bool(false)
}
'O' => {
self.consume("Optional");
self.expect('(');
let type_name = self.parse_ident();
self.expect(')');
PyValue::Optional(type_name)
}
c if c == '-' || c.is_ascii_digit() => self.parse_number(),
c => panic!("Unexpected character '{c}' at position {}", self.pos),
}
}
fn parse_dict(&mut self) -> PyValue {
self.expect('{');
let mut entries = Vec::new();
loop {
self.skip_ws();
if self.peek() == '}' {
self.advance();
break;
}
let key = self.parse_string();
self.expect(':');
let value = self.parse_value();
entries.push((key, value));
self.skip_ws();
if self.peek() == ',' {
self.advance();
}
}
PyValue::Dict(entries)
}
fn parse_list(&mut self) -> PyValue {
self.expect('[');
let mut items = Vec::new();
loop {
self.skip_ws();
if self.peek() == ']' {
self.advance();
break;
}
items.push(self.parse_value());
self.skip_ws();
if self.peek() == ',' {
self.advance();
}
}
PyValue::List(items)
}
fn parse_string(&mut self) -> String {
self.skip_ws();
let quote = self.advance();
assert!(
quote == '"' || quote == '\'',
"Expected string, got '{quote}'"
);
let mut s = String::new();
loop {
let c = self.advance();
if c == quote {
break;
}
match c {
'\\' => {
let next = self.advance();
match next {
'"' | '\'' | '\\' => s.push(next),
'n' => s.push('\n'),
't' => s.push('\t'),
'r' => s.push('\r'),
_ => {
s.push('\\');
s.push(next);
}
}
}
_ => s.push(c),
}
}
s
}
fn parse_number(&mut self) -> PyValue {
self.skip_ws();
let start = self.pos;
if self.peek() == '-' {
self.advance();
}
while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
self.advance();
}
let mut is_float = false;
if self.pos < self.chars.len() && self.chars[self.pos] == '.' {
is_float = true;
self.advance();
while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
self.advance();
}
}
if self.pos < self.chars.len()
&& (self.chars[self.pos] == 'e' || self.chars[self.pos] == 'E')
{
is_float = true;
self.advance();
if self.pos < self.chars.len()
&& (self.chars[self.pos] == '+' || self.chars[self.pos] == '-')
{
self.advance();
}
while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
self.advance();
}
}
let text: String = self.chars[start..self.pos].iter().collect();
if is_float {
PyValue::Float(text.parse().expect("Invalid float"))
} else {
PyValue::Int(text.parse().expect("Invalid int"))
}
}
fn parse_ident(&mut self) -> String {
self.skip_ws();
let start = self.pos;
while self.pos < self.chars.len()
&& (self.chars[self.pos].is_alphanumeric() || self.chars[self.pos] == '_')
{
self.advance();
}
self.chars[start..self.pos].iter().collect()
}
}
fn parse_valhalla_config(config_script: &str) -> PyValue {
let marker = "\nconfig = {";
let start = config_script
.find(marker)
.expect("No `config` dict in valhalla_build_config script");
let dict_start = start + "\nconfig = ".len();
let mut parser = PyParser::new(&config_script[dict_start..]);
parser.parse_value()
}
fn parse_valhalla_help_text(config_script: &str) -> PyValue {
let marker = "\nhelp_text = {";
let start = config_script
.find(marker)
.expect("No `nhelp_text` dict in valhalla_build_config script");
let dict_start = start + "\nhelp_text = ".len();
let mut parser = PyParser::new(&config_script[dict_start..]);
parser.parse_value()
}
fn lookup_help<'a>(help: &'a PyValue, path: &[&str]) -> Option<&'a str> {
let mut current = help;
for &key in path {
match current {
PyValue::Dict(entries) => {
current = &entries.iter().find(|(k, _)| k == key)?.1;
}
_ => return None,
}
}
match current {
PyValue::Str(s) => Some(s.as_str()),
_ => None,
}
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect()
}
fn rust_field_name(key: &str) -> String {
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "dyn",
];
if key.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("level_{key}")
} else if RUST_KEYWORDS.contains(&key) {
format!("r#{key}")
} else {
key.to_string()
}
}
fn rust_string_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 16);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
_ => out.push(c),
}
}
out.push_str("\".to_string()");
out
}
fn rust_float_literal(f: f64) -> String {
if f.fract() == 0.0 && !f.is_infinite() && !f.is_nan() {
format!("{f:.1}")
} else {
format!("{f}")
}
}
fn struct_name_from_path(path: &[&str]) -> String {
path.iter()
.map(|s| to_pascal_case(s))
.collect::<Vec<_>>()
.join("")
}
fn infer_list_type(items: &[PyValue]) -> String {
if items.is_empty() {
return "Vec<String>".to_string();
}
match &items[0] {
PyValue::Str(_) => "Vec<String>".to_string(),
PyValue::Int(_) => "Vec<i64>".to_string(),
PyValue::Float(_) => "Vec<f64>".to_string(),
_ => "Vec<String>".to_string(),
}
}
fn optional_rust_type(type_name: &str) -> String {
match type_name {
"str" => "Option<String>".to_string(),
"int" => "Option<i64>".to_string(),
"bool" => "Option<bool>".to_string(),
"list" => "Option<Vec<String>>".to_string(),
_ => panic!("Unknown Optional type: {type_name}"),
}
}
fn can_derive_default(entries: &[(String, PyValue)]) -> bool {
entries.iter().all(|(_, value)| {
match value {
PyValue::Dict(_) => true, PyValue::Optional(_) => true, PyValue::List(l) => l.is_empty(), PyValue::Str(s) => s.is_empty(), PyValue::Int(n) => *n == Default::default(),
PyValue::Float(f) => *f == Default::default(),
PyValue::Bool(b) => *b == Default::default(),
}
})
}
fn default_value_expr(value: &PyValue, parent_path: &[&str], key: &str) -> String {
match value {
PyValue::Dict(_) => {
let child_path: Vec<&str> = parent_path
.iter()
.copied()
.chain(std::iter::once(key))
.collect();
let child_type = struct_name_from_path(&child_path);
format!("{child_type}::default()")
}
PyValue::Str(s) => rust_string_literal(s),
PyValue::Int(n) => n.to_string(),
PyValue::Float(f) => rust_float_literal(*f),
PyValue::Bool(b) => b.to_string(),
PyValue::List(items) => list_default_expr(items),
PyValue::Optional(_) => "None".to_string(),
}
}
fn list_default_expr(items: &[PyValue]) -> String {
if items.is_empty() {
return "vec![]".to_string();
}
let parts: Vec<String> = items
.iter()
.map(|v| match v {
PyValue::Str(s) => rust_string_literal(s),
PyValue::Int(n) => n.to_string(),
PyValue::Float(f) => rust_float_literal(*f),
PyValue::Bool(b) => b.to_string(),
_ => panic!("Unsupported list element type in default"),
})
.collect();
format!("vec![{}]", parts.join(", "))
}
fn generate_config_code(config: &PyValue, help: &PyValue) -> String {
let mut output = String::new();
if let PyValue::Dict(entries) = config {
output.push_str("/// Valhalla configuration builder.\n");
output.push_str(
"/// Auto-generated from `valhalla/scripts/valhalla_build_config` — do not edit.\n",
);
generate_struct(&mut output, "ConfigBuilder", entries, &[], help);
}
output.push_str(CONFIG_BUILDER_IMPL);
output
}
const CONFIG_BUILDER_IMPL: &str = r#"impl ConfigBuilder {
/// Produces the finalized [`Config`].
///
/// All fields are initialized with Valhalla's built-in defaults from
/// `valhalla_build_config`. Modify any field before calling `build()`.
///
/// # Examples
///
/// ```
/// let mut builder = valhalla::ConfigBuilder::default();
/// builder.mjolnir.tile_extract = "path/to/tiles.tar".to_string();
/// let config = builder.build();
/// ```
pub fn build(&self) -> Config {
let mut pt = ffi::ptree_new();
self.write_to_ptree(pt.pin_mut(), "");
Config(pt)
}
}
pub(crate) trait PtreePut {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str);
}
impl PtreePut for String {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_str(pt, path, self);
}
}
impl PtreePut for i64 {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_int(pt, path, *self);
}
}
impl PtreePut for f64 {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_float(pt, path, *self);
}
}
impl PtreePut for bool {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_bool(pt, path, *self);
}
}
impl<T: PtreePut> PtreePut for Option<T> {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
if let Some(v) = self {
v.put(pt, path);
}
}
}
impl PtreePut for Vec<String> {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_str_array(pt, path, self);
}
}
impl PtreePut for Vec<i64> {
fn put(&self, pt: std::pin::Pin<&mut ffi::ptree>, path: &str) {
ffi::ptree_put_int_array(pt, path, self);
}
}
fn ptree_path(prefix: &str, key: &str) -> String {
if prefix.is_empty() {
key.to_string()
} else {
format!("{prefix}.{key}")
}
}
"#;
fn generate_struct(
output: &mut String,
struct_name: &str,
entries: &[(String, PyValue)],
path: &[&str],
help: &PyValue,
) {
let mut note_path: Vec<&str> = path.to_vec();
note_path.push("__note__");
if let Some(note) = lookup_help(help, ¬e_path) {
for line in note.lines() {
output.push_str(&format!("/// {line}\n"));
}
}
let can_derive = can_derive_default(entries);
if can_derive {
output.push_str("#[derive(Debug, Clone, Default)]\n");
} else {
output.push_str("#[derive(Debug, Clone)]\n");
}
output.push_str(&format!("pub struct {struct_name} {{\n"));
for (key, value) in entries {
let field = rust_field_name(key);
let mut field_path: Vec<&str> = path.to_vec();
field_path.push(key.as_str());
if let Some(doc) = lookup_help(help, &field_path) {
for line in doc.lines() {
output.push_str(&format!(" /// {line}\n"));
}
}
match value {
PyValue::Dict(_) => {
let child_path: Vec<&str> = path
.iter()
.copied()
.chain(std::iter::once(key.as_str()))
.collect();
let child_type = struct_name_from_path(&child_path);
output.push_str(&format!(" pub {field}: {child_type},\n"));
}
PyValue::Str(_) => output.push_str(&format!(" pub {field}: String,\n")),
PyValue::Int(_) => output.push_str(&format!(" pub {field}: i64,\n")),
PyValue::Float(_) => output.push_str(&format!(" pub {field}: f64,\n")),
PyValue::Bool(_) => output.push_str(&format!(" pub {field}: bool,\n")),
PyValue::List(items) => {
let list_type = infer_list_type(items);
output.push_str(&format!(" pub {field}: {list_type},\n"));
}
PyValue::Optional(type_name) => {
let rust_type = optional_rust_type(type_name);
output.push_str(&format!(" pub {field}: {rust_type},\n"));
}
}
}
output.push_str("}\n\n");
if !can_derive {
output.push_str(&format!("impl Default for {struct_name} {{\n"));
output.push_str(" fn default() -> Self {\n");
output.push_str(" Self {\n");
for (key, value) in entries {
let field = rust_field_name(key);
let default_expr = default_value_expr(value, path, key);
output.push_str(&format!(" {field}: {default_expr},\n"));
}
output.push_str(" }\n");
output.push_str(" }\n");
output.push_str("}\n\n");
}
output.push_str(&format!("impl {struct_name} {{\n"));
output.push_str(" pub(crate) fn write_to_ptree(&self, mut pt: std::pin::Pin<&mut ffi::ptree>, prefix: &str) {\n");
for (key, value) in entries {
let field = rust_field_name(key);
if matches!(value, PyValue::Dict(_)) {
output.push_str(&format!(
" self.{field}.write_to_ptree(pt.as_mut(), &ptree_path(prefix, \"{key}\"));\n"
));
} else {
output.push_str(&format!(
" self.{field}.put(pt.as_mut(), &ptree_path(prefix, \"{key}\"));\n"
));
}
}
output.push_str(" }\n");
output.push_str("}\n\n");
for (key, value) in entries {
if let PyValue::Dict(child_entries) = value {
let child_path: Vec<&str> = path
.iter()
.copied()
.chain(std::iter::once(key.as_str()))
.collect();
let child_name = struct_name_from_path(&child_path);
generate_struct(output, &child_name, child_entries, &child_path, help);
}
}
}