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")
};
}
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);
}
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,
})
}