fn load_dotenv(manifest_dir: &std::path::Path) {
let app_env = std::env::var("APP_ENV").unwrap_or_default();
let mut search_roots: Vec<std::path::PathBuf> = vec![manifest_dir.to_path_buf()];
if let Some(parent) = manifest_dir.parent() {
search_roots.push(parent.to_path_buf());
}
let mut candidates: Vec<std::path::PathBuf> = Vec::new();
for root in &search_roots {
if !app_env.is_empty() {
candidates.push(root.join(format!(".env.{}", app_env)));
}
candidates.push(root.join(".env.local"));
candidates.push(root.join(".env.prod"));
candidates.push(root.join(".env"));
}
let path = match candidates.into_iter().find(|p| p.exists()) {
Some(p) => p,
None => return,
};
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return,
};
println!("cargo:rerun-if-changed={}", path.display());
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let val = val.trim().trim_matches('\'').trim_matches('"');
if std::env::var(key).is_err() {
#[allow(unused_unsafe)]
unsafe {
std::env::set_var(key, val);
}
}
}
}
}
fn resolve_proto_root(
manifest_dir: &std::path::Path,
) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
if let Ok(explicit) = std::env::var("UDB_PROTO_ROOT") {
let p = resolve_existing_project_path(&explicit, manifest_dir);
if !p.exists() {
return Err(format!(
"UDB_PROTO_ROOT points to a path that does not exist: {}",
p.display()
)
.into());
}
return Ok(p);
}
let local_candidate = manifest_dir.join("proto");
if local_candidate.join("udb").exists() {
return Ok(local_candidate);
}
let sibling_candidate = manifest_dir
.parent()
.map(|root| root.join("proto"))
.ok_or("unable to locate proto directory: set UDB_PROTO_ROOT to override")?;
if !sibling_candidate.exists() {
return Err(format!(
"auto-detected proto root does not exist. Tried:\n - {}\n - {}\nSet UDB_PROTO_ROOT to the correct path.",
local_candidate.display(),
sibling_candidate.display(),
)
.into());
}
Ok(sibling_candidate)
}
fn resolve_existing_project_path(raw: &str, manifest_dir: &std::path::Path) -> std::path::PathBuf {
let trimmed = raw.trim();
let path = std::path::PathBuf::from(trimmed);
if trimmed.is_empty() || path.is_absolute() || path.exists() {
return path;
}
if let Some(repo_root) = manifest_dir.parent() {
let candidate = repo_root.join(&path);
if candidate.exists() {
return candidate;
}
}
let mut dir = std::env::current_dir().unwrap_or_else(|_| manifest_dir.to_path_buf());
loop {
let candidate = dir.join(&path);
if candidate.exists() {
return candidate;
}
match dir.parent() {
Some(parent) => dir = parent.to_path_buf(),
None => return path,
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rustc-check-cfg=cfg(udb_portable)");
let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
if std::env::var_os("PROTOC").is_none() {
let protoc = protoc_bin_vendored::protoc_bin_path()?;
#[allow(unused_unsafe)]
unsafe {
std::env::set_var("PROTOC", protoc);
}
}
load_dotenv(&manifest_dir);
let proto_root = resolve_proto_root(&manifest_dir)?;
let requested_prefix = std::env::var("UDB_PROTO_PREFIX").unwrap_or_else(|_| "udb".to_string());
let prefix = if requested_prefix != "udb"
&& proto_root.join("udb").exists()
&& std::env::var("UDB_PROTO_PREFIX_FORCE").is_err()
{
println!(
"cargo:warning=Ignoring legacy UDB_PROTO_PREFIX={requested_prefix}; using neutral proto/udb. Set UDB_PROTO_PREFIX_FORCE=1 to override."
);
"udb".to_string()
} else {
requested_prefix
};
println!("cargo:rerun-if-changed={}", proto_root.display());
println!("cargo:rerun-if-env-changed=UDB_PROTO_ROOT");
println!("cargo:rerun-if-env-changed=UDB_PROTO_PREFIX");
let udb_root = proto_root.join(&prefix);
if !udb_root.exists() {
return Err(format!(
"udb proto directory not found: {}\n\
Adjust UDB_PROTO_PREFIX (current value: \"{}\") or UDB_PROTO_ROOT.",
udb_root.display(),
prefix
)
.into());
}
let out_dir = std::env::var("OUT_DIR")?;
let descriptor_path = std::path::Path::new(&out_dir).join("udb_descriptor.bin");
let mut proto_files = Vec::new();
collect_proto_files(&udb_root, &mut proto_files)?;
proto_files.sort();
if proto_files.is_empty() {
return Err(format!("no .proto files found under {}", udb_root.display()).into());
}
for file in &proto_files {
println!("cargo:rerun-if-changed={}", file.display());
}
let mut includes: Vec<std::path::PathBuf> = vec![proto_root.clone()];
for candidate in [
manifest_dir.join("third_party/googleapis"),
proto_root
.parent()
.map(|parent| parent.join("third_party/googleapis"))
.unwrap_or_default(),
] {
if candidate.join("google/api/annotations.proto").exists() {
includes.push(candidate);
break;
}
}
tonic_build::configure()
.build_server(true)
.build_client(true)
.bytes([
".udb.entity.v1.Chunk.data",
".udb.entity.v1.ProjectionDriftDivergentRow.row_key_json",
])
.file_descriptor_set_path(&descriptor_path)
.compile_protos(&proto_files, &includes)?;
let mut packages = std::collections::BTreeSet::new();
for file in &proto_files {
if let Some(pkg) = read_proto_package(file) {
packages.insert(pkg);
}
}
if packages.is_empty() {
return Err("no `package` declarations found in the compiled protos".into());
}
let mut protocol_rs = render_package_modules(&packages);
for legacy in ["udb.entity.v1", "udb.events.v1", "udb.services.v1"] {
if packages.contains(legacy) {
protocol_rs.push_str(&format!("pub use {}::*;\n", legacy.replace('.', "::")));
}
}
std::fs::write(
std::path::Path::new(&out_dir).join("protocol.rs"),
protocol_rs,
)?;
let redaction_rs = render_storage_only_redaction(&proto_files);
std::fs::write(
std::path::Path::new(&out_dir).join("dto_redaction.rs"),
redaction_rs,
)?;
Ok(())
}
fn render_storage_only_redaction(proto_files: &[std::path::PathBuf]) -> String {
let mut entries: std::collections::BTreeMap<(String, String), Vec<String>> =
std::collections::BTreeMap::new();
for file in proto_files {
let Ok(contents) = std::fs::read_to_string(file) else {
continue;
};
let package = read_proto_package(file).unwrap_or_default();
let mut msg_stack: Vec<(String, i32)> = Vec::new();
let mut depth: i32 = 0;
for line in contents.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("message ") {
if trimmed.contains('{') {
let name = rest
.split_whitespace()
.next()
.unwrap_or("")
.trim_end_matches('{')
.trim()
.to_string();
if !name.is_empty() {
msg_stack.push((name, depth));
}
}
}
if trimmed.contains("OUTPUT_VIEW_STORAGE_ONLY") {
if let (Some((msg, _)), Some(field)) =
(msg_stack.last(), field_name_before_eq(trimmed))
{
let is_field = !field.is_empty()
&& field
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
if is_field {
let v = entries.entry((package.clone(), msg.clone())).or_default();
if !v.contains(&field) {
v.push(field);
}
}
}
}
depth += line.matches('{').count() as i32 - line.matches('}').count() as i32;
while let Some((_, opened_at)) = msg_stack.last() {
if *opened_at >= depth {
msg_stack.pop();
} else {
break;
}
}
}
}
let mut out = String::new();
out.push_str("// @generated by build.rs (render_storage_only_redaction) — DO NOT EDIT.\n");
out.push_str(
"// Blanks OUTPUT_VIEW_STORAGE_ONLY entity fields from outbound DTOs (final_task.md §7).\n",
);
out.push_str(
"/// Blank every `output_view: OUTPUT_VIEW_STORAGE_ONLY` field on an outbound DTO.\n",
);
out.push_str("pub trait RedactStorageOnly {\n fn redact_storage_only(&mut self);\n}\n\n");
out.push_str(
"/// Proto-derived `(fully-qualified-message, storage-only-fields)` coverage emitted with the redaction impls.\n",
);
out.push_str("pub const GENERATED_STORAGE_ONLY_REDACTION_FIELDS: &[(&str, &[&str])] = &[\n");
for ((package, message), fields) in &entries {
let full_name = if package.is_empty() {
message.clone()
} else {
format!("{package}.{message}")
};
out.push_str(&format!(" (\"{full_name}\", &["));
for (idx, field) in fields.iter().enumerate() {
if idx > 0 {
out.push_str(", ");
}
out.push_str(&format!("\"{field}\""));
}
out.push_str("]),\n");
}
out.push_str("];\n\n");
for ((package, message), fields) in &entries {
let type_name = prost_rust_type_name(message);
let path = format!(
"crate::proto::{}::{}",
package.replace('.', "::"),
type_name
);
out.push_str(&format!("impl RedactStorageOnly for {path} {{\n"));
out.push_str(" fn redact_storage_only(&mut self) {\n");
for f in fields {
out.push_str(&format!(
" self.{f} = ::core::default::Default::default();\n"
));
}
out.push_str(" }\n}\n\n");
}
out
}
fn prost_rust_type_name(proto_message: &str) -> String {
if proto_message
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
{
let mut chars = proto_message.chars();
match chars.next() {
Some(first) => {
let mut out = first.to_string();
out.push_str(&chars.as_str().to_ascii_lowercase());
out
}
None => String::new(),
}
} else {
heck::ToUpperCamelCase::to_upper_camel_case(proto_message)
}
}
fn field_name_before_eq(line: &str) -> Option<String> {
let eq = line.find('=')?;
line[..eq].split_whitespace().last().map(|s| s.to_string())
}
fn collect_proto_files(
dir: &std::path::Path,
out: &mut Vec<std::path::PathBuf>,
) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
if path.is_dir() {
collect_proto_files(&path, out)?;
} else if path.extension().and_then(|ext| ext.to_str()) == Some("proto") {
out.push(path);
}
}
Ok(())
}
fn read_proto_package(path: &std::path::Path) -> Option<String> {
let contents = std::fs::read_to_string(path).ok()?;
for line in contents.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("package ") {
return Some(rest.trim().trim_end_matches(';').trim().to_string());
}
}
None
}
fn render_package_modules(packages: &std::collections::BTreeSet<String>) -> String {
#[derive(Default)]
struct Node {
children: std::collections::BTreeMap<String, Node>,
is_package: bool,
}
let mut root = Node::default();
for pkg in packages {
let mut node = &mut root;
for segment in pkg.split('.') {
node = node.children.entry(segment.to_string()).or_default();
}
node.is_package = true;
}
fn render(node: &Node, path: &str, indent: usize, out: &mut String) {
for (segment, child) in &node.children {
let pad = " ".repeat(indent);
let child_path = if path.is_empty() {
segment.clone()
} else {
format!("{path}.{segment}")
};
out.push_str(&format!("{pad}pub mod {segment} {{\n"));
if child.is_package {
out.push_str(&format!(
"{pad} tonic::include_proto!(\"{child_path}\");\n"
));
}
render(child, &child_path, indent + 1, out);
out.push_str(&format!("{pad}}}\n"));
}
}
let mut out =
String::from("// @generated by build.rs from the compiled proto packages — do not edit.\n");
render(&root, "", 0, &mut out);
out
}