mod files;
mod git;
use std::path::{Path, PathBuf};
pub(crate) use files::{CrateFiles, VcsInfo, hash_sources};
pub(crate) use git::Repository;
#[cfg(test)]
pub(crate) use git::{echoes_path_format, is_date};
use files::nearest_lockfile;
const ENGINE_PACKAGES: [&str; 4] = ["henad-core", "henad-build", "henad-compute", "henad-explore"];
const ENGINE_PREFIX: &str = "crates/henad-explore/";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StampScope {
Commit,
Engine,
SourceHash,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Stamp {
pub(crate) commit: String,
pub(crate) commit_date: String,
pub(crate) dirty: Option<bool>,
pub(crate) source_hash: Option<u64>,
pub(crate) crate_hash: Option<u64>,
pub(crate) watched: Vec<PathBuf>,
}
impl Stamp {
pub(crate) fn of(crate_dir: &Path, scope: StampScope) -> Self {
let own = CrateFiles::new(crate_dir);
if let Some(info) = VcsInfo::read(crate_dir) {
return Self::packaged(&own, &info, scope);
}
let repository = Repository::find(crate_dir);
match scope {
StampScope::Commit => {
if let Some(repository) = &repository {
return Self::from_git(repository, &[own]);
}
let mut stamp = Self::untracked(&own, None, true);
if !own.is_package()
&& let Some(enclosing) = Repository::enclosing(crate_dir)
{
stamp.watched.extend(enclosing.watched());
}
stamp
}
StampScope::Engine => {
let mut stamp = match repository.as_ref().filter(|found| found.prefix() == ENGINE_PREFIX) {
Some(henad) => {
let packages: Vec<CrateFiles> = ENGINE_PACKAGES
.iter()
.map(|package| CrateFiles::in_repository(henad, package))
.collect();
Self::from_git(henad, &packages)
}
None => Self::untracked(&own, repository.as_ref(), true),
};
stamp.crate_hash = hash_sources(own.sources(repository.as_ref()));
stamp
}
StampScope::SourceHash => Self::untracked(&own, repository.as_ref(), false),
}
}
fn packaged(own: &CrateFiles, info: &VcsInfo, scope: StampScope) -> Self {
let source_hash = hash_sources(own.sources(None));
Self {
commit: info.short_commit().to_owned(),
commit_date: String::new(),
dirty: Some(info.dirty),
source_hash,
crate_hash: source_hash.filter(|_| scope == StampScope::Engine),
watched: own.watched(),
}
}
fn from_git(repository: &Repository, packages: &[CrateFiles]) -> Self {
let mut sources = Vec::new();
let mut watched = repository.watched();
let mut pathspecs = Vec::new();
for package in packages {
sources.extend(package.sources(Some(repository)));
watched.extend(package.watched());
pathspecs.extend(package.pathspecs(repository));
}
let mut lockfile_tracked = true;
if let Some(lockfile) = nearest_lockfile(repository.directory()) {
match repository.tracked(&lockfile) {
Some(pathspec) => pathspecs.push(pathspec),
None => lockfile_tracked = false,
}
sources.push(("Cargo.lock".to_owned(), lockfile.clone()));
watched.push(lockfile);
}
let commit = repository.commit();
let dirty = if commit.is_empty() {
None
} else {
match repository.dirty(&pathspecs) {
Some(false) if !lockfile_tracked => None,
dirty => dirty,
}
};
Self {
commit_date: repository.commit_date(),
commit,
dirty,
source_hash: hash_sources(sources),
crate_hash: None,
watched,
}
}
fn untracked(own: &CrateFiles, repository: Option<&Repository>, lockfile: bool) -> Self {
let mut sources = own.sources(repository);
let mut watched = own.watched();
if lockfile
&& !own.is_package()
&& let Some(lockfile) = nearest_lockfile(own.directory())
{
sources.push(("Cargo.lock".to_owned(), lockfile.clone()));
watched.push(lockfile);
}
Self {
commit: String::new(),
commit_date: String::new(),
dirty: None,
source_hash: hash_sources(sources),
crate_hash: None,
watched,
}
}
pub(crate) fn cargo_lines(&self, scope: StampScope) -> Vec<String> {
let hex = |hash: Option<u64>| hash.map(|hash| format!("{hash:016x}")).unwrap_or_default();
let dirty = self.dirty.map(|dirty| dirty.to_string()).unwrap_or_default();
let mut lines = vec![
format!("cargo:rustc-env=HENAD_BUILD_COMMIT={}", self.commit),
format!("cargo:rustc-env=HENAD_BUILD_COMMIT_DATE={}", self.commit_date),
format!("cargo:rustc-env=HENAD_BUILD_DIRTY={dirty}"),
format!("cargo:rustc-env=HENAD_BUILD_SOURCE_HASH={}", hex(self.source_hash)),
];
if scope == StampScope::Engine {
lines.push(format!(
"cargo:rustc-env=HENAD_BUILD_CRATE_HASH={}",
hex(self.crate_hash)
));
lines.push(format!(
"cargo:rustc-env=HENAD_BUILD_STAMP_VERSION={}",
env!("CARGO_PKG_VERSION")
));
}
lines.extend(
self.watched
.iter()
.map(|path| format!("cargo:rerun-if-changed={}", path.display())),
);
lines
}
}
#[expect(clippy::print_stdout, reason = "a build script talks to Cargo through stdout")]
pub(crate) fn print(scope: StampScope) {
let Some(crate_dir) = std::env::var_os("CARGO_MANIFEST_DIR") else {
println!("cargo:warning=`CARGO_MANIFEST_DIR` is not set. Call the stamp from a build script");
return;
};
for line in Stamp::of(Path::new(&crate_dir), scope).cargo_lines(scope) {
println!("{line}");
}
}