Skip to main content

kconfig_rs/
lib.rs

1use std::env;
2use std::fs::File;
3use std::io::{BufRead as _, BufReader, Write as _};
4use std::path::PathBuf;
5
6pub enum State {
7    Bool { active: bool },
8    String { value: String },
9    Number { value: String },
10}
11
12pub struct Config {
13    pub key: String,
14    pub state: State,
15}
16
17pub struct Generated {
18    pub path: PathBuf,
19    pub cfgs: Vec<Config>,
20}
21
22macro_rules! check_result {
23    ($e:expr, $($args:expr),+) => {
24        {
25            match $e {
26                Ok(r) => r,
27                Err(error) => {
28                    print!("cargo::error=");
29                    print!($($args, )*);
30                    println!(": {error}");
31                    return None;
32                }
33            }
34        }
35    };
36}
37
38macro_rules! expect_env {
39    ($var:literal) => {
40        env::var($var).expect("must run from build.rs")
41    };
42}
43
44/// Translate Kconfig into Rust cfgs.
45pub fn generate() -> Option<Generated> {
46    println!("cargo::rerun-if-env-changed=CARGO_KCONFIG_DOTCONFIG");
47
48    let out = PathBuf::from(expect_env!("OUT_DIR"));
49    let config = PathBuf::from(env::var("CARGO_KCONFIG_DOTCONFIG").unwrap_or(".config".into()));
50
51    if config.is_relative() {
52        println!("cargo::warning={}: relative path", config.display());
53        println!("cargo::warning=This may cause unexpected configurations for some crates.");
54        println!("cargo::warning=Wrong or missing CARGO_KCONFIG_DOTCONFIG?");
55    }
56
57    let config = check_result!(config.canonicalize(), "{}", config.display());
58
59    if !config.is_file() {
60        println!(
61            "cargo::error={}: not a file. Wrong CARGO_KCONFIG_DOTCONFIG?",
62            config.display()
63        );
64        return None;
65    }
66
67    println!("cargo::rerun-if-changed={}", config.display());
68
69    let config = check_result!(File::open(&config), "Failed to open {}", config.display());
70    let config = BufReader::new(config);
71
72    let mut cfgs = Vec::new();
73    for line in config.lines() {
74        let Ok(line) = line else {
75            continue;
76        };
77
78        if line.starts_with('#') {
79            if line.starts_with("# CONFIG_") && line.ends_with(" is not set") {
80                let key = line
81                    .strip_prefix("# CONFIG_")
82                    .unwrap()
83                    .strip_suffix(" is not set")
84                    .unwrap();
85
86                cfgs.push(Config {
87                    key: key.to_lowercase(),
88                    state: State::Bool { active: false },
89                });
90
91                continue;
92            }
93        }
94
95        let Some((key, value)) = line.split_once('=') else {
96            continue;
97        };
98
99        let Some(key) = key.strip_prefix("CONFIG_") else {
100            continue;
101        };
102
103        let key = key.to_lowercase();
104
105        let state = match value {
106            "y" | "m" => State::Bool { active: true },
107            "n" => State::Bool { active: false },
108            _ => {
109                if value.starts_with('"') && value.ends_with('"') {
110                    State::String {
111                        value: value.to_owned(),
112                    }
113                } else {
114                    State::Number {
115                        value: value.to_owned(),
116                    }
117                }
118            }
119        };
120
121        cfgs.push(Config { key, state });
122    }
123
124    let kconfig_rs_path = out.join("kconfig.rs");
125    let mut kconfig_rs = match File::create(out.join("kconfig.rs")) {
126        Ok(f) => f,
127        Err(error) => {
128            println!("cargo::error=Failed to generate kconfig.rs: {}", error);
129            return None;
130        }
131    };
132
133    for cfg in &cfgs {
134        println!("cargo::rustc-check-cfg=cfg({}, values(any()))", cfg.key);
135
136        if matches!(cfg.state, State::Bool { active: false }) {
137            continue;
138        }
139
140        check_result!(
141            writeln!(&mut kconfig_rs, "#[allow(dead_code)]"),
142            "{}",
143            kconfig_rs_path.display()
144        );
145
146        match &cfg.state {
147            State::Bool { .. } => {
148                println!("cargo::rustc-cfg={}", cfg.key);
149                // Skip writing booleans to kconfig.rs
150                // They can be checked with #[cfg] alone.
151            }
152            State::String { value } => {
153                println!("cargo::rustc-cfg={}={}", cfg.key, value);
154                check_result!(
155                    writeln!(
156                        &mut kconfig_rs,
157                        "pub const CONFIG_{}: &str = {};",
158                        cfg.key.to_uppercase(),
159                        value,
160                    ),
161                    "{}",
162                    kconfig_rs_path.display()
163                );
164            }
165            State::Number { value } => {
166                if value.is_empty() {
167                    println!(
168                        "cargo::warning=Skipping unassigned config {}. Check your .config",
169                        cfg.key
170                    );
171                    continue;
172                }
173
174                println!("cargo::rustc-cfg={}=\"{}\"", cfg.key, value);
175                check_result!(
176                    writeln!(
177                        &mut kconfig_rs,
178                        "pub const CONFIG_{}: u32 = {};",
179                        cfg.key.to_uppercase(),
180                        value,
181                    ),
182                    "{}",
183                    kconfig_rs_path.display()
184                );
185            }
186        };
187    }
188
189    Some(Generated {
190        path: kconfig_rs_path,
191        cfgs,
192    })
193}