use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
struct GitState {
tag: Option<(String, u32)>,
hash: String,
dirty: bool,
}
fn main() {
let version = env::var("CARGO_PKG_VERSION").unwrap_or_default();
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let release_tag = format!("v{version}");
println!("cargo:rerun-if-env-changed=TRAIL_RELEASE");
for path in ["src", "Cargo.toml", "Cargo.lock"] {
let path = manifest_dir.join(path);
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
let declared_release = env::var("TRAIL_RELEASE").is_ok_and(|tag| tag == release_tag);
let (label, note) = match git_state(&manifest_dir) {
Some(git) if declared_release && git.tag == Some((release_tag.clone(), 0)) => {
(version.clone(), None)
}
Some(git) => describe(&version, &release_tag, &git),
None if declared_release => (version.clone(), None),
None if manifest_dir.join(".git").exists() => (
format!("{version}-dev"),
Some("dev build — commit unknown, git could not be run".to_owned()),
),
None => (version.clone(), None),
};
if declared_release && note.is_some() {
println!("cargo:warning=TRAIL_RELEASE={release_tag} ignored: HEAD is not that tag");
}
let kind = if note.is_none() { "release" } else { "dev" };
let long = match ¬e {
Some(note) => format!("{label} ({note})"),
None => label.clone(),
};
println!("cargo:rustc-env=TRAIL_VERSION={label}");
println!("cargo:rustc-env=TRAIL_LONG_VERSION={long}");
println!("cargo:rustc-env=TRAIL_BUILD_KIND={kind}");
}
fn describe(version: &str, release_tag: &str, git: &GitState) -> (String, Option<String>) {
let dirty = if git.dirty { ".dirty" } else { "" };
let changes = if git.dirty {
", with uncommitted changes"
} else {
""
};
match &git.tag {
Some((tag, 0)) if tag == release_tag && !git.dirty => (version.to_owned(), None),
Some((tag, count)) if tag == release_tag => (
format!("{version}-dev.{count}+{}{dirty}", git.hash),
Some(format!(
"dev build — {count} commit{} past {tag}{changes}",
if *count == 1 { "" } else { "s" }
)),
),
Some((tag, count)) => (
format!("{version}-dev+{}{dirty}", git.hash),
Some(format!(
"dev build — v{version} is not tagged; {count} commit{} past {tag}{changes}",
if *count == 1 { "" } else { "s" }
)),
),
None => (
format!("{version}-dev+{}{dirty}", git.hash),
Some(format!("dev build — no release tag found{changes}")),
),
}
}
fn git_state(manifest_dir: &Path) -> Option<GitState> {
let toplevel = git(manifest_dir, &["rev-parse", "--show-toplevel"])?;
if Path::new(&toplevel).canonicalize().ok()? != manifest_dir.canonicalize().ok()? {
return None;
}
watch_git(manifest_dir);
let described = git(
manifest_dir,
&[
"describe",
"--tags",
"--long",
"--dirty",
"--always",
"--abbrev=7",
"--match",
"v[0-9]*",
],
)?;
let (described, dirty) = match described.strip_suffix("-dirty") {
Some(rest) => (rest, true),
None => (described.as_str(), false),
};
let mut parts = described.rsplitn(3, '-');
let tagged = match (parts.next(), parts.next(), parts.next()) {
(Some(hash), Some(count), Some(tag)) => hash
.strip_prefix('g')
.zip(count.parse::<u32>().ok())
.map(|(hash, count)| (tag.to_owned(), count, hash.to_owned())),
_ => None,
};
Some(match tagged {
Some((tag, count, hash)) => GitState {
tag: Some((tag, count)),
hash,
dirty,
},
None => GitState {
tag: None,
hash: described.to_owned(),
dirty,
},
})
}
fn watch_git(manifest_dir: &Path) {
let git_dir = git(manifest_dir, &["rev-parse", "--absolute-git-dir"]);
let common_dir =
git(manifest_dir, &["rev-parse", "--git-common-dir"]).map(|dir| manifest_dir.join(dir));
let mut watched: Vec<PathBuf> = Vec::new();
if let Some(git_dir) = git_dir.map(PathBuf::from) {
watched.push(git_dir.join("HEAD"));
watched.push(git_dir.join("index"));
}
if let Some(common_dir) = common_dir {
watched.push(common_dir.join("refs"));
watched.push(common_dir.join("packed-refs"));
}
for path in watched.iter().filter(|path| path.exists()) {
println!("cargo:rerun-if-changed={}", path.display());
}
}
fn git(dir: &Path, args: &[&str]) -> Option<String> {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.env("GIT_OPTIONAL_LOCKS", "0")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
Some(text.trim().to_owned())
}