microsoft/openvmm
Publicmirrored from https://github.com/microsoft/openvmmAvailable
openhcl/build_info/src/lib.rs
279lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | //! Provides build metadata |
| 5 | |
| 6 | #![expect(missing_docs)] |
| 7 | |
| 8 | use inspect::Inspect; |
| 9 | |
| 10 | #[derive(Debug, Inspect)] |
| 11 | pub struct BuildInfo { |
| 12 | #[inspect(safe)] |
| 13 | crate_name: &'static str, |
| 14 | #[inspect(safe, rename = "scm_revision")] |
| 15 | revision: &'static str, |
| 16 | #[inspect(safe, rename = "scm_branch")] |
| 17 | branch: &'static str, |
| 18 | #[inspect(safe)] |
| 19 | internal_scm_revision: &'static str, |
| 20 | #[inspect(safe)] |
| 21 | internal_scm_branch: &'static str, |
| 22 | #[inspect(safe)] |
| 23 | openhcl_version: &'static str, |
| 24 | } |
| 25 | |
| 26 | impl BuildInfo { |
| 27 | pub const fn new() -> Self { |
| 28 | // TODO: Once Option::unwrap_or() is stable in the const context |
| 29 | // can replace the if statements with it. |
| 30 | // Deliberately not storing `Option` to the build information |
| 31 | // structure to be closer to PODs. |
| 32 | Self { |
| 33 | crate_name: env!("CARGO_PKG_NAME"), |
| 34 | revision: if let Some(r) = option_env!("BUILD_GIT_SHA") { |
| 35 | r |
| 36 | } else { |
| 37 | "" |
| 38 | }, |
| 39 | branch: if let Some(b) = option_env!("BUILD_GIT_BRANCH") { |
| 40 | b |
| 41 | } else { |
| 42 | "" |
| 43 | }, |
| 44 | internal_scm_revision: if let Some(r) = option_env!("INTERNAL_GIT_SHA") { |
| 45 | r |
| 46 | } else { |
| 47 | "" |
| 48 | }, |
| 49 | internal_scm_branch: if let Some(r) = option_env!("INTERNAL_GIT_BRANCH") { |
| 50 | r |
| 51 | } else { |
| 52 | "" |
| 53 | }, |
| 54 | openhcl_version: if let Some(r) = option_env!("OPENHCL_VERSION") { |
| 55 | r |
| 56 | } else { |
| 57 | "" |
| 58 | }, |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | pub fn crate_name(&self) -> &'static str { |
| 63 | self.crate_name |
| 64 | } |
| 65 | |
| 66 | pub fn scm_revision(&self) -> &'static str { |
| 67 | self.revision |
| 68 | } |
| 69 | |
| 70 | pub fn scm_branch(&self) -> &'static str { |
| 71 | self.branch |
| 72 | } |
| 73 | |
| 74 | pub fn openhcl_version(&self) -> &'static str { |
| 75 | self.openhcl_version |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // Parse a `&str` segment as a u32. Panics if the segment is empty or not a valid u32. |
| 80 | const fn const_parse_u32_segment(s: &str) -> u32 { |
| 81 | assert!(!s.is_empty(), "version segment must not be empty"); |
| 82 | match u32::from_str_radix(s, 10) { |
| 83 | Ok(v) => v, |
| 84 | Err(_) => panic!("version segment is not a valid u32"), |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | // Const-compatible equivalent of `s.split_once('.')`. |
| 89 | const fn const_split_once_dot(s: &str) -> Option<(&str, &str)> { |
| 90 | let bytes = s.as_bytes(); |
| 91 | let mut i = 0; |
| 92 | while i < bytes.len() { |
| 93 | if bytes[i] == b'.' { |
| 94 | let (left, dot_right) = s.split_at(i); |
| 95 | let (_, right) = dot_right.split_at(1); |
| 96 | return Some((left, right)); |
| 97 | } |
| 98 | i += 1; |
| 99 | } |
| 100 | None |
| 101 | } |
| 102 | |
| 103 | // Parse the `OPENHCL_VERSION` environment variable into four u32 components. |
| 104 | // Strictly enforces the format to be "major.minor.build.platform", |
| 105 | // where each is a valid u32. |
| 106 | // Empty string is permitted to not panic on builds which omit the |
| 107 | // `OPENHCL_VERSION` environment variable like `cargo test`. |
| 108 | const fn const_parse_version(s: &str) -> [u32; 4] { |
| 109 | if s.is_empty() { |
| 110 | return [0, 0, 0, 0]; |
| 111 | } |
| 112 | let (major, rest) = match const_split_once_dot(s) { |
| 113 | Some(pair) => pair, |
| 114 | None => panic!("expected 4 dot-separated components in version string"), |
| 115 | }; |
| 116 | let (minor, rest) = match const_split_once_dot(rest) { |
| 117 | Some(pair) => pair, |
| 118 | None => panic!("expected 4 dot-separated components in version string"), |
| 119 | }; |
| 120 | let (build, platform) = match const_split_once_dot(rest) { |
| 121 | Some(pair) => pair, |
| 122 | None => panic!("expected 4 dot-separated components in version string"), |
| 123 | }; |
| 124 | // The fourth segment must not contain additional dots. |
| 125 | assert!( |
| 126 | const_split_once_dot(platform).is_none(), |
| 127 | "expected exactly 4 dot-separated components in version string" |
| 128 | ); |
| 129 | let major = const_parse_u32_segment(major); |
| 130 | let minor = const_parse_u32_segment(minor); |
| 131 | let build = const_parse_u32_segment(build); |
| 132 | let platform = const_parse_u32_segment(platform); |
| 133 | [major, minor, build, platform] |
| 134 | } |
| 135 | |
| 136 | /// Parsed components of the OPENHCL_VERSION env var (major.minor.build.platform). |
| 137 | /// All parsing happens at compile time — components are stored as u32. |
| 138 | #[derive(Debug)] |
| 139 | pub struct OpenHclVersion { |
| 140 | product_name: &'static str, |
| 141 | major: u32, |
| 142 | minor: u32, |
| 143 | build: u32, |
| 144 | platform: u32, |
| 145 | } |
| 146 | |
| 147 | impl OpenHclVersion { |
| 148 | pub const fn new() -> Self { |
| 149 | let [major, minor, build, platform] = const_parse_version(BuildInfo::new().openhcl_version); |
| 150 | Self { |
| 151 | product_name: "OpenHCL", |
| 152 | major, |
| 153 | minor, |
| 154 | build, |
| 155 | platform, |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | pub const fn product_name(&self) -> &'static str { |
| 160 | self.product_name |
| 161 | } |
| 162 | |
| 163 | pub const fn major(&self) -> u32 { |
| 164 | self.major |
| 165 | } |
| 166 | |
| 167 | pub const fn minor(&self) -> u32 { |
| 168 | self.minor |
| 169 | } |
| 170 | |
| 171 | pub const fn build(&self) -> u32 { |
| 172 | self.build |
| 173 | } |
| 174 | |
| 175 | pub const fn platform(&self) -> u32 { |
| 176 | self.platform |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | pub static OPENHCL_VERSION: OpenHclVersion = OpenHclVersion::new(); |
| 181 | |
| 182 | // Placing into a separate section to make easier to discover |
| 183 | // the build information even without a debugger. |
| 184 | // |
| 185 | // The #[used] attribute is not used as the static is reachable |
| 186 | // via a public function. |
| 187 | // |
| 188 | // The #[external_name] attribute is used to give the static |
| 189 | // an unmangled name and again be easily discoverable even without |
| 190 | // a debugger. With a debugger, the non-mangled name is easier |
| 191 | // to use. |
| 192 | |
| 193 | // UNSAFETY: link_section and export_name are unsafe. |
| 194 | #[expect(unsafe_code)] |
| 195 | // SAFETY: The build_info section is custom and carries no safety requirements. |
| 196 | #[unsafe(link_section = ".build_info")] |
| 197 | // SAFETY: The name "BUILD_INFO" is only declared here in OpenHCL and shouldn't |
| 198 | // collide with any other symbols. It is a special symbol intended for |
| 199 | // post-mortem debugging, and no runtime functionality should depend on it. |
| 200 | #[unsafe(export_name = "BUILD_INFO")] |
| 201 | static BUILD_INFO: BuildInfo = BuildInfo::new(); |
| 202 | |
| 203 | pub fn get() -> &'static BuildInfo { |
| 204 | // Without `black_box`, BUILD_INFO is optimized away |
| 205 | // in the release builds with `fat` LTO. |
| 206 | std::hint::black_box(&BUILD_INFO) |
| 207 | } |
| 208 | |
| 209 | #[cfg(test)] |
| 210 | mod tests { |
| 211 | use super::const_parse_version; |
| 212 | |
| 213 | #[test] |
| 214 | fn empty_string() { |
| 215 | // Ensure running `cargo test` doesn't panic. |
| 216 | assert_eq!(const_parse_version(""), [0, 0, 0, 0]); |
| 217 | } |
| 218 | |
| 219 | #[test] |
| 220 | fn full_version() { |
| 221 | assert_eq!(const_parse_version("1.6.499.2"), [1, 6, 499, 2]); |
| 222 | } |
| 223 | |
| 224 | #[test] |
| 225 | fn zero_platform() { |
| 226 | assert_eq!(const_parse_version("1.1.1.0"), [1, 1, 1, 0]); |
| 227 | } |
| 228 | |
| 229 | #[test] |
| 230 | fn zero_major_allowed() { |
| 231 | assert_eq!(const_parse_version("0.1.1.0"), [0, 1, 1, 0]); |
| 232 | } |
| 233 | |
| 234 | #[test] |
| 235 | fn zero_minor_allowed() { |
| 236 | assert_eq!(const_parse_version("1.0.1.0"), [1, 0, 1, 0]); |
| 237 | } |
| 238 | |
| 239 | #[test] |
| 240 | fn zero_build_allowed() { |
| 241 | assert_eq!(const_parse_version("1.1.0.0"), [1, 1, 0, 0]); |
| 242 | } |
| 243 | |
| 244 | #[test] |
| 245 | #[should_panic(expected = "expected 4 dot-separated components")] |
| 246 | fn partial_version_panics() { |
| 247 | const_parse_version("1.2"); |
| 248 | } |
| 249 | |
| 250 | #[test] |
| 251 | #[should_panic(expected = "version segment is not a valid u32")] |
| 252 | fn non_digit_segment_panics() { |
| 253 | const_parse_version("1.2.3A.4"); |
| 254 | } |
| 255 | |
| 256 | #[test] |
| 257 | #[should_panic(expected = "expected exactly 4 dot-separated components")] |
| 258 | fn extra_segments_panics() { |
| 259 | const_parse_version("1.2.3.4.5"); |
| 260 | } |
| 261 | |
| 262 | #[test] |
| 263 | #[should_panic(expected = "expected 4 dot-separated components")] |
| 264 | fn single_component_panics() { |
| 265 | const_parse_version("42"); |
| 266 | } |
| 267 | |
| 268 | #[test] |
| 269 | #[should_panic(expected = "version segment is not a valid u32")] |
| 270 | fn overflow_panics() { |
| 271 | const_parse_version("9999999999.0.0.0"); |
| 272 | } |
| 273 | |
| 274 | #[test] |
| 275 | #[should_panic(expected = "version segment must not be empty")] |
| 276 | fn empty_segment_panics() { |
| 277 | const_parse_version("1..3.4"); |
| 278 | } |
| 279 | } |
| 280 | |