Skip to main content

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}