treetop_core/
build_info.rs1use serde::Serialize;
2use std::sync::OnceLock;
3
4#[cfg(test)]
5#[path = "../build_support.rs"]
6mod build_support;
7
8#[derive(Debug, Clone, Serialize)]
9pub struct BuildInfo {
10 pub crate_name: &'static str,
11 pub crate_url: &'static str,
12 pub crate_version: &'static str,
13 pub version: String,
14 pub git: Option<GitInfo>,
15 pub rustc_semver: Option<&'static str>,
16 pub target_triple: Option<&'static str>,
17 pub profile: Option<&'static str>,
18 pub build_unix: Option<i64>,
19 pub cedar_version: &'static str,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct GitInfo {
25 pub describe: &'static str,
26 pub branch: &'static str,
27 pub sha: &'static str,
28 pub dirty: bool,
29}
30
31static CELL: OnceLock<BuildInfo> = OnceLock::new();
32
33pub fn build_info() -> &'static BuildInfo {
34 CELL.get_or_init(|| {
35 let pkg_name = env!("CARGO_PKG_NAME");
36 let pkg_ver = env!("CARGO_PKG_VERSION");
37 let cedar_version = option_env!("TREETOP_CEDAR_VERSION").unwrap_or("unknown");
38
39 let crate_url = option_env!("CARGO_PKG_REPOSITORY").unwrap_or("");
40
41 let describe = option_env!("TREETOP_GIT_DESCRIBE").unwrap_or("");
42 let sha = option_env!("TREETOP_GIT_SHA").unwrap_or("");
43 let branch = option_env!("TREETOP_GIT_BRANCH").unwrap_or("");
44 let dirty = option_env!("TREETOP_GIT_DIRTY").unwrap_or("false") == "true";
45
46 let version = format_human_version(pkg_ver, describe, dirty);
47 let build_unix = option_env!("TREETOP_BUILD_UNIX").and_then(|s| s.parse().ok());
48
49 let git = if !describe.is_empty() {
50 Some(GitInfo {
51 describe,
52 sha,
53 branch,
54 dirty,
55 })
56 } else {
57 None
58 };
59
60 BuildInfo {
61 crate_name: pkg_name,
62 crate_url,
63 crate_version: pkg_ver,
64 version,
65 git,
66 rustc_semver: option_env!("TREETOP_RUSTC_SEMVER"),
67 target_triple: option_env!("TREETOP_TARGET_TRIPLE"),
68 profile: option_env!("TREETOP_PROFILE"),
69 build_unix,
70 cedar_version,
71 }
72 })
73}
74
75pub fn format_human_version(pkg_ver: &str, git_describe_input: &str, git_dirty: bool) -> String {
76 if git_describe_input.is_empty() {
77 return pkg_ver.to_string();
79 }
80 let mut git_describe = git_describe_input;
81 if git_describe.ends_with("-dirty") {
82 git_describe = &git_describe[..git_describe.len() - "-dirty".len()];
83 }
84 let dirty = if git_dirty { "-dirty" } else { "" };
85
86 fn looks_like_sha(s: &str) -> bool {
87 let s = s.strip_prefix('g').unwrap_or(s);
88 s.len() >= 7 && s.chars().all(|c| c.is_ascii_hexdigit())
89 }
90
91 let parts: Vec<&str> = git_describe.split('-').collect();
92 if parts.len() >= 3 {
93 let maybe_dist = parts[parts.len() - 2];
94 let maybe_sha = parts[parts.len() - 1];
95 if maybe_dist.parse::<u64>().is_ok() && looks_like_sha(maybe_sha) {
96 let tag = parts[..parts.len() - 2].join("-");
97 let short = maybe_sha.strip_prefix('g').unwrap_or(maybe_sha);
98 return format!("{tag}+{maybe_dist}.g{short}{dirty}");
99 }
100 }
101
102 if looks_like_sha(git_describe) {
103 let short = git_describe.strip_prefix('g').unwrap_or(git_describe);
104 return format!("0.0.0+g{short}{dirty}");
105 }
106
107 let tag_ign_v = git_describe.trim_start_matches('v');
108 if tag_ign_v == pkg_ver {
109 return format!("v{tag_ign_v}{dirty}");
110 }
111 format!("{git_describe}{dirty}")
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_cargo_values() {
120 let build_info = build_info();
121 assert_eq!(build_info.crate_name, "treetop-core");
122 assert_eq!(build_info.cedar_version, "4.12.0");
123 }
124
125 #[test]
126 fn test_git_values_match_checkout() {
127 let Some(git) = &build_info().git else {
128 return;
131 };
132
133 let output = std::process::Command::new("git")
134 .args(["rev-parse", "HEAD"])
135 .output()
136 .expect("git must be runnable when build metadata contains git state");
137 assert!(output.status.success());
138 assert_eq!(git.sha, String::from_utf8_lossy(&output.stdout).trim());
139
140 let output = std::process::Command::new("git")
141 .args(["status", "--porcelain", "--untracked-files=no"])
142 .output()
143 .expect("git must be runnable when build metadata contains git state");
144 assert!(output.status.success());
145 assert_eq!(git.dirty, !output.stdout.is_empty());
146 }
147}