irox_build_rs/
lib.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Compile-time build metadata injection inspired by `shadow-rs`
7//!
8
9#![forbid(unsafe_code)]
10
11use crate::cargo::load_buildhost_variables;
12pub use crate::error::*;
13use std::collections::BTreeMap;
14use std::fmt::{Display, Formatter};
15use std::io::Write;
16use std::path::Path;
17
18mod cargo;
19mod error;
20#[cfg(feature = "git")]
21mod git;
22
23pub enum ErrorType {
24    IOError,
25}
26
27#[derive(Debug, Clone, Eq, PartialEq)]
28pub enum VariableSource {
29    Environment,
30    Cargo,
31    Git,
32    BuildHost,
33    Other(String),
34}
35
36#[derive(Debug, Clone, Eq, PartialEq)]
37pub enum VariableType {
38    String(String),
39    Bool(bool),
40}
41impl Display for VariableType {
42    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
43        match self {
44            VariableType::String(s) => write!(f, "{s}"),
45            VariableType::Bool(b) => write!(f, "{b}"),
46        }
47    }
48}
49impl VariableType {
50    pub fn as_str(&self) -> &str {
51        match self {
52            VariableType::String(s) => s.as_str(),
53            VariableType::Bool(b) => {
54                if *b {
55                    "true"
56                } else {
57                    "false"
58                }
59            }
60        }
61    }
62}
63
64#[derive(Default, Debug, Clone, Eq, PartialEq)]
65pub struct BuildEnvironment {
66    pub(crate) variables: BTreeMap<String, BuildVariable>,
67}
68impl BuildEnvironment {
69    pub fn as_parsed_environment(&self) -> ParsedBuildVariables {
70        let mut out = ParsedBuildVariables::default();
71        for (k, v) in &self.variables {
72            let v = v.value.as_str();
73            if cargo::CARGO_ENV_VARIABLES.contains(&k.as_str()) {
74                out.cargo_items.insert(k, v);
75                out.grouped.entry("CARGO_ITEMS").or_default().insert(k, v);
76            } else if cargo::RUSTC_ENV_VARIABLES.contains(&k.as_str()) {
77                out.rustc_items.insert(k, v);
78                out.grouped.entry("RUSTC_ITEMS").or_default().insert(k, v);
79            } else if cargo::BUILD_HOST_VARIABLES.contains(&k.as_str()) {
80                out.build_host.insert(k, v);
81                out.grouped.entry("BUILD_HOST").or_default().insert(k, v);
82            } else {
83                #[cfg(feature = "git")]
84                if git::GIT_VARIABLES.contains(&k.as_str()) {
85                    out.git_items.insert(k, v);
86                    out.grouped.entry("GIT_ITEMS").or_default().insert(k, v);
87                }
88            }
89            out.all_items.insert(k, v);
90        }
91        out
92    }
93}
94#[derive(Default, Debug, Clone, Eq, PartialEq)]
95pub struct ParsedBuildVariables<'a> {
96    pub all_items: BTreeMap<&'a str, &'a str>,
97    pub cargo_items: BTreeMap<&'a str, &'a str>,
98    pub rustc_items: BTreeMap<&'a str, &'a str>,
99    pub build_host: BTreeMap<&'a str, &'a str>,
100    pub git_items: BTreeMap<&'a str, &'a str>,
101
102    pub grouped: BTreeMap<&'a str, BTreeMap<&'a str, &'a str>>,
103}
104
105#[derive(Debug, Clone, Eq, PartialEq)]
106pub struct BuildVariable {
107    pub source: VariableSource,
108    pub name: String,
109    pub value: VariableType,
110}
111
112impl BuildVariable {
113    pub fn new_str(name: &str, value: &str, source: VariableSource) -> BuildVariable {
114        BuildVariable {
115            name: name.to_string(),
116            value: VariableType::String(value.to_string()),
117            source,
118        }
119    }
120    pub fn new_bool(name: &str, value: bool, source: VariableSource) -> BuildVariable {
121        BuildVariable {
122            name: name.to_string(),
123            value: VariableType::Bool(value),
124            source,
125        }
126    }
127}
128
129#[derive(Debug, Clone)]
130pub struct Settings {
131    include_cargo: bool,
132    include_rustc: bool,
133    include_buildhost: bool,
134    #[cfg(feature = "git")]
135    include_git: bool,
136
137    extra_envs: Vec<String>,
138    extra_varbls: Vec<BuildVariable>,
139}
140impl Default for Settings {
141    fn default() -> Self {
142        Settings {
143            include_cargo: true,
144            include_rustc: true,
145            include_buildhost: true,
146            #[cfg(feature = "git")]
147            include_git: true,
148            extra_envs: vec![],
149            extra_varbls: vec![],
150        }
151    }
152}
153impl Settings {
154    pub fn new() -> Self {
155        Settings::default()
156    }
157    pub fn without_cargo(self) -> Self {
158        Settings {
159            include_cargo: false,
160            ..self
161        }
162    }
163    pub fn without_rustc(self) -> Self {
164        Settings {
165            include_rustc: false,
166            ..self
167        }
168    }
169    pub fn without_buildhost(self) -> Self {
170        Settings {
171            include_buildhost: false,
172            ..self
173        }
174    }
175    #[cfg(feature = "git")]
176    pub fn without_git(self) -> Self {
177        Settings {
178            include_git: false,
179            ..self
180        }
181    }
182    pub fn with_extra_envs(self, envs: &[&str]) -> Self {
183        let envs: Vec<String> = envs.iter().map(|v| v.to_string()).collect();
184        Settings {
185            extra_envs: envs,
186            ..self
187        }
188    }
189    pub fn with_extra_varbls(self, varbls: &[BuildVariable]) -> Self {
190        let varbls: Vec<BuildVariable> = Vec::from(varbls);
191        Settings {
192            extra_varbls: varbls,
193            ..self
194        }
195    }
196}
197pub fn generate_build_environment() -> Result<BuildEnvironment, Error> {
198    generate_build_environment_settings(&mut Settings::default())
199}
200
201fn add_env(name: &str, envt: &mut BuildEnvironment) {
202    let value = std::env::var(name).unwrap_or_default();
203    envt.variables.insert(
204        name.to_string(),
205        BuildVariable {
206            source: VariableSource::Environment,
207            name: name.to_string(),
208            value: VariableType::String(value),
209        },
210    );
211}
212pub fn generate_build_environment_settings(
213    settings: &mut Settings,
214) -> Result<BuildEnvironment, Error> {
215    let mut envt = BuildEnvironment::default();
216    if settings.include_cargo {
217        for varbl in cargo::CARGO_ENV_VARIABLES {
218            add_env(varbl, &mut envt);
219        }
220    }
221    if settings.include_rustc {
222        for varbl in cargo::RUSTC_ENV_VARIABLES {
223            add_env(varbl, &mut envt);
224        }
225    }
226    if settings.include_buildhost {
227        load_buildhost_variables(&mut envt)?;
228    }
229
230    for extra_env in &settings.extra_envs {
231        add_env(extra_env.as_str(), &mut envt);
232    }
233    for varbl in settings.extra_varbls.drain(..) {
234        envt.variables.insert(varbl.name.clone(), varbl);
235    }
236
237    #[cfg(feature = "git")]
238    {
239        if settings.include_git {
240            if let Err(e) = git::load_git_variables(&mut envt) {
241                eprintln!("Warning: Unable to load git variables: {e:#?}");
242            }
243        }
244    }
245
246    Ok(envt)
247}
248
249pub fn generate_module() -> Result<(), Error> {
250    generate_module_settings(Settings::default())
251}
252pub fn generate_module_settings(mut settings: Settings) -> Result<(), Error> {
253    let out_dir = std::env::var_os("OUT_DIR").unwrap();
254    let dest_path = Path::new(&out_dir);
255    std::fs::create_dir_all(dest_path)?;
256    let dest_file = dest_path.join("builders.rs");
257    let mut dest_file = std::fs::OpenOptions::new()
258        .create(true)
259        .truncate(true)
260        .write(true)
261        .open(dest_file)?;
262    dest_file.set_len(0)?;
263
264    let env = generate_build_environment_settings(&mut settings)?;
265    write_environment(&mut dest_file, &env, &settings)
266}
267pub fn write_environment<T: Write>(
268    mut dest_file: &mut T,
269    env: &BuildEnvironment,
270    settings: &Settings,
271) -> Result<(), Error> {
272    let mut groups = BTreeMap::new();
273
274    for varbl in env.variables.values() {
275        let name = &varbl.name;
276        match &varbl.value {
277            VariableType::String(val) => {
278                writeln!(dest_file, r##"pub const {name}: &str = r#"{val}"#;"##)?;
279            }
280            VariableType::Bool(val) => {
281                writeln!(dest_file, "pub const {name}: bool = {val};")?;
282            }
283        }
284    }
285
286    if settings.include_cargo {
287        filter_and_write(
288            &mut dest_file,
289            env,
290            &mut groups,
291            "CARGO_ITEMS",
292            &cargo::CARGO_ENV_VARIABLES,
293        )?;
294    }
295    if settings.include_rustc {
296        filter_and_write(
297            &mut dest_file,
298            env,
299            &mut groups,
300            "RUSTC_ITEMS",
301            &cargo::RUSTC_ENV_VARIABLES,
302        )?;
303    }
304
305    filter_and_write(
306        &mut dest_file,
307        env,
308        &mut groups,
309        "BUILD_HOST",
310        &cargo::BUILD_HOST_VARIABLES,
311    )?;
312
313    #[cfg(feature = "git")]
314    {
315        if settings.include_git {
316            filter_and_write(
317                &mut dest_file,
318                env,
319                &mut groups,
320                "GIT_ITEMS",
321                &git::GIT_VARIABLES,
322            )?;
323        }
324    }
325
326    write_aggregation_block(
327        &mut dest_file,
328        "ALL_ITEMS",
329        env.variables
330            .values()
331            .cloned()
332            .collect::<Vec<BuildVariable>>()
333            .as_slice(),
334    )?;
335    write_grouped_block(&mut dest_file, groups)?;
336
337    Ok(())
338}
339
340fn filter_and_write<T: Write>(
341    dest_file: &mut T,
342    env: &BuildEnvironment,
343    groups: &mut BTreeMap<String, Vec<BuildVariable>>,
344    name: &str,
345    filter: &[&str],
346) -> Result<(), Error> {
347    let varbls: Vec<BuildVariable> = env
348        .variables
349        .values()
350        .filter(|v| filter.contains(&v.name.as_str()))
351        .cloned()
352        .collect();
353    groups.insert(name.to_string(), varbls.clone());
354    write_aggregation_block(dest_file, name, varbls.as_slice())
355}
356
357fn write_aggregation_block<T: Write>(
358    dest_file: &mut T,
359    name: &str,
360    items: &[BuildVariable],
361) -> Result<(), Error> {
362    writeln!(
363        dest_file,
364        "static {name}: std::sync::OnceLock<std::collections::BTreeMap<&'static str, &'static str>> = std::sync::OnceLock::new();"
365    )?;
366    writeln!(dest_file, "#[allow(non_snake_case)]")?;
367    writeln!(
368        dest_file,
369        "pub fn get_{name}() -> &'static std::collections::BTreeMap<&'static str, &'static str> {{"
370    )?;
371    writeln!(
372        dest_file,
373        "\t{name}.get_or_init(|| std::collections::BTreeMap::from(["
374    )?;
375    for varbl in items {
376        let name = &varbl.name;
377        match varbl.value {
378            VariableType::String(_) => writeln!(dest_file, "\t\t(\"{name}\", {name}),")?,
379            VariableType::Bool(b) => writeln!(dest_file, "\t\t(\"{name}\", \"{b}\"),")?,
380        }
381    }
382    writeln!(dest_file, "\t]))")?;
383    writeln!(dest_file, "}}")?;
384    Ok(())
385}
386
387fn write_grouped_block<T: Write>(
388    dest_file: &mut T,
389    items: BTreeMap<String, Vec<BuildVariable>>,
390) -> Result<(), Error> {
391    writeln!(
392        dest_file,
393        "static GROUPS: std::sync::OnceLock<std::collections::BTreeMap<&'static str, std::collections::BTreeMap<&'static str, &'static str>>> = std::sync::OnceLock::new();"
394    )?;
395    writeln!(dest_file, "#[allow(non_snake_case)]")?;
396    writeln!(
397        dest_file,
398        "pub fn get_GROUPS() -> &'static std::collections::BTreeMap<&'static str, std::collections::BTreeMap<&'static str, &'static str>> {{"
399    )?;
400    writeln!(
401        dest_file,
402        "\tGROUPS.get_or_init(|| std::collections::BTreeMap::from(["
403    )?;
404    for (k, items) in items {
405        writeln!(
406            dest_file,
407            "\t\t(\"{k}\", std::collections::BTreeMap::from(["
408        )?;
409        for varbl in items {
410            let name = &varbl.name;
411            match varbl.value {
412                VariableType::String(_) => writeln!(dest_file, "\t\t\t(\"{name}\", {name}),")?,
413                VariableType::Bool(b) => writeln!(dest_file, "\t\t\t(\"{name}\", \"{b}\"),")?,
414            }
415        }
416        writeln!(dest_file, "\t\t])),")?;
417    }
418    writeln!(dest_file, "\t]))")?;
419    writeln!(dest_file, "}}")?;
420    Ok(())
421}