osm-katana/build.rs
//! osm-katana's build script. Two jobs:
//! 1. Emit `OSM_KATANA_GIT_HASH` — the short git SHA the build was cut from, read
//! PURE-FS (no `git` process) from the checkout's own plumbing. This is korp's
//! `build_provenance::head_sha` pattern (`korp/src/build_provenance.rs`), the one
//! pattern the build-version law asks for: `osm-katana --version` prints
//! `osm-katana v<semver> (<short-hash>)`, exactly the shape `korp --version` has.
//! Handles a symbolic or detached `HEAD`, a loose or packed ref, and a linked
//! worktree (`.git` is a `gitdir:` file, refs live in the common dir). Degrades to
//! `unknown` — never fails the build — for a source tarball with no `.git`.
//! 2. Generate the protobuf modules for the PBF reader.
use std::io::Write as _;
use std::path::{Path, PathBuf};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out = std::env::var("OUT_DIR")?;
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let (hash, watch) = head_sha(&manifest_dir);
for p in watch {
println!("cargo:rerun-if-changed={}", p.display());
}
println!("cargo:rustc-env=OSM_KATANA_GIT_HASH={hash}");
protobuf_codegen::Codegen::new()
.pure()
.out_dir(&out)
.inputs(["proto/fileformat.proto", "proto/osmformat.proto"])
.include("proto")
.run()?;
// Write a mod.rs so that `include!(OUT_DIR/proto_mod.rs)` can declare both
// generated modules and Rust will resolve them relative to OUT_DIR.
std::fs::File::create(format!("{out}/proto_mod.rs"))?
.write_all(b"pub mod fileformat;\npub mod osmformat;\n")?;
println!("cargo:rerun-if-changed=proto/fileformat.proto");
println!("cargo:rerun-if-changed=proto/osmformat.proto");
Ok(())
}
// ── THE BUILD STAMP (korp's `head_sha`, verbatim in shape) ───────────────────
//
// Read the short SHA of `HEAD` from the git plumbing under `root`, plus the files a
// change to it would move (for `rerun-if-changed`). `"unknown"` when there is no
// checkout, or nothing resolvable — never an error, a tarball build must still build.
fn head_sha(root: &Path) -> (String, Vec<PathBuf>) {
let mut watch = Vec::new();
let Some(git) = resolve_git_dir(root) else {
return ("unknown".into(), watch);
};
let common = common_dir(&git);
watch.push(git.join("HEAD"));
watch.push(common.join("packed-refs"));
let Ok(head) = std::fs::read_to_string(git.join("HEAD")) else {
return ("unknown".into(), watch);
};
let head = head.trim();
let full = match head.strip_prefix("ref:") {
Some(ref_name) => {
let ref_name = ref_name.trim();
let loose = git.join(ref_name);
let loose_common = common.join(ref_name);
watch.push(loose.clone());
watch.push(loose_common.clone());
match std::fs::read_to_string(&loose)
.ok()
.or_else(|| std::fs::read_to_string(&loose_common).ok())
{
Some(s) => s.trim().to_string(),
None => match resolve_packed(&common.join("packed-refs"), ref_name) {
Some(s) => s,
None => return ("unknown".into(), watch),
},
}
}
None => head.to_string(),
};
if full.len() < 7 {
return ("unknown".into(), watch);
}
(full.chars().take(10).collect(), watch)
}
/// The git directory for a checkout at (or above) `root`: a `.git` DIRECTORY
/// normally, or the `gitdir:` a `.git` FILE points at for a linked worktree.
/// osm-katana is a workspace member, so the checkout root is a parent of the crate.
fn resolve_git_dir(root: &Path) -> Option<PathBuf> {
let mut dir = Some(root);
while let Some(d) = dir {
let dot = d.join(".git");
if dot.is_dir() {
return Some(dot);
}
if dot.is_file() {
let content = std::fs::read_to_string(&dot).ok()?;
let rel = content.strip_prefix("gitdir:")?.trim();
let p = Path::new(rel);
return Some(if p.is_absolute() { p.to_path_buf() } else { d.join(p) });
}
dir = d.parent();
}
None
}
/// The shared common git dir: for a linked worktree, `gitdir/commondir` points at
/// the repo's real git dir (where `packed-refs` and the shared refs live).
fn common_dir(gitdir: &Path) -> PathBuf {
match std::fs::read_to_string(gitdir.join("commondir")) {
Ok(s) => {
let rel = s.trim();
let p = Path::new(rel);
if p.is_absolute() { p.to_path_buf() } else { gitdir.join(p) }
}
Err(_) => gitdir.to_path_buf(),
}
}
/// Resolve `ref_name` out of a `packed-refs` file (`<sha> refs/heads/foo`), skipping
/// comment (`#`) and peeled-tag (`^`) lines.
fn resolve_packed(packed: &Path, ref_name: &str) -> Option<String> {
let content = std::fs::read_to_string(packed).ok()?;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
continue;
}
if let Some((sha, name)) = line.split_once(' ')
&& name.trim() == ref_name
{
return Some(sha.trim().to_string());
}
}
None
}