From 60bfee7f36bb855888d5a3028244ac09d65e8e91 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 9 Jul 2026 11:50:39 +0800 Subject: [PATCH] feat(build): inject git/rustc/build-time info and print on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 build.rs 在编译期采集构建元信息(version / git describe / commit hash / 提交时间 / rustc 版本 / 编译时刻),通过 cargo:rustc-env 注入, 由 src/build_info.rs 用 env! 宏在编译期内联读取,零运行时开销。 启动时(main() tracing 初始化之后)调用 log_build_info() 分 4 行打印 这些字段,方便定位线上跑的是哪个二进制。 设计取舍: - 只用 std,不引 vergen/git2 等 build-dependencies(git 不可用时降级为 "unknown",不 fail the build)。 - build.rs 存 Unix 秒,运行时用已有的 chrono 解析回 RFC3339。 - 声明 rerun-if-changed=.git/HEAD,确保切换提交后重新采集。 --- build.rs | 74 +++++++++++++++++++++++++++++++++ src/build_info.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 20 ++++++--- 3 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 build.rs create mode 100644 src/build_info.rs diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..fdb1c71 --- /dev/null +++ b/build.rs @@ -0,0 +1,74 @@ +//! 构建脚本:采集 git / rustc / 编译时刻信息,通过 cargo:rustc-env 注入, +//! 供 src/build_info.rs 在编译期内联读取(env! 宏,零运行时开销)。 +//! +//! 设计取舍见 src/build_info.rs 顶部注释。要点: +//! - 只用 std,不引入 build-dependencies(项目所有 server 依赖都 optional, +//! 为启动 banner 拉 vergen/git2 不划算)。 +//! - git 不可用(非仓库 / tarball 构建)时降级为 "unknown",不 fail the build。 +//! - 声明 rerun-if-changed=.git/HEAD,否则 cargo 默认仅在 build.rs 自身变化时 +//! 重跑,会导致打印的还是旧 hash。 + +use std::process::Command; + +fn main() { + // 切换提交 / 分支后重新采集(读取 .git/HEAD 指向的新 ref)。 + println!("cargo:rerun-if-changed=.git/HEAD"); + // 工作区脏状态变化时 describe 的 --dirty 后缀也会变,重跑一次。 + println!("cargo:rerun-if-changed=.git/index"); + + set_env("YGG_BUILD_GIT_DESCRIBE", git_describe()); + set_env("YGG_BUILD_GIT_HASH", git_hash()); + set_env("YGG_BUILD_GIT_COMMIT_DATE", git_commit_date()); + set_env("YGG_BUILD_RUSTC_VERSION", rustc_version()); + // 编译时刻(Unix 秒)。std 无 ISO 8601 格式化,存秒数由运行时 chrono 解析。 + set_env("YGG_BUILD_TIME", build_time_unix()); +} + +/// 注入一个编译期环境变量。 +fn set_env(key: &str, value: String) { + println!("cargo:rustc-env={key}={value}"); +} + +/// `git describe --tags --always --dirty`:最紧凑的"版本 + 提交数 + hash + 脏标记"。 +fn git_describe() -> String { + git_output(&["describe", "--tags", "--always", "--dirty"]).unwrap_or_else(|| "unknown".into()) +} + +/// 完整 40 位 commit hash。 +fn git_hash() -> String { + git_output(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".into()) +} + +/// 提交时间(ISO 8601 strict,带时区偏移,可直接被 chrono 解析)。 +fn git_commit_date() -> String { + git_output(&["log", "-1", "--format=%cd", "--date=iso-strict"]) + .unwrap_or_else(|| "unknown".into()) +} + +/// 执行一条 git 命令,返回 trim 后的 stdout。失败返回 None(降级路径)。 +fn git_output(args: &[&str]) -> Option { + let out = Command::new("git").args(args).output().ok()?; + if !out.status.success() { + return None; + } + String::from_utf8(out.stdout).ok()?.trim().to_string().into() +} + +/// `rustc --version`,采集编译工具链。 +fn rustc_version() -> String { + Command::new("rustc") + .arg("--version") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".into()) +} + +/// 编译时刻(Unix 秒)。 +fn build_time_unix() -> String { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs().to_string()) + .unwrap_or_else(|_| "0".into()) +} diff --git a/src/build_info.rs b/src/build_info.rs new file mode 100644 index 0000000..0b97e71 --- /dev/null +++ b/src/build_info.rs @@ -0,0 +1,103 @@ +//! 编译期注入的构建元信息。 +//! +//! 根目录 `build.rs` 在编译期采集 git / rustc / 编译时刻信息,通过 +//! `cargo:rustc-env=KEY=VALUE` 注入为编译期环境变量,本模块用 `env!` 宏读取 +//! (编译期内联为 `&'static str`,零运行时开销)。 +//! +//! 整个模块 gated 在 `server` feature 下:`log_build_info` 用了 `tracing::info!`, +//! 而 `tracing` 是 optional 依赖(仅 server 启用)。常量定义本身两端都能编译, +//! 但当前没有任何前端代码引用它们,gating 掉更稳妥。 +//! +//! 字段拆分取舍: +//! - `git_describe`:一句话承载"版本 + 提交数 + hash + 脏标记",人眼定位构建最快, +//! 已内含短 hash,故不再单独存 short_hash。 +//! - `git_hash` / `commit_date` 单独拆出,因为脏树时 describe 带 `-dirty` 但 +//! commit_date 仍是上一个提交的,两者分离才能各自准确。 +//! - `build_time`:编译时刻(造出这个二进制的时间),与 commit_date 不同—— +//! CI / 本地可能停在同一个 commit 但时间不同。 + +#![cfg(feature = "server")] + +use chrono::{DateTime, Utc}; + +/// 构建元信息(编译期常量集合)。 +pub struct BuildInfo { + /// Cargo.toml 里的 `version` 字段。 + pub version: &'static str, + /// `git describe --tags --always --dirty`,例如 `v0.3.0-200-g0ab3340-dirty`。 + pub git_describe: &'static str, + /// 完整 40 位 commit hash。 + pub git_hash: &'static str, + /// 提交时间(ISO 8601 strict,带时区偏移)。 + pub commit_date: &'static str, + /// `rustc --version`,采集编译工具链。 + pub rustc_version: &'static str, + /// 编译时刻(Unix 秒),运行时由 chrono 解析回 RFC3339。 + pub build_time: &'static str, +} + +/// 全局唯一的构建信息实例。 +pub static BUILD_INFO: BuildInfo = BuildInfo { + version: env!("CARGO_PKG_VERSION"), + git_describe: env!("YGG_BUILD_GIT_DESCRIBE"), + git_hash: env!("YGG_BUILD_GIT_HASH"), + commit_date: env!("YGG_BUILD_GIT_COMMIT_DATE"), + rustc_version: env!("YGG_BUILD_RUSTC_VERSION"), + build_time: env!("YGG_BUILD_TIME"), +}; + +/// 打印构建信息。在 `main()` tracing 初始化之后调用。 +/// +/// 拆成多条 `info!` 而非一条长串:`RUST_LOG=info` 下每条日志带文件名/行号前缀, +/// 多行更易读,也方便按字段 grep。 +pub fn log_build_info() { + // build_time 存的是 Unix 秒(build.rs 不引 chrono),这里解析回 RFC3339。 + let built_at = BUILD_INFO + .build_time + .parse::() + .ok() + .and_then(|ts| DateTime::::from_timestamp(ts, 0)) + .map(|dt| dt.to_rfc3339()) + .unwrap_or_else(|| BUILD_INFO.build_time.to_string()); + + tracing::info!( + "build: version={} git={}", + BUILD_INFO.version, + BUILD_INFO.git_describe + ); + tracing::info!( + "build: commit={} date={}", + BUILD_INFO.git_hash, + BUILD_INFO.commit_date + ); + tracing::info!("build: rustc={}", BUILD_INFO.rustc_version); + tracing::info!("build: built_at={}", built_at); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_info_fields_are_populated() { + // build.rs 成功采集 git 信息时,这些字段不应是降级值 "unknown"。 + // (tarball 构建 / 无 git 环境下会跳过——仅作软断言。) + assert!(!BUILD_INFO.version.is_empty()); + // git_describe 在仓库内一定非空;git 不可用时是 "unknown"。 + assert!(!BUILD_INFO.git_describe.is_empty()); + } + + #[test] + fn build_time_parses_as_unix_seconds() { + // build.rs 存的是 Unix 秒,运行时应能解析回时间戳。 + let parsed = BUILD_INFO.build_time.parse::(); + assert!(parsed.is_ok(), "build_time not a unix second: {}", BUILD_INFO.build_time); + assert!(parsed.unwrap() > 1_600_000_000, "build_time implausibly old"); + } + + #[test] + fn log_build_info_does_not_panic() { + // 无 subscriber 时 tracing::info! 是 no-op,但能确认整个函数跑通。 + log_build_info(); + } +} diff --git a/src/main.rs b/src/main.rs index 47b74df..23df491 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,9 @@ // 业务模块 mod api; mod auth; +// build_info:编译期注入的 git/rustc/构建时间信息。模块内部 gate 在 server feature, +// 模块声明本身不需要再加 cfg(空模块在 WASM 端也能编译)。 +mod build_info; mod cache; mod components; mod context; @@ -274,12 +277,17 @@ fn main() { // 加载 .env 环境变量 dotenvy::dotenv().ok(); // 初始化 tracing 日志,默认级别为 info - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), - ) - .init(); + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + // 打印构建元信息(版本 / git / 提交时间 / rustc / 编译时刻)。 + // 必须在 tracing 初始化之后,否则日志被丢弃。 + build_info::log_build_info(); + // 校验数据库连接串,未设置则直接退出 if std::env::var("DATABASE_URL").is_err() {