Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

crates/knut-pipelines/build.rs

//! Build script: generates the Spark Connect gRPC bindings from the vendored
//! `proto/` set — but ONLY when the `grpc` feature is enabled.
//!
//! The default workspace build never reaches the codegen path: the
//! `tonic-build` / `protoc-bin-vendored` build-dependencies are `optional`
//! and pulled in solely by `grpc`, and this whole file is a no-op without it.
//! That keeps `cargo build --workspace` green with no `protoc` and no network.

fn main() {
    // The build stamp — emitted UNCONDITIONALLY (before any feature gate) so
    // `env!("KNUT_GIT_HASH")` always resolves in this crate: the `knutpipe` /
    // `knut-spark-server` `--version` carry the short git commit built from.
    emit_git_hash();

    // Re-run if the protos change (cheap; harmless without the feature).
    println!("cargo:rerun-if-changed=proto/spark/connect");
    println!("cargo:rerun-if-changed=build.rs");

    #[cfg(feature = "grpc")]
    generate();
}

/// Emit `cargo:rustc-env=KNUT_GIT_HASH=<short sha>` — the short git commit the
/// binary was built from — read from git metadata as **plain files only**: NO
/// `git`, NO `std::process::Command`, NO shell (the nordisk pure-Rust zero-shell
/// law). `knut-pipelines` is a workspace member (`crates/knut-pipelines`), so the
/// emitter walks up from `CARGO_MANIFEST_DIR` to find the repo `.git`. Handles a
/// normal `.git` dir, a linked-worktree `.git` FILE (`gitdir:` → own HEAD, shared
/// `commondir` for refs), a detached HEAD, loose refs, and `packed-refs`. Falls
/// back to `unknown` so `env!("KNUT_GIT_HASH")` always resolves.
fn emit_git_hash() {
    use std::path::{Path, PathBuf};

    fn find_git_dir(start: &Path) -> Option<PathBuf> {
        let mut cur = Some(start);
        while let Some(d) = cur {
            let g = d.join(".git");
            if g.is_dir() {
                return Some(g);
            }
            if g.is_file() {
                if let Ok(s) = std::fs::read_to_string(&g) {
                    if let Some(p) = s.lines().find_map(|l| l.strip_prefix("gitdir:")) {
                        return Some(PathBuf::from(p.trim()));
                    }
                }
            }
            cur = d.parent();
        }
        None
    }

    fn resolve(git_dir: &Path) -> Option<String> {
        let head_path = git_dir.join("HEAD");
        let head = std::fs::read_to_string(&head_path).ok()?;
        println!("cargo:rerun-if-changed={}", head_path.display());
        let head = head.trim();

        let common = std::fs::read_to_string(git_dir.join("commondir"))
            .ok()
            .map(|s| git_dir.join(s.trim()))
            .and_then(|p| std::fs::canonicalize(&p).ok())
            .unwrap_or_else(|| git_dir.to_path_buf());

        if let Some(reference) = head.strip_prefix("ref:") {
            let reference = reference.trim();
            let loose = common.join(reference);
            if let Ok(s) = std::fs::read_to_string(&loose) {
                println!("cargo:rerun-if-changed={}", loose.display());
                return Some(s.trim().to_string());
            }
            let packed_path = common.join("packed-refs");
            println!("cargo:rerun-if-changed={}", packed_path.display());
            let packed = std::fs::read_to_string(&packed_path).ok()?;
            return packed.lines().find_map(|l| {
                let l = l.trim();
                if l.starts_with('#') || l.starts_with('^') {
                    return None;
                }
                let (sha, name) = l.split_once(' ')?;
                (name == reference).then(|| sha.to_string())
            });
        }
        // Detached HEAD: the file IS the object id.
        Some(head.to_string())
    }

    let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo");
    let hash = find_git_dir(Path::new(&manifest))
        .and_then(|g| resolve(&g))
        .map(|sha| sha.chars().take(7).collect::<String>())
        .filter(|s| s.len() == 7 && s.bytes().all(|b| b.is_ascii_hexdigit()))
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=KNUT_GIT_HASH={hash}");
}

#[cfg(feature = "grpc")]
fn generate() {
    use std::path::PathBuf;

    // Prefer a system protoc; fall back to the vendored binary so the feature
    // builds even on machines without protoc installed.
    if std::env::var_os("PROTOC").is_none()
        && let Ok(p) = protoc_bin_vendored::protoc_bin_path()
    {
        // SAFETY: build scripts are single-threaded at this point.
        unsafe {
            std::env::set_var("PROTOC", p);
        }
    }

    let proto_root = PathBuf::from("proto");
    let dir = proto_root.join("spark/connect");

    // The full self-contained Spark Connect proto set. pipelines.proto pulls
    // in base/commands/relations/expressions/types/common/catalog (and the ml
    // protos transitively via relations), so we compile them all together.
    let protos: Vec<PathBuf> = [
        "base.proto",
        "catalog.proto",
        "commands.proto",
        "common.proto",
        "example_plugins.proto",
        "expressions.proto",
        "ml.proto",
        "ml_common.proto",
        "pipelines.proto",
        "relations.proto",
        "types.proto",
    ]
    .iter()
    .map(|f| dir.join(f))
    .collect();

    // tonic 0.14 split prost codegen into `tonic-prost-build`.
    tonic_prost_build::configure()
        .build_server(false) // we are a client/viewer only
        .build_client(true)
        .compile_protos(&protos, &[proto_root])
        .expect("failed to compile Spark Connect protos for the `grpc` feature");
}