shipwright 0.2.0

Binary-side helper: emits --version / --version --json per the Shipwright contract.
Documentation
// Example `build.rs` for any Nimblesite binary that links `shipwright`.
// Copy into your crate as `build.rs`.
//
// Emits compile-time env vars consumed by the `BuildInfo` struct:
//   SHIPWRIGHT_GIT_SHA     (short sha or "unknown")
//   SHIPWRIGHT_GIT_DIRTY   ("true" / "false")
//   SHIPWRIGHT_BUILD_TIME  (RFC3339 UTC)
//   SHIPWRIGHT_TARGET      (rust target triple from Cargo)
//   SHIPWRIGHT_TOOLCHAIN   ("rustc <version>")
//
// Consume in src/main.rs:
//
//   use shipwright::{dispatch, BuildInfo, VersionSpec};
//   use shipwright_manifest::{ExecutableKind, Language};
//
//   let build = BuildInfo {
//       git_sha:   option_env!("SHIPWRIGHT_GIT_SHA"),
//       git_dirty: option_env!("SHIPWRIGHT_GIT_DIRTY").map(|s| s == "true"),
//       build_time: option_env!("SHIPWRIGHT_BUILD_TIME"),
//       target:    option_env!("SHIPWRIGHT_TARGET"),
//       toolchain: option_env!("SHIPWRIGHT_TOOLCHAIN"),
//   };
//   let spec = VersionSpec {
//       name: env!("CARGO_PKG_NAME"),
//       version: env!("CARGO_PKG_VERSION"),
//       kind: ExecutableKind::Lsp,
//       language: Language::Rust,
//       product: Some("deslop"),
//       capabilities: &["lsp", "stdio"],
//       build,
//   };
//   let args: Vec<String> = std::env::args().collect();
//   let mut stdout = std::io::stdout().lock();
//   if dispatch(&args, &mut stdout, &spec)? { return Ok(()); }

use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=SHIPWRIGHT_GIT_SHA");

    let sha = read_env_or_cmd("SHIPWRIGHT_GIT_SHA", &["git", "rev-parse", "--short=10", "HEAD"])
        .unwrap_or_else(|| "unknown".into());
    println!("cargo:rustc-env=SHIPWRIGHT_GIT_SHA={sha}");

    let dirty = Command::new("git")
        .args(["status", "--porcelain"])
        .output()
        .map(|o| !o.stdout.is_empty())
        .unwrap_or(false);
    println!("cargo:rustc-env=SHIPWRIGHT_GIT_DIRTY={}", if dirty { "true" } else { "false" });

    let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
    // Minimal RFC3339 (UTC seconds); for real output use the `time` crate.
    println!("cargo:rustc-env=SHIPWRIGHT_BUILD_TIME={}", rfc3339_from_secs(secs));

    if let Ok(target) = std::env::var("TARGET") {
        println!("cargo:rustc-env=SHIPWRIGHT_TARGET={target}");
    }

    let tc = Command::new("rustc")
        .arg("--version")
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=SHIPWRIGHT_TOOLCHAIN={tc}");
}

fn read_env_or_cmd(env: &str, cmd: &[&str]) -> Option<String> {
    if let Ok(v) = std::env::var(env) {
        return Some(v);
    }
    let out = Command::new(cmd.first()?).args(&cmd[1..]).output().ok()?;
    if !out.status.success() {
        return None;
    }
    Some(String::from_utf8(out.stdout).ok()?.trim().to_string())
}

fn rfc3339_from_secs(secs: u64) -> String {
    // ymd/hms from unix seconds without external deps.
    let days = secs / 86_400;
    let secs_of_day = secs % 86_400;
    let h = secs_of_day / 3600;
    let m = (secs_of_day % 3600) / 60;
    let s = secs_of_day % 60;
    let (y, mo, d) = epoch_days_to_ymd(days as i64);
    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}

fn epoch_days_to_ymd(mut days: i64) -> (i32, u32, u32) {
    // Howard Hinnant's civil_from_days algorithm.
    days += 719_468;
    let era = if days >= 0 { days / 146_097 } else { (days - 146_096) / 146_097 };
    let doe = (days - era * 146_097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = y + if m <= 2 { 1 } else { 0 };
    (y as i32, m as u32, d as u32)
}