Skip to main content

treetop_core/
build_info.rs

1use 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    /// Exact version of the `cedar-policy` crate linked directly by Treetop.
20    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        // No git info available (published crate, source tarball, etc.)
78        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            // Registry packages and source archives deliberately contain no
129            // live checkout metadata.
130            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}