scsh 1.41.16

Scoped Skills Helper — preflight a git repo and run its scoped skills in ephemeral containers.
//! Build script: stamp the binary with the current git commit (seven hex digits) and
//! whether the tree was dirty at build time, so `scsh version` can report them. A
//! crates.io install has no `.git`, but cargo embeds `.cargo_vcs_info.json` — the exact
//! commit the release was published from — into every crate, so the stamp survives
//! `cargo install scsh` too.
//! Also refuse to compile when `src/Dockerfile` exceeds Apple Containers' soft size
//! limit (gRPC header — apple/container#735), so macOS / Apple Silicon stays green.
//! Std-only — shells out to `git` with `-C $CARGO_MANIFEST_DIR` so the hash is found
//! even when cargo's working directory is not the crate root.

use std::path::{Path, PathBuf};
use std::process::Command;

/// Must stay in sync with `runtime::APPLE_CONTAINER_DOCKERFILE_SOFT_LIMIT`.
const APPLE_CONTAINER_DOCKERFILE_SOFT_LIMIT: usize = 15_000;

fn main() {
  let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
  let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));

  println!("cargo:rerun-if-changed=build.rs");
  println!("cargo:rerun-if-changed=src/Dockerfile");
  println!("cargo:rerun-if-env-changed=SCSH_GIT_DESCRIBE");
  println!("cargo:rerun-if-changed=.cargo_vcs_info.json");
  let head = manifest_dir.join(".git/HEAD");
  let index = manifest_dir.join(".git/index");
  if head.exists() {
    println!("cargo:rerun-if-changed={}", head.display());
  }
  if index.exists() {
    println!("cargo:rerun-if-changed={}", index.display());
  }

  check_dockerfile_fits_apple_containers(&manifest_dir);

  let describe = git_describe(&manifest_dir);
  if describe.is_empty() {
    println!(
      "cargo:warning=scsh: could not determine git commit from {}; `scsh version` will omit a hash",
      manifest_dir.display()
    );
  }

  std::fs::write(out_dir.join("scsh_build_info.rs"), format!("pub const GIT_DESCRIBE: &str = {describe:?};\n"))
    .expect("write scsh_build_info.rs");

  // Kept for integration tests (`option_env!("SCSH_GIT_DESCRIBE")`).
  println!("cargo:rustc-env=SCSH_GIT_DESCRIBE={describe}");
}

/// Fail the build early if `src/Dockerfile` would break Apple Containers on macOS.
fn check_dockerfile_fits_apple_containers(manifest_dir: &Path) {
  let path = manifest_dir.join("src/Dockerfile");
  let bytes = std::fs::metadata(&path).map(|m| m.len() as usize).unwrap_or(0);
  if bytes >= APPLE_CONTAINER_DOCKERFILE_SOFT_LIMIT {
    panic!(
      "src/Dockerfile is {bytes} bytes (≥ {APPLE_CONTAINER_DOCKERFILE_SOFT_LIMIT}). \
       Apple Containers rejects Dockerfiles near 16KB with \"Stream unexpectedly closed\" \
       (https://github.com/apple/container/issues/735). Trim comments or split stages — \
       see runtime::APPLE_CONTAINER_DOCKERFILE_SOFT_LIMIT / CONTRIBUTING.md."
    );
  }
}

/// Seven hex digits of HEAD, plus `-dirty` when the tree is not clean. A published crate
/// carries no `.git`, so `.cargo_vcs_info.json` — written by `cargo publish` with the
/// exact release commit, and clean by definition (publish refuses a dirty tree) — is
/// consulted first: it only ever exists in an unpacked crate, where it is authoritative
/// even if some parent directory happens to be an unrelated git checkout. Empty only
/// when neither source knows the commit.
fn git_describe(repo: &Path) -> String {
  if let Ok(v) = std::env::var("SCSH_GIT_DESCRIBE") {
    if !v.is_empty() {
      return v;
    }
  }

  if let Some(sha) = cargo_vcs_info_sha(repo) {
    return sha;
  }

  let hash: String = match git_in(repo, &["rev-parse", "HEAD"]) {
    Some(h) => h.chars().take(7).collect(),
    None => return String::new(),
  };

  if git_dirty(repo) {
    format!("{hash}-dirty")
  } else {
    hash
  }
}

/// The seven-digit `git.sha1` from `.cargo_vcs_info.json`, or `None` when the file is
/// absent (a checkout) or holds no plausible hash. Std-only parse: locate the `"sha1"`
/// key and take the quoted hex string after it.
fn cargo_vcs_info_sha(repo: &Path) -> Option<String> {
  let text = std::fs::read_to_string(repo.join(".cargo_vcs_info.json")).ok()?;
  let after_key = text.split("\"sha1\"").nth(1)?;
  let sha: String = after_key.split('"').nth(1)?.chars().take_while(|c| c.is_ascii_hexdigit()).take(7).collect();
  (sha.len() == 7).then_some(sha)
}

fn git_in(repo: &Path, args: &[&str]) -> Option<String> {
  let out = Command::new("git").arg("-C").arg(repo).args(args).output().ok()?;
  if !out.status.success() {
    return None;
  }
  let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
  (!s.is_empty()).then_some(s)
}

fn git_dirty(repo: &Path) -> bool {
  Command::new("git")
    .arg("-C")
    .arg(repo)
    .args(["status", "--porcelain"])
    .output()
    .ok()
    .filter(|o| o.status.success())
    .is_some_and(|o| !o.stdout.is_empty())
}