use std::{env, fs, io, path::Path, path::PathBuf, process::Command};
fn git(repo_dir: &Path, args: &[&str]) -> Option<String> {
Command::new("git")
.arg("-C")
.arg(repo_dir)
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn git_ok(repo_dir: &Path, args: &[&str]) -> Option<bool> {
Command::new("git")
.arg("-C")
.arg(repo_dir)
.args(args)
.output()
.ok()
.map(|o| o.status.success())
}
fn find_repo_root(manifest_dir: &Path) -> Option<PathBuf> {
let toplevel = git(manifest_dir, &["rev-parse", "--show-toplevel"])?;
let root = PathBuf::from(&toplevel);
let cargo_toml = root.join("Cargo.toml");
let pkg_name = env::var("CARGO_PKG_NAME").unwrap_or_default();
match fs::read_to_string(&cargo_toml) {
Ok(contents) => {
let quoted_name = format!("\"{}\"", pkg_name);
let is_ours = contents.lines().any(|line| {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("name") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
return rest.trim().starts_with("ed_name);
}
}
false
});
if !is_ours {
return None;
}
}
Err(_) => return None,
}
Some(root)
}
fn write_if_changed(path: &Path, contents: &[u8]) -> io::Result<()> {
if let Ok(existing) = fs::read(path)
&& existing == contents
{
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)
}
fn opt_str(v: &Option<String>) -> String {
match v {
Some(s) => format!("Some({:?})", s),
None => "None".to_string(),
}
}
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let repo_root = find_repo_root(&manifest_dir);
if let Some(ref root) = repo_root {
let git_dir = git(root, &["rev-parse", "--git-dir"])
.map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
root.join(path)
}
})
.unwrap_or_else(|| root.join(".git"));
for name in &["HEAD", "packed-refs", "refs/heads", "refs/tags"] {
let path = git_dir.join(name);
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
let git_entry = root.join(".git");
if git_entry.is_file() {
println!("cargo:rerun-if-changed={}", git_entry.display());
}
} else {
println!("cargo:rerun-if-changed=build.rs");
}
let (git_describe, git_sha, git_sha_short, git_branch, git_dirty, git_commit_year) =
match repo_root {
Some(ref root) => (
git(root, &["describe", "--tags", "--dirty"]),
git(root, &["rev-parse", "HEAD"]),
git(root, &["rev-parse", "--short", "HEAD"]),
git(root, &["symbolic-ref", "--short", "-q", "HEAD"]),
git_ok(root, &["diff-index", "--quiet", "HEAD", "--"]).map(|clean| !clean),
git(root, &["show", "-s", "--format=%as", "HEAD"])
.and_then(|s| s.split('-').next().map(str::to_string)),
),
None => (None, None, None, None, None, None),
};
let build_year: u16 = git_commit_year
.as_deref()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
(1970 + secs / 31_557_600) as u16
});
let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
let version_string = if let Some(ref desc) = git_describe {
desc.clone()
} else if let Some(ref sha) = git_sha_short {
let dirty_suffix = if git_dirty == Some(true) {
".dirty"
} else {
""
};
format!("{pkg_version}+{sha}{dirty_suffix}")
} else {
pkg_version.clone()
};
println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let dest = out_dir.join("build_info.rs");
let version_string_lit = format!("{:?}", version_string);
let pkg_version_lit = format!("{:?}", pkg_version);
let git_describe_lit = opt_str(&git_describe);
let git_sha_lit = opt_str(&git_sha);
let git_sha_short_lit = opt_str(&git_sha_short);
let git_branch_lit = opt_str(&git_branch);
let git_dirty_lit = match git_dirty {
Some(b) => format!("Some({b})"),
None => "None".to_string(),
};
let rs = format!(
r#"// @generated by build.rs — do not edit.
/// Composite version string for display. Prefers `git describe` output
/// when available, falls back to `CARGO_PKG_VERSION` + short SHA, or just
/// `CARGO_PKG_VERSION` if no git info is available at all.
pub const VERSION: &str = {version_string_lit};
/// Cargo package version from Cargo.toml (always present).
pub const PKG_VERSION: &str = {pkg_version_lit};
/// Full output of `git describe --tags --dirty --always`, if available.
/// Examples: "v0.3.1", "v0.3.1-7-gabcdef1", "v0.3.1-dirty", "abcdef1".
pub const GIT_DESCRIBE: Option<&str> = {git_describe_lit};
/// Full commit SHA, if available.
pub const GIT_SHA: Option<&str> = {git_sha_lit};
/// Abbreviated commit SHA, if available.
pub const GIT_SHA_SHORT: Option<&str> = {git_sha_short_lit};
/// Current branch name, if on a branch (None for detached HEAD).
pub const GIT_BRANCH: Option<&str> = {git_branch_lit};
/// Whether the working tree had uncommitted changes at build time.
pub const GIT_DIRTY: Option<bool> = {git_dirty_lit};
/// Year for copyright notices. From the git commit date when available,
/// otherwise the calendar year at build time.
pub const BUILD_YEAR: u16 = {build_year};
"#,
);
write_if_changed(&dest, rs.as_bytes()).expect("failed to write build_info.rs");
}