module_info/macros.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4// Build-time diagnostics emitted via `cargo:warning=…` (the only build-script
5// channel cargo surfaces by default). The leading `\x1b[2K\r` overwrites
6// cargo's `warning: <crate>@<ver>:` prefix in a TTY; in non-TTY log
7// collectors the escape is inert and the prefix shows through.
8//
9// `note!` is exported for consumers' build scripts. `error!` and `warn!`
10// stay crate-private to avoid colliding with `log::error!` / `log::warn!`.
11
12/// Emit a styled `Info:` line on cargo's build-script output channel.
13///
14/// Use from `build.rs` to add log lines that match the styling
15/// [`generate_project_metadata_and_linker_script`](crate::generate_project_metadata_and_linker_script)
16/// emits for its metadata dump. On non-Linux targets the macro is a no-op.
17#[cfg(target_os = "linux")]
18#[macro_export]
19macro_rules! note {
20 () => {
21 ::std::println!("cargo:warning=\x1b[2K\r");
22 };
23 ($($arg:tt)+) => {
24 ::std::println!("cargo:warning=\x1b[2K\r \x1b[1m\x1b[36mInfo:\x1b[0m {}", ::std::format!($($arg)+))
25 }
26}
27
28/// Non-Linux stub of `note!` so cross-platform `build.rs` compiles
29/// without `#[cfg]` guards.
30#[cfg(not(target_os = "linux"))]
31#[macro_export]
32macro_rules! note {
33 () => {};
34 ($($arg:tt)+) => {};
35}
36
37/// Macro for printing error messages during build
38#[cfg(target_os = "linux")]
39macro_rules! error {
40 ($($arg:tt)+) => {
41 ::std::println!("cargo:warning=\x1b[2K\r \x1b[1m\x1b[31mError:\x1b[0m {}", ::std::format!($($arg)+))
42 }
43}
44
45/// Macro for printing warning messages during build
46#[cfg(target_os = "linux")]
47macro_rules! warn {
48 ($($arg:tt)*) => {{
49 ::std::println!("cargo:warning=\x1b[2K\r \x1b[1m\x1b[33mWarning:\x1b[0m {}", ::std::format!($($arg)*))
50 }};
51}
52
53/// A macro that conditionally prints debug messages to cargo output.
54///
55/// This macro checks the environment variable `MODULE_INFO_DEBUG` to determine
56/// whether to print debug messages. The check is performed once and the result is
57/// cached using an atomic variable.
58///
59/// # Behavior
60///
61/// - First call: Checks `MODULE_INFO_DEBUG` environment variable
62/// - If `MODULE_INFO_DEBUG=true`: Prints formatted message to cargo output with purple "Debug:" prefix
63/// - If `MODULE_INFO_DEBUG` is unset or not "true": Suppresses output
64///
65/// # Implementation Details
66///
67/// The macro uses an atomic variable to cache the debug state:
68/// - 0: Not yet checked
69/// - 1: Debugging enabled
70/// - 2: Debugging disabled
71///
72/// The `static` is declared *inside* the macro body, which means each call
73/// site gets its own `DEBUG_STATE`. That's intentional: there is no public
74/// crate-level "is debug on?" flag, and keeping the state adjacent to its
75/// single consumer avoids a hidden global. The per-site cost is a single
76/// env-var read the first time that exact `debug!(...)` expands on the
77/// executing build (amortized across the life of the build script).
78#[cfg(target_os = "linux")]
79macro_rules! debug {
80 ($($arg:tt)*) => {{
81 static DEBUG_STATE: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0); // 0=unchecked, 1=enabled, 2=disabled
82
83 if DEBUG_STATE.load(std::sync::atomic::Ordering::Relaxed) == 0 {
84 let val = ::std::env::var("MODULE_INFO_DEBUG")
85 .map(|v| v.to_lowercase() == "true")
86 .unwrap_or(false);
87 DEBUG_STATE.store(if val { 1 } else { 2 }, std::sync::atomic::Ordering::Relaxed);
88 }
89
90 if DEBUG_STATE.load(std::sync::atomic::Ordering::Relaxed) == 1 {
91 ::std::println!("cargo:warning=\x1b[2K\r \x1b[1m\x1b[35mDebug:\x1b[0m {}", ::std::format!($($arg)*));
92 }
93 }};
94}
95
96/// Read the embedded metadata at runtime.
97///
98/// Two call shapes:
99///
100/// - `get_module_info!()` returns
101/// `ModuleInfoResult<HashMap<String, String>>` covering every field that
102/// resolved. Fields disabled at build time appear with value `""`.
103/// Fields whose symbols failed to resolve (`.note.package` stripped from
104/// the binary, or no `module_info::embed!()` invocation reached the
105/// linker) are **omitted** from the map, so check `contains_key` before
106/// assuming a key is present.
107/// - `get_module_info!(ModuleInfoField::Binary)` returns
108/// `ModuleInfoResult<String>` for a single field. Disabled fields
109/// yield `Ok("")`; unresolved symbols yield
110/// `Err(ModuleInfoError::NotAvailable)`.
111///
112/// If the process has already crashed, the same data is reachable via
113/// `coredumpctl info`, `readelf -n`, or WinDbg against the core dump,
114/// without going through this macro.
115///
116/// # Examples
117///
118/// Retrieve all module info:
119/// ```rust
120/// use module_info::{get_module_info, ModuleInfoResult};
121///
122/// fn print_all() -> ModuleInfoResult<()> {
123/// for (key, value) in get_module_info!()? {
124/// println!("{key}: {value}");
125/// }
126/// Ok(())
127/// }
128/// ```
129///
130/// ```rust
131/// use module_info::{get_module_info, ModuleInfoResult};
132///
133/// fn binary_name() -> ModuleInfoResult<String> {
134/// get_module_info!(ModuleInfoField::Binary)
135/// }
136/// ```
137///
138/// `ModuleInfoField::Foo` is matched as tokens by the macro, so the enum
139/// itself does not need to be imported when it appears only inside
140/// `get_module_info!(...)`.
141///
142/// # Safety
143///
144/// The macro reads `extern "C" static: u8` symbols emitted by the build
145/// script's linker script. The symbols are placed inside the read-only
146/// `.note.package` payload by the linker; the macro takes their address
147/// and hands it to [`crate::extract_module_info`], which scans for the
148/// terminating NUL. Memory is never written, and the bound check inside
149/// `extract_module_info` keeps a stripped or corrupted section from
150/// reading past the cap.
151///
152/// # Availability
153///
154/// This form is active when `embed-module-info` is enabled and the target
155/// is Linux. The fallback form (declared below) is active otherwise and
156/// returns `ModuleInfoError::NotAvailable` for every variant.
157#[cfg(all(feature = "embed-module-info", target_os = "linux"))]
158#[macro_export]
159macro_rules! get_module_info {
160 // Internal macro for processing a single field.
161 //
162 // Declares exactly the one extern static this invocation will read, then
163 // hands its address to `extract_module_info`. Each symbol is typed as a
164 // single `u8`; the linker script places it at a specific byte inside the
165 // `.note.package` payload, so the symbol's "value" is really its *address*.
166 // Typing it as a sized array (`[u8; 255]`) would be a lie: the symbol is
167 // not a standalone 255-byte object, it's a pointer into a shared JSON blob.
168 // Opaque `u8` is the smallest honest type and matches how the macro uses
169 // it (`&$symbol as *const u8`). Declaring only the single symbol per
170 // invocation keeps clippy's `unused_extern_items` lint quiet when the macro
171 // is used many times in one function.
172 (@__extract $symbol:ident) => {{
173 extern "C" {
174 #[allow(non_upper_case_globals)]
175 static $symbol: u8;
176 }
177 unsafe { $crate::extract_module_info(&$symbol as *const u8) }
178 }};
179
180 // Internal macro for adding a field to the accumulating HashMap.
181 //
182 // Insert every successfully-read field into the map, including empty
183 // strings. The embedded JSON always carries every key (the
184 // `.note.package` layout is fixed at build time), so an empty value
185 // encodes the documented "disabled at build time" state; consumers
186 // rely on `map.contains_key("repo")` returning true regardless of
187 // whether the embedder deliberately left `repo` empty. Filtering out
188 // empty values here would diverge from the single-field form of the
189 // macro (which returns `Ok("")` for the same bytes) and would force
190 // callers to reach for `map.get("repo").cloned().unwrap_or_default()`
191 // to reimplement the documented contract.
192 //
193 // A read failure (e.g. the symbol resolved to a null pointer on a
194 // binary where the note section was stripped) is still skipped;
195 // `print_module_info`'s guardrail above treats "fewer than the required
196 // identity fields populated" as `NotAvailable`.
197 (@__add_to_map $info_map:ident, $symbol:ident, $key:literal) => {{
198 if let Ok(value) = get_module_info!(@__extract $symbol) {
199 $info_map.insert($key.to_string(), value);
200 }
201 }};
202
203 // Public rules: one per `ModuleInfoField` variant. Dispatch happens at
204 // macro-expansion time, so a typo like `ModuleInfoField::Foo` is a
205 // compile error (no matching rule) rather than a runtime error.
206 // `ModuleInfoField` is `#[non_exhaustive]`, so adding a variant requires
207 // adding a rule here, which keeps the macro and the enum in lock-step.
208 (ModuleInfoField::Binary) => { get_module_info!(@__extract module_info_binary) };
209 (ModuleInfoField::Version) => { get_module_info!(@__extract module_info_version) };
210 (ModuleInfoField::ModuleVersion) => { get_module_info!(@__extract module_info_moduleVersion) };
211 (ModuleInfoField::Maintainer) => { get_module_info!(@__extract module_info_maintainer) };
212 (ModuleInfoField::Name) => { get_module_info!(@__extract module_info_name) };
213 (ModuleInfoField::Type) => { get_module_info!(@__extract module_info_type) };
214 (ModuleInfoField::Repo) => { get_module_info!(@__extract module_info_repo) };
215 (ModuleInfoField::Branch) => { get_module_info!(@__extract module_info_branch) };
216 (ModuleInfoField::Hash) => { get_module_info!(@__extract module_info_hash) };
217 (ModuleInfoField::Copyright) => { get_module_info!(@__extract module_info_copyright) };
218 (ModuleInfoField::Os) => { get_module_info!(@__extract module_info_os) };
219 (ModuleInfoField::OsVersion) => { get_module_info!(@__extract module_info_osVersion) };
220
221 // Public rule: handles requests for all module info fields
222 () => {{
223 // Fully-qualified path: do NOT `use std::collections::HashMap;`.
224 // This macro is invoked in arbitrary call sites; a local `use`
225 // would introduce `HashMap` into the caller's scope and could
226 // shadow or conflict with their existing imports.
227 // Pre-allocate with exact capacity
228 let mut info_map: ::std::collections::HashMap<::std::string::String, ::std::string::String> =
229 ::std::collections::HashMap::with_capacity($crate::ModuleInfoField::count());
230
231 // Each @__add_to_map call expands @__extract, which declares its own
232 // single-symbol extern block; no shared declaration needed here.
233 get_module_info!(@__add_to_map info_map, module_info_binary, "binary");
234 get_module_info!(@__add_to_map info_map, module_info_version, "version");
235 get_module_info!(@__add_to_map info_map, module_info_moduleVersion, "moduleVersion");
236 get_module_info!(@__add_to_map info_map, module_info_maintainer, "maintainer");
237 get_module_info!(@__add_to_map info_map, module_info_name, "name");
238 get_module_info!(@__add_to_map info_map, module_info_type, "type");
239 get_module_info!(@__add_to_map info_map, module_info_repo, "repo");
240 get_module_info!(@__add_to_map info_map, module_info_branch, "branch");
241 get_module_info!(@__add_to_map info_map, module_info_hash, "hash");
242 get_module_info!(@__add_to_map info_map, module_info_copyright, "copyright");
243 get_module_info!(@__add_to_map info_map, module_info_os, "os");
244 get_module_info!(@__add_to_map info_map, module_info_osVersion, "osVersion");
245
246 $crate::ModuleInfoResult::<::std::collections::HashMap<::std::string::String, ::std::string::String>>::Ok(info_map)
247 }};
248}
249
250/// No-op version of get_module_info macro for non-Linux platforms
251///
252/// This ensures that code can still be compiled on non-Linux platforms without errors,
253/// but returns `NotAvailable` at runtime. The per-variant rules below must
254/// mirror the Linux-side set exactly; an unknown variant name must be a
255/// compile error on every target, not "compiles on Windows, fails on Linux."
256#[cfg(any(not(feature = "embed-module-info"), not(target_os = "linux")))]
257#[macro_export]
258macro_rules! get_module_info {
259 // One rule per known variant, keyed off a literal identifier (not
260 // `$field:ident`), so a typo like `ModuleInfoField::Foo` is a compile
261 // error here the same way it is on Linux.
262 (ModuleInfoField::Binary) => { $crate::__module_info_not_available!("Binary") };
263 (ModuleInfoField::Version) => { $crate::__module_info_not_available!("Version") };
264 (ModuleInfoField::ModuleVersion) => { $crate::__module_info_not_available!("ModuleVersion") };
265 (ModuleInfoField::Maintainer) => { $crate::__module_info_not_available!("Maintainer") };
266 (ModuleInfoField::Name) => { $crate::__module_info_not_available!("Name") };
267 (ModuleInfoField::Type) => { $crate::__module_info_not_available!("Type") };
268 (ModuleInfoField::Repo) => { $crate::__module_info_not_available!("Repo") };
269 (ModuleInfoField::Branch) => { $crate::__module_info_not_available!("Branch") };
270 (ModuleInfoField::Hash) => { $crate::__module_info_not_available!("Hash") };
271 (ModuleInfoField::Copyright) => { $crate::__module_info_not_available!("Copyright") };
272 (ModuleInfoField::Os) => { $crate::__module_info_not_available!("Os") };
273 (ModuleInfoField::OsVersion) => { $crate::__module_info_not_available!("OsVersion") };
274
275 // Handle the empty form that returns all fields
276 () => {{
277 $crate::ModuleInfoResult::<::std::collections::HashMap<::std::string::String, ::std::string::String>>::Err(
278 $crate::ModuleInfoError::NotAvailable(
279 "Module info is only available on Linux platforms with embed-module-info feature enabled.".to_string(),
280 ),
281 )
282 }};
283}
284
285/// Internal helper: builds the `Err(NotAvailable(..))` result the per-variant
286/// rules of the non-Linux `get_module_info!` return. Kept private (leading
287/// `__`) because it's an implementation detail of the macro.
288#[cfg(any(not(feature = "embed-module-info"), not(target_os = "linux")))]
289#[doc(hidden)]
290#[macro_export]
291macro_rules! __module_info_not_available {
292 ($field:literal) => {{
293 $crate::ModuleInfoResult::<String>::Err($crate::ModuleInfoError::NotAvailable(format!(
294 "Module info field '{}' is only available on Linux platforms with embed-module-info feature enabled.",
295 $field
296 )))
297 }};
298}