kconfig-rs 0.3.1

Build library for translating Kconfig into Rust cfgs
Documentation
use std::env;
use std::fs::File;
use std::io::{BufRead as _, BufReader, Write as _};
use std::path::PathBuf;

pub enum State {
    Bool { active: bool },
    String { value: String },
    Number { value: String },
}

pub struct Config {
    pub key: String,
    pub state: State,
}

pub struct Generated {
    pub path: PathBuf,
    pub cfgs: Vec<Config>,
}

macro_rules! check_result {
    ($e:expr, $($args:expr),+) => {
        {
            match $e {
                Ok(r) => r,
                Err(error) => {
                    print!("cargo::error=");
                    print!($($args, )*);
                    println!(": {error}");
                    return None;
                }
            }
        }
    };
}

macro_rules! expect_env {
    ($var:literal) => {
        env::var($var).expect("must run from build.rs")
    };
}

/// Translate Kconfig into Rust cfgs.
pub fn generate() -> Option<Generated> {
    println!("cargo::rerun-if-env-changed=CARGO_KCONFIG_DOTCONFIG");

    let out = PathBuf::from(expect_env!("OUT_DIR"));
    let config = PathBuf::from(env::var("CARGO_KCONFIG_DOTCONFIG").unwrap_or(".config".into()));

    if config.is_relative() {
        println!("cargo::warning={}: relative path", config.display());
        println!("cargo::warning=This may cause unexpected configurations for some crates.");
        println!("cargo::warning=Wrong or missing CARGO_KCONFIG_DOTCONFIG?");
    }

    let config = check_result!(config.canonicalize(), "{}", config.display());

    if !config.is_file() {
        println!(
            "cargo::error={}: not a file. Wrong CARGO_KCONFIG_DOTCONFIG?",
            config.display()
        );
        return None;
    }

    println!("cargo::rerun-if-changed={}", config.display());

    let config = check_result!(File::open(&config), "Failed to open {}", config.display());
    let config = BufReader::new(config);

    let mut cfgs = Vec::new();
    for line in config.lines() {
        let Ok(line) = line else {
            continue;
        };

        if line.starts_with('#') {
            if line.starts_with("# CONFIG_") && line.ends_with(" is not set") {
                let key = line
                    .strip_prefix("# CONFIG_")
                    .unwrap()
                    .strip_suffix(" is not set")
                    .unwrap();

                cfgs.push(Config {
                    key: key.to_lowercase(),
                    state: State::Bool { active: false },
                });

                continue;
            }
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };

        let Some(key) = key.strip_prefix("CONFIG_") else {
            continue;
        };

        let key = key.to_lowercase();

        let state = match value {
            "y" | "m" => State::Bool { active: true },
            "n" => State::Bool { active: false },
            _ => {
                if value.starts_with('"') && value.ends_with('"') {
                    State::String {
                        value: value.to_owned(),
                    }
                } else {
                    State::Number {
                        value: value.to_owned(),
                    }
                }
            }
        };

        cfgs.push(Config { key, state });
    }

    let kconfig_rs_path = out.join("kconfig.rs");
    let mut kconfig_rs = match File::create(out.join("kconfig.rs")) {
        Ok(f) => f,
        Err(error) => {
            println!("cargo::error=Failed to generate kconfig.rs: {}", error);
            return None;
        }
    };

    for cfg in &cfgs {
        println!("cargo::rustc-check-cfg=cfg({}, values(any()))", cfg.key);

        if matches!(cfg.state, State::Bool { active: false }) {
            continue;
        }

        check_result!(
            writeln!(&mut kconfig_rs, "#[allow(dead_code)]"),
            "{}",
            kconfig_rs_path.display()
        );

        match &cfg.state {
            State::Bool { .. } => {
                println!("cargo::rustc-cfg={}", cfg.key);
                // Skip writing booleans to kconfig.rs
                // They can be checked with #[cfg] alone.
            }
            State::String { value } => {
                println!("cargo::rustc-cfg={}={}", cfg.key, value);
                check_result!(
                    writeln!(
                        &mut kconfig_rs,
                        "pub const CONFIG_{}: &str = {};",
                        cfg.key.to_uppercase(),
                        value,
                    ),
                    "{}",
                    kconfig_rs_path.display()
                );
            }
            State::Number { value } => {
                if value.is_empty() {
                    println!(
                        "cargo::warning=Skipping unassigned config {}. Check your .config",
                        cfg.key
                    );
                    continue;
                }

                println!("cargo::rustc-cfg={}=\"{}\"", cfg.key, value);
                check_result!(
                    writeln!(
                        &mut kconfig_rs,
                        "pub const CONFIG_{}: u32 = {};",
                        cfg.key.to_uppercase(),
                        value,
                    ),
                    "{}",
                    kconfig_rs_path.display()
                );
            }
        };
    }

    Some(Generated {
        path: kconfig_rs_path,
        cfgs,
    })
}