Add a version banner with git revision and build metadata (echo --version, startup log, CTCP VERSION)
All checks were successful
CI / check (push) Successful in 5m42s

This commit is contained in:
Jean Chevronnet 2026-07-20 14:42:23 +00:00
parent 58082248ec
commit 61db74875f
No known key found for this signature in database
4 changed files with 119 additions and 1 deletions

View file

@ -12,5 +12,61 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.build_client(false)
.compile_protos(&["proto/echo.proto"], &["proto"])?;
println!("cargo:rerun-if-changed=proto/echo.proto");
emit_build_info();
Ok(())
}
// Capture the git revision and build metadata at compile time and expose them as
// `env!()`-readable vars, so the binary can print a real version banner (its own
// commit hash, build date, toolchain). Degrades gracefully when git is absent
// (a tarball build) — the fields just read "unknown"/empty.
fn emit_build_info() {
use std::process::Command;
let git = |args: &[&str]| {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
};
let hash = git(&["rev-parse", "--short=12", "HEAD"]).unwrap_or_else(|| "unknown".into());
let dirty = git(&["status", "--porcelain"]).map(|s| !s.is_empty()).unwrap_or(false);
let commit_date = git(&["log", "-1", "--format=%cd", "--date=short"]).unwrap_or_default();
// Build time (UTC seconds), honouring SOURCE_DATE_EPOCH for reproducible builds.
let epoch = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
});
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
let rustc_ver = Command::new(rustc)
.arg("--version")
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".into());
println!("cargo:rustc-env=ECHO_GIT_HASH={hash}");
println!("cargo:rustc-env=ECHO_GIT_DIRTY={}", if dirty { "-dirty" } else { "" });
println!("cargo:rustc-env=ECHO_COMMIT_DATE={commit_date}");
println!("cargo:rustc-env=ECHO_BUILD_EPOCH={epoch}");
println!("cargo:rustc-env=ECHO_RUSTC={rustc_ver}");
println!("cargo:rustc-env=ECHO_TARGET={}", std::env::var("TARGET").unwrap_or_default());
// Re-run (refresh the hash) when the checked-out commit moves.
println!("cargo:rerun-if-changed=.git/HEAD");
if let Ok(head) = std::fs::read_to_string(".git/HEAD") {
if let Some(reference) = head.strip_prefix("ref: ").map(str::trim) {
println!("cargo:rerun-if-changed=.git/{reference}");
}
}
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
}

View file

@ -11,7 +11,7 @@ impl Engine {
let cmd = parts.next().unwrap_or("").to_ascii_uppercase();
let arg = parts.next().unwrap_or("");
let response = match cmd.as_str() {
"VERSION" => format!("VERSION echo services {}", env!("CARGO_PKG_VERSION")),
"VERSION" => format!("VERSION {}", crate::version::short()),
"PING" => format!("PING {arg}"),
"TIME" => format!("TIME {}", echo_api::human_time(self.now_secs())),
"CLIENTINFO" => "CLIENTINFO ACTION CLIENTINFO PING TIME VERSION".to_string(),

View file

@ -12,6 +12,7 @@ mod keycard;
mod link;
mod migrate;
mod proto;
mod version;
mod wiktionary;
#[cfg(test)]
mod i18n_check;
@ -57,6 +58,12 @@ async fn main() -> Result<()> {
// One-off migration: `echo import <anope.json> <out.log> [node-name]` builds a
// fresh event log from an Anope db_json and exits, touching nothing else.
let argv: Vec<String> = std::env::args().collect();
// `echo --version` / `-V` prints the build banner (version, git revision, build
// date, toolchain, target) and exits.
if matches!(argv.get(1).map(String::as_str), Some("--version") | Some("-V") | Some("version")) {
println!("{}", version::banner());
return Ok(());
}
if argv.get(1).map(String::as_str) == Some("import") {
let (Some(src), Some(out)) = (argv.get(2), argv.get(3)) else {
eprintln!("usage: echo import <anope.json> <out.log> [node-name]");
@ -88,6 +95,7 @@ async fn main() -> Result<()> {
}
let path = std::env::args().nth(1).unwrap_or_else(|| "config.toml".to_string());
tracing::info!(revision = %version::revision(), built = %version::built(), rustc = version::RUSTC, "starting {}", version::short());
let cfg = config::Config::load(&path)?;
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

54
src/version.rs Normal file
View file

@ -0,0 +1,54 @@
//! Build/version metadata, captured at compile time by `build.rs` and formatted
//! for `echo --version`, the startup log, and the CTCP VERSION reply.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const GIT_HASH: &str = env!("ECHO_GIT_HASH");
pub const GIT_DIRTY: &str = env!("ECHO_GIT_DIRTY"); // "-dirty" or ""
pub const COMMIT_DATE: &str = env!("ECHO_COMMIT_DATE");
pub const RUSTC: &str = env!("ECHO_RUSTC");
pub const TARGET: &str = env!("ECHO_TARGET");
const BUILD_EPOCH: &str = env!("ECHO_BUILD_EPOCH");
fn profile() -> &'static str {
if cfg!(debug_assertions) {
"debug"
} else {
"release"
}
}
/// The build time as "YYYY-MM-DD HH:MM:SS UTC".
pub fn built() -> String {
echo_api::human_time(BUILD_EPOCH.parse().unwrap_or(0))
}
/// `git a1b2c3d4e5f6` (or `…-dirty`), the revision alone.
pub fn revision() -> String {
format!("{GIT_HASH}{GIT_DIRTY}")
}
/// One compact line — for the CTCP VERSION reply and the startup log.
/// `echo 0.0.1 (a1b2c3d4e5f6)`.
pub fn short() -> String {
format!("echo {VERSION} ({})", revision())
}
/// The full multi-line banner for `echo --version`.
pub fn banner() -> String {
let commit = if COMMIT_DATE.is_empty() {
String::new()
} else {
format!(" ({COMMIT_DATE})")
};
format!(
"echo \u{2014} federated IRC services\n \
version: {VERSION}\n \
revision: {rev}{commit}\n \
built: {built} ({profile})\n \
rustc: {RUSTC}\n \
target: {TARGET}",
rev = revision(),
built = built(),
profile = profile(),
)
}