use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; fn main() { println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA"); println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA_SHORT"); let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let git_dir = find_git_dir(&manifest_dir); // Track the files that determine the current commit. Absolute paths are // required because the build script runs from `packages/cli/`, not the // workspace root where `.git` lives. if let Some(git_dir) = git_dir.as_deref() { emit_rerun(&git_dir.join("HEAD")); emit_rerun(&git_dir.join("packed-refs")); if let Some(target) = head_ref_target(git_dir) { emit_rerun(&git_dir.join(target)); } } emit_rerun(&manifest_dir.join(".cargo_vcs_info.json")); let full_hash = env_nonempty("DIOXUS_CLI_GIT_SHA") .or_else(|| git_dir.as_deref().and_then(read_head_sha)) .or_else(|| read_cargo_vcs_info(&manifest_dir)) .or_else(|| git_cli_sha(&manifest_dir)); if let Some(full_hash) = full_hash { let full_hash = full_hash.trim(); println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}"); let short_hash = env_nonempty("DIOXUS_CLI_GIT_SHA_SHORT") .unwrap_or_else(|| full_hash.chars().take(7).collect()); println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}"); } } fn env_nonempty(key: &str) -> Option { env::var(key).ok().filter(|s| !s.trim().is_empty()) } fn emit_rerun(path: &Path) { println!("cargo:rerun-if-changed={}", path.display()); } /// Walk up from `start` looking for `.git`. Handles the file form used by /// git worktrees and submodules, where `.git` contains `gitdir: `. fn find_git_dir(start: &Path) -> Option { for dir in start.ancestors() { let candidate = dir.join(".git"); let meta = match fs::metadata(&candidate) { Ok(m) => m, Err(_) => continue, }; if meta.is_dir() { return Some(candidate); } if meta.is_file() { let contents = fs::read_to_string(&candidate).ok()?; let pointer = contents .lines() .find_map(|l| l.strip_prefix("gitdir:"))? .trim(); let resolved = if Path::new(pointer).is_absolute() { PathBuf::from(pointer) } else { dir.join(pointer) }; return Some(resolved); } } None } /// If HEAD is a symbolic ref (`ref: refs/heads/foo`), return the target path. fn head_ref_target(git_dir: &Path) -> Option { let head = fs::read_to_string(git_dir.join("HEAD")).ok()?; head.strip_prefix("ref:").map(|s| s.trim().to_owned()) } fn read_head_sha(git_dir: &Path) -> Option { let head = fs::read_to_string(git_dir.join("HEAD")).ok()?; let head = head.trim(); let Some(target) = head.strip_prefix("ref:").map(str::trim) else { // Detached HEAD: the file holds the SHA directly. return is_sha(head).then(|| head.to_owned()); }; // Loose ref file. if let Ok(sha) = fs::read_to_string(git_dir.join(target)) { let sha = sha.trim(); if is_sha(sha) { return Some(sha.to_owned()); } } // Packed refs (after `git gc` / shallow clones, refs only live here). let packed = fs::read_to_string(git_dir.join("packed-refs")).ok()?; for line in packed.lines() { if line.starts_with('#') || line.starts_with('^') || line.is_empty() { continue; } let (sha, name) = line.split_once(' ')?; if name == target && is_sha(sha) { return Some(sha.to_owned()); } } None } /// Cargo writes `.cargo_vcs_info.json` next to `Cargo.toml` when publishing, /// recording the source SHA at publish time. This is how `cargo install` /// from crates.io can still report a real commit despite the `.crate` /// tarball having no `.git/`. fn read_cargo_vcs_info(manifest_dir: &Path) -> Option { let contents = fs::read_to_string(manifest_dir.join(".cargo_vcs_info.json")).ok()?; // Cheap targeted parse so we don't pull serde into the build script. // The file is generated by cargo and the field name is stable. let after = contents.split("\"sha1\"").nth(1)?; let sha = after.split('"').nth(1)?.trim(); is_sha(sha).then(|| sha.to_owned()) } fn git_cli_sha(manifest_dir: &Path) -> Option { // Scope `git` to the manifest dir so we never accidentally pick up a // SHA from a parent repo (e.g. someone's `~` is a git repo). let output = Command::new("git") .arg("-C") .arg(manifest_dir) .args(["rev-parse", "HEAD"]) .output() .ok()?; if !output.status.success() { return None; } let sha = String::from_utf8(output.stdout).ok()?.trim().to_owned(); is_sha(&sha).then_some(sha) } fn is_sha(s: &str) -> bool { matches!(s.len(), 40 | 64) && s.chars().all(|c| c.is_ascii_hexdigit()) }