use std::path::{Path, PathBuf};
use std::process::Command;
use std::string::{String, ToString};
use std::vec::Vec;
use std::{env, fs, println};
use sha2::{Digest, Sha256};
pub const UNKNOWN: &str = "unknown";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Capture {
pub git: String,
pub dirty: String,
pub lock: String,
pub watch: Vec<PathBuf>,
}
impl Capture {
pub fn directives(&self) -> Vec<String> {
let mut lines = Vec::with_capacity(3 + self.watch.len());
lines.push(std::format!(
"cargo:rustc-env=KICKSTART_BUILD_GIT={}",
self.git
));
lines.push(std::format!(
"cargo:rustc-env=KICKSTART_BUILD_DIRTY={}",
self.dirty
));
lines.push(std::format!(
"cargo:rustc-env=KICKSTART_BUILD_LOCK={}",
self.lock
));
for path in &self.watch {
lines.push(std::format!("cargo:rerun-if-changed={}", path.display()));
}
lines
}
}
pub fn emit() {
let manifest = env::var_os("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR is unset: call emit() from a build script");
for line in capture(Path::new(&manifest)).directives() {
println!("{line}");
}
}
pub fn capture(manifest_dir: &Path) -> Capture {
let mut watch = Vec::new();
let lock = match find_lock(manifest_dir) {
Some(path) => {
let hash = fs::read(&path).map(|bytes| lock_hash(&bytes));
watch.push(path);
hash.unwrap_or_else(|_| UNKNOWN.to_string())
}
None => UNKNOWN.to_string(),
};
let ask = |args: &[&str]| git(manifest_dir, args);
let (commit, dirty) = match ask(&["rev-parse", "--short", "HEAD"]) {
Some(commit) => {
let dirty = match ask(&[
"--no-optional-locks",
"status",
"--porcelain",
"--untracked-files=no",
]) {
Some(status) => (!status.is_empty()).to_string(),
None => UNKNOWN.to_string(),
};
(commit, dirty)
}
None => (UNKNOWN.to_string(), UNKNOWN.to_string()),
};
if commit != UNKNOWN {
watch.extend(git_inputs(manifest_dir));
}
watch.retain(|path| path.exists());
Capture {
git: commit,
dirty,
lock,
watch,
}
}
pub fn lock_hash(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
digest[..4]
.iter()
.map(|b| std::format!("{b:02x}"))
.collect()
}
pub fn find_lock(manifest_dir: &Path) -> Option<PathBuf> {
manifest_dir
.ancestors()
.map(|dir| dir.join("Cargo.lock"))
.find(|path| path.is_file())
}
fn git_inputs(dir: &Path) -> Vec<PathBuf> {
let mut inputs = Vec::new();
let git_dir = git(dir, &["rev-parse", "--absolute-git-dir"]).map(PathBuf::from);
let common_dir = git(
dir,
&["rev-parse", "--path-format=absolute", "--git-common-dir"],
)
.map(PathBuf::from);
if let Some(git_dir) = &git_dir {
inputs.push(git_dir.join("HEAD"));
inputs.push(git_dir.join("index"));
}
if let Some(common_dir) = common_dir.as_ref().or(git_dir.as_ref()) {
inputs.push(common_dir.join("packed-refs"));
if let Some(branch) = git(dir, &["symbolic-ref", "-q", "HEAD"]) {
inputs.push(common_dir.join(branch));
}
}
if let Some(top) = git(dir, &["rev-parse", "--show-toplevel"]).map(PathBuf::from)
&& let Some(files) = git(&top, &["ls-files", "-z"])
{
inputs.extend(
files
.split('\0')
.filter(|file| !file.is_empty())
.map(|file| top.join(file)),
);
}
inputs
}
fn git(dir: &Path, args: &[&str]) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|text| text.trim_end().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_lock_hash_is_sha256s_first_eight_digits() {
assert_eq!(lock_hash(b""), "e3b0c442");
assert_eq!(lock_hash(b"abc"), "ba7816bf");
}
#[test]
fn directives_use_the_names_build_info_reads() {
let capture = Capture {
git: "a1b2c3d".to_string(),
dirty: "false".to_string(),
lock: "9f8e7d6c".to_string(),
watch: std::vec![PathBuf::from("/x/Cargo.lock")],
};
assert_eq!(
capture.directives(),
[
"cargo:rustc-env=KICKSTART_BUILD_GIT=a1b2c3d",
"cargo:rustc-env=KICKSTART_BUILD_DIRTY=false",
"cargo:rustc-env=KICKSTART_BUILD_LOCK=9f8e7d6c",
"cargo:rerun-if-changed=/x/Cargo.lock",
]
);
}
#[test]
fn nothing_found_is_unknown() {
let temp = env::temp_dir();
if find_lock(&temp).is_some() || git(&temp, &["rev-parse"]).is_some() {
return;
}
let dir = temp.join(std::format!("kickstart-build-info-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let capture = capture(&dir);
fs::remove_dir_all(&dir).unwrap();
assert_eq!(capture.git, UNKNOWN);
assert_eq!(capture.dirty, UNKNOWN);
assert_eq!(capture.lock, UNKNOWN);
assert!(capture.watch.is_empty());
}
#[test]
fn this_crate_is_captured() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let capture = capture(manifest);
let lock = manifest.join("Cargo.lock");
assert_eq!(capture.lock, lock_hash(&fs::read(&lock).unwrap()));
assert!(capture.watch.contains(&lock));
if capture.git != UNKNOWN {
assert!(capture.git.chars().all(|c| c.is_ascii_hexdigit()));
assert!(capture.dirty == "true" || capture.dirty == "false");
assert!(capture.watch.contains(&manifest.join("Cargo.toml")));
}
}
}