Skip to main content

yggdrasil/
build_info.rs

1//! 编译期注入的构建元信息。
2//!
3//! 根目录 `build.rs` 在编译期采集 git / rustc / 编译时刻信息,通过
4//! `cargo:rustc-env=KEY=VALUE` 注入为编译期环境变量,本模块用 `env!` 宏读取
5//! (编译期内联为 `&'static str`,零运行时开销)。
6//!
7//! 整个模块 gated 在 `server` feature 下:`log_build_info` 用了 `tracing::info!`,
8//! 而 `tracing` 是 optional 依赖(仅 server 启用)。常量定义本身两端都能编译,
9//! 但当前没有任何前端代码引用它们,gating 掉更稳妥。
10//!
11//! 字段拆分取舍:
12//! - `git_describe`:一句话承载"版本 + 提交数 + hash + 脏标记",人眼定位构建最快,
13//!   已内含短 hash,故不再单独存 short_hash。
14//! - `git_hash` / `commit_date` 单独拆出,因为脏树时 describe 带 `-dirty` 但
15//!   commit_date 仍是上一个提交的,两者分离才能各自准确。
16//! - `build_time`:编译时刻(造出这个二进制的时间),与 commit_date 不同——
17//!   CI / 本地可能停在同一个 commit 但时间不同。
18
19#![cfg(feature = "server")]
20
21use chrono::{DateTime, Utc};
22
23/// 构建元信息(编译期常量集合)。
24pub struct BuildInfo {
25    /// Cargo.toml 里的 `version` 字段。
26    pub version: &'static str,
27    /// `git describe --tags --always --dirty`,例如 `v0.3.0-200-g0ab3340-dirty`。
28    pub git_describe: &'static str,
29    /// 完整 40 位 commit hash。
30    pub git_hash: &'static str,
31    /// 提交时间(ISO 8601 strict,带时区偏移)。
32    pub commit_date: &'static str,
33    /// `rustc --version`,采集编译工具链。
34    pub rustc_version: &'static str,
35    /// 编译时刻(Unix 秒),运行时由 chrono 解析回 RFC3339。
36    pub build_time: &'static str,
37}
38
39/// 全局唯一的构建信息实例。
40pub static BUILD_INFO: BuildInfo = BuildInfo {
41    version: env!("CARGO_PKG_VERSION"),
42    git_describe: env!("YGG_BUILD_GIT_DESCRIBE"),
43    git_hash: env!("YGG_BUILD_GIT_HASH"),
44    commit_date: env!("YGG_BUILD_GIT_COMMIT_DATE"),
45    rustc_version: env!("YGG_BUILD_RUSTC_VERSION"),
46    build_time: env!("YGG_BUILD_TIME"),
47};
48
49/// 打印构建信息。在 `main()` tracing 初始化之后调用。
50///
51/// 拆成多条 `info!` 而非一条长串:`RUST_LOG=info` 下每条日志带文件名/行号前缀,
52/// 多行更易读,也方便按字段 grep。
53pub fn log_build_info() {
54    // build_time 存的是 Unix 秒(build.rs 不引 chrono),这里解析回 RFC3339。
55    let built_at = BUILD_INFO
56        .build_time
57        .parse::<i64>()
58        .ok()
59        .and_then(|ts| DateTime::<Utc>::from_timestamp(ts, 0))
60        .map(|dt| dt.to_rfc3339())
61        .unwrap_or_else(|| BUILD_INFO.build_time.to_string());
62
63    tracing::info!(
64        "build: version={} git={}",
65        BUILD_INFO.version,
66        BUILD_INFO.git_describe
67    );
68    tracing::info!(
69        "build: commit={} date={}",
70        BUILD_INFO.git_hash,
71        BUILD_INFO.commit_date
72    );
73    tracing::info!("build: rustc={}", BUILD_INFO.rustc_version);
74    tracing::info!("build: built_at={}", built_at);
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn build_info_fields_are_populated() {
83        // build.rs 成功采集 git 信息时,这些字段不应是降级值 "unknown"。
84        // (tarball 构建 / 无 git 环境下会跳过——仅作软断言。)
85        assert!(!BUILD_INFO.version.is_empty());
86        // git_describe 在仓库内一定非空;git 不可用时是 "unknown"。
87        assert!(!BUILD_INFO.git_describe.is_empty());
88    }
89
90    #[test]
91    fn build_time_parses_as_unix_seconds() {
92        // build.rs 存的是 Unix 秒,运行时应能解析回时间戳。
93        let parsed = BUILD_INFO.build_time.parse::<i64>();
94        assert!(
95            parsed.is_ok(),
96            "build_time not a unix second: {}",
97            BUILD_INFO.build_time
98        );
99        assert!(
100            parsed.unwrap() > 1_600_000_000,
101            "build_time implausibly old"
102        );
103    }
104
105    #[test]
106    fn log_build_info_does_not_panic() {
107        // 无 subscriber 时 tracing::info! 是 no-op,但能确认整个函数跑通。
108        log_build_info();
109    }
110}