use cargo_athena::api::munge;
use std::process::{Command, exit};
const RELEASE_BRANCHES: &[&str] = &["main", "master"];
pub struct BuildTag {
pub tag: String,
pub channel: &'static str,
pub commit: Option<String>,
pub dirty: bool,
}
struct GitState {
branch: String,
commit: String,
dirty: bool,
}
fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn git_state() -> Option<GitState> {
if git(&["rev-parse", "--is-inside-work-tree"]).as_deref() != Some("true") {
return None;
}
Some(GitState {
branch: git(&["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_default(),
commit: git(&["rev-parse", "--short", "HEAD"]).unwrap_or_default(),
dirty: git(&["status", "--porcelain"])
.map(|s| !s.is_empty())
.unwrap_or(false),
})
}
fn channel_of(tag: &str) -> &'static str {
munge::channel_of(tag)
}
pub fn export_source_build_tag() {
if std::env::var_os("ATHENA_VERSION_TAG").is_some_and(|v| !v.is_empty()) {
return;
}
let Some(st) = git_state() else { return };
let off_road = st.dirty || !RELEASE_BRANCHES.contains(&st.branch.as_str());
if !off_road {
return;
}
let slot = if st.commit.is_empty() {
"local".to_string()
} else {
st.commit.clone()
};
let tag = format!("{}{}", munge::DEV_PREFIX, munge::version_tag(&slot));
unsafe {
std::env::set_var("ATHENA_VERSION_TAG", &tag);
if !st.commit.is_empty() {
std::env::set_var("ATHENA_GIT_COMMIT", &st.commit);
}
if st.dirty {
std::env::set_var("ATHENA_GIT_DIRTY", "true");
}
}
}
pub fn export_dev_tag(flag: Option<Option<String>>) {
let st = git_state();
let slot = match flag {
None => return,
Some(Some(v)) => v,
Some(None) => st
.as_ref()
.map(|s| s.commit.clone())
.filter(|c| !c.is_empty())
.unwrap_or_default(),
};
let slot = match munge::version_tag(&slot) {
s if s.is_empty() => "local".to_string(),
s => s,
};
unsafe {
std::env::set_var("ATHENA_VERSION_TAG", format!("{}{slot}", munge::DEV_PREFIX));
if let Some(s) = &st {
if !s.commit.is_empty() {
std::env::set_var("ATHENA_GIT_COMMIT", &s.commit);
}
if s.dirty {
std::env::set_var("ATHENA_GIT_DIRTY", "true");
}
}
}
}
pub fn resolve(
semver: &str,
dev_tag: Option<Option<String>>,
allow_dirty: bool,
yes: bool,
gate: bool,
) -> BuildTag {
let st = git_state();
let commit_of = |st: &Option<GitState>| {
st.as_ref()
.map(|s| s.commit.clone())
.filter(|c| !c.is_empty())
};
if let Ok(t) = std::env::var("ATHENA_VERSION_TAG")
&& !t.is_empty()
{
let tag = munge::version_tag(&t);
if tag.is_empty() {
eprintln!(
"error: ATHENA_VERSION_TAG={t:?} normalizes to an empty tag \
(no [a-z0-9] characters)."
);
exit(2);
}
return BuildTag {
channel: channel_of(&tag),
tag,
commit: commit_of(&st),
dirty: st.as_ref().map(|s| s.dirty).unwrap_or(false),
};
}
let dirty = st.as_ref().map(|s| s.dirty).unwrap_or(false);
let on_release = st
.as_ref()
.map(|s| RELEASE_BRANCHES.contains(&s.branch.as_str()))
.unwrap_or(true);
let commit = commit_of(&st);
if gate && dirty && !allow_dirty {
eprintln!(
"error: working tree has uncommitted changes that would be baked \
into the binary.\n Commit them for a clean build, or pass \
--allow-dirty to build a dev version."
);
exit(2);
}
let is_release = on_release && !dirty && dev_tag.is_none();
if is_release {
return BuildTag {
tag: munge::version_tag(semver),
channel: "release",
commit,
dirty,
};
}
if gate
&& let Some(state) = &st
&& !on_release
&& !yes
{
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
use std::io::Write;
eprint!(
"warning: not on a release branch (on {:?}); this builds a DEV \
version, not a clean semver release.\nProceed? [y/N] ",
state.branch
);
let _ = std::io::stderr().flush();
let mut s = String::new();
std::io::stdin().read_line(&mut s).ok();
if !matches!(s.trim(), "y" | "Y" | "yes" | "Yes") {
eprintln!("aborted.");
exit(1);
}
} else {
eprintln!(
"warning: not on a release branch (on {:?}); building a DEV \
version (non-interactive — pass --yes to silence).",
state.branch
);
}
}
let slot_raw = match dev_tag {
Some(Some(v)) => v,
_ => commit.clone().unwrap_or_default(),
};
let slot = {
let s = munge::version_tag(&slot_raw);
if s.is_empty() {
commit
.clone()
.map(|c| munge::version_tag(&c))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "local".to_string())
} else {
s
}
};
BuildTag {
tag: format!("{}{slot}", munge::DEV_PREFIX),
channel: "dev",
commit,
dirty,
}
}