anaxa_builder/
build_rs.rs

1use crate::{codegen, config_io, parser};
2use anyhow::{Context, Result};
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Helper for `build.rs` to integrate Anaxa configuration.
8pub struct BuildHelper {
9    kconfig_dir: PathBuf,
10    config_file: PathBuf,
11    out_dir: PathBuf,
12}
13
14impl BuildHelper {
15    pub fn new() -> Result<Self> {
16        let out_dir = env::var_os("OUT_DIR").context("OUT_DIR not set")?.into();
17        Ok(Self {
18            kconfig_dir: PathBuf::from("src"),
19            config_file: PathBuf::from(".config"),
20            out_dir,
21        })
22    }
23
24    pub fn with_kconfig_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
25        self.kconfig_dir = dir.into();
26        self
27    }
28
29    pub fn with_config_file<P: Into<PathBuf>>(mut self, file: P) -> Self {
30        self.config_file = file.into();
31        self
32    }
33
34    pub fn build(self) -> Result<()> {
35        let tree = parser::build_config_tree(&self.kconfig_dir)?;
36        let configs = parser::flatten_configs(&tree);
37        let values = config_io::load_config(&self.config_file, &configs)?;
38
39        let out_path = self.out_dir.join("config.rs");
40        let rust_code = codegen::rust::generate_consts(&configs, &values)?;
41        fs::write(&out_path, rust_code)
42            .with_context(|| format!("Failed to write to {:?}", out_path))?;
43
44        println!("cargo:rerun-if-changed={}", self.config_file.display());
45        emit_rerun_if_changed(&self.kconfig_dir)?;
46
47        for item in &configs {
48            if let Some(val) = values.get(&item.name) {
49                if val.as_bool() == Some(true) {
50                    println!("cargo:rustc-cfg={}", item.name);
51                }
52            }
53        }
54
55        for (k, v) in values {
56            let v_str = match v {
57                toml::Value::String(s) => s,
58                toml::Value::Integer(i) => i.to_string(),
59                toml::Value::Float(f) => f.to_string(),
60                toml::Value::Boolean(b) => b.to_string(),
61                _ => continue,
62            };
63            println!("cargo:rustc-env=ANAXA_{}={}", k.to_uppercase(), v_str);
64        }
65
66        Ok(())
67    }
68}
69
70/// Helper for `build.rs` to integrate Anaxa configuration.
71///
72/// This function:
73/// 1. Scans `kconfig_dir` for `Kconfig.toml` files.
74/// 2. Loads configuration values from `config_file`.
75/// 3. Generates `config.rs` in `OUT_DIR`.
76/// 4. Emits `cargo:rustc-cfg` for enabled boolean configs.
77/// 5. Emits `cargo:rerun-if-changed` for the config file and all `Kconfig.toml` files.
78pub fn emit_cargo_instructions<P1, P2>(kconfig_dir: P1, config_file: P2) -> Result<()>
79where
80    P1: AsRef<Path>,
81    P2: AsRef<Path>,
82{
83    let kconfig_dir = kconfig_dir.as_ref();
84    let config_file = config_file.as_ref();
85
86    let tree = parser::build_config_tree(kconfig_dir)?;
87    let configs = parser::flatten_configs(&tree);
88
89    let values = config_io::load_config(config_file, &configs)?;
90
91    let out_dir = env::var_os("OUT_DIR").context("OUT_DIR not set")?;
92    let out_path = PathBuf::from(out_dir).join("config.rs");
93    let rust_code = codegen::rust::generate_consts(&configs, &values)?;
94    fs::write(&out_path, rust_code)
95        .with_context(|| format!("Failed to write to {:?}", out_path))?;
96
97    println!("cargo:rerun-if-changed={}", config_file.display());
98
99    emit_rerun_if_changed(kconfig_dir)?;
100
101    for item in &configs {
102        if let Some(val) = values.get(&item.name) {
103            if val.as_bool() == Some(true) {
104                println!("cargo:rustc-cfg={}", item.name);
105            }
106        }
107    }
108
109    Ok(())
110}
111
112fn emit_rerun_if_changed(dir: &Path) -> Result<()> {
113    use walkdir::WalkDir;
114    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
115        if entry.file_name() == "Kconfig.toml" {
116            println!("cargo:rerun-if-changed={}", entry.path().display());
117        }
118    }
119    Ok(())
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::fs;
126    use tempfile::tempdir;
127
128    #[test]
129    fn test_emit_cargo_instructions() -> Result<()> {
130        let dir = tempdir()?;
131        let kconfig_path = dir.path().join("Kconfig.toml");
132        fs::write(
133            &kconfig_path,
134            r#"
135[[config]]
136name = "TEST_BOOL"
137type = "bool"
138default = true
139desc = "Test"
140"#,
141        )?;
142
143        let config_file = dir.path().join(".config");
144        fs::write(&config_file, "TEST_BOOL = true\n")?;
145
146        let out_dir = tempdir()?;
147        unsafe {
148            env::set_var("OUT_DIR", out_dir.path());
149        }
150
151        emit_cargo_instructions(dir.path(), &config_file)?;
152
153        let config_rs = out_dir.path().join("config.rs");
154        assert!(config_rs.exists());
155        let content = fs::read_to_string(config_rs)?;
156        assert!(content.contains("pub const TEST_BOOL: bool = true;"));
157
158        Ok(())
159    }
160}