use heck::ToUpperCamelCase;
use indexmap::IndexMap;
use std::path::Path;
use std::{env, fs};
fn main() {
cfg_aliases::cfg_aliases! {
asdf: { any(feature = "asdf", not(target_os = "windows")) },
macos: { target_os = "macos" },
linux: { target_os = "linux" },
vfox: { any(feature = "vfox", target_os = "windows") },
}
built::write_built_file().expect("Failed to acquire build-time information");
codegen_settings();
codegen_registry();
}
fn raw_string_literal(s: &str) -> String {
let mut max_hashes = 0;
let mut current_hashes = 0;
let mut after_quote = false;
for c in s.chars() {
if after_quote {
if c == '#' {
current_hashes += 1;
max_hashes = max_hashes.max(current_hashes);
} else {
after_quote = false;
current_hashes = 0;
}
}
if c == '"' {
after_quote = true;
current_hashes = 0;
}
}
let hashes = "#".repeat(max_hashes + 1);
format!("r{hashes}\"{s}\"{hashes}")
}
fn parse_options(opts: Option<&toml::Value>) -> Vec<(String, String)> {
opts.map(|opts| {
if let Some(table) = opts.as_table() {
table
.iter()
.map(|(k, v)| {
let value = match v {
toml::Value::String(s) => s.clone(),
toml::Value::Table(t) => {
toml::to_string(t).unwrap_or_default()
}
_ => v.to_string(),
};
(k.clone(), value)
})
.collect::<Vec<_>>()
} else {
vec![]
}
})
.unwrap_or_default()
}
fn codegen_registry() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("registry.rs");
let mut lines = vec!["[".to_string()];
let registry: toml::Table = fs::read_to_string("registry.toml")
.unwrap()
.parse()
.unwrap();
let tools = registry.get("tools").unwrap().as_table().unwrap();
for (short, info) in tools {
let info = info.as_table().unwrap();
let aliases = info
.get("aliases")
.cloned()
.unwrap_or(toml::Value::Array(vec![]))
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect::<Vec<_>>();
let test = info.get("test").map(|t| {
let t = t.as_array().unwrap();
(
t[0].as_str().unwrap().to_string(),
t[1].as_str().unwrap().to_string(),
)
});
let mut backends = vec![];
for backend in info.get("backends").unwrap().as_array().unwrap() {
match backend {
toml::Value::String(backend) => {
backends.push(format!(
r##"RegistryBackend{{
full: r#"{backend}"#,
platforms: &[],
options: &[],
}}"##
));
}
toml::Value::Table(backend) => {
let full = backend.get("full").unwrap().as_str().unwrap();
let platforms = backend
.get("platforms")
.map(|p| {
p.as_array()
.unwrap()
.iter()
.map(|p| p.as_str().unwrap().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let backend_options = parse_options(backend.get("options"));
backends.push(format!(
r##"RegistryBackend{{
full: r#"{full}"#,
platforms: &[{platforms}],
options: &[{options}],
}}"##,
platforms = platforms
.into_iter()
.map(|p| format!("\"{p}\""))
.collect::<Vec<_>>()
.join(", "),
options = backend_options
.iter()
.map(|(k, v)| format!(
"({}, {})",
raw_string_literal(k),
raw_string_literal(v)
))
.collect::<Vec<_>>()
.join(", ")
));
}
_ => panic!("Unknown backend type"),
}
}
let os = info
.get("os")
.map(|os| {
let os = os.as_array().unwrap();
let mut os = os
.iter()
.map(|o| o.as_str().unwrap().to_string())
.collect::<Vec<_>>();
os.sort();
os
})
.unwrap_or_default();
let description = info
.get("description")
.map(|d| d.as_str().unwrap().to_string());
let depends = info
.get("depends")
.map(|depends| {
let depends = depends.as_array().unwrap();
let mut depends = depends
.iter()
.map(|d| d.as_str().unwrap().to_string())
.collect::<Vec<_>>();
depends.sort();
depends
})
.unwrap_or_default();
let idiomatic_files = info
.get("idiomatic_files")
.map(|idiomatic_files| {
idiomatic_files
.as_array()
.unwrap()
.iter()
.map(|f| f.as_str().unwrap().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let rt = format!(
r#"RegistryTool{{short: "{short}", description: {description}, backends: &[{backends}], aliases: &[{aliases}], test: &{test}, os: &[{os}], depends: &[{depends}], idiomatic_files: &[{idiomatic_files}]}}"#,
description = description
.map(|d| format!("Some({})", raw_string_literal(&d)))
.unwrap_or("None".to_string()),
backends = backends.into_iter().collect::<Vec<_>>().join(", "),
aliases = aliases
.iter()
.map(|a| format!("\"{a}\""))
.collect::<Vec<_>>()
.join(", "),
test = test
.map(|(t, v)| format!(
"Some(({}, {}))",
raw_string_literal(&t),
raw_string_literal(&v)
))
.unwrap_or("None".to_string()),
os = os
.iter()
.map(|o| format!("\"{o}\""))
.collect::<Vec<_>>()
.join(", "),
depends = depends
.iter()
.map(|d| format!("\"{d}\""))
.collect::<Vec<_>>()
.join(", "),
idiomatic_files = idiomatic_files
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", "),
);
lines.push(format!(r#" ("{short}", {rt}),"#));
for alias in aliases {
lines.push(format!(r#" ("{alias}", {rt}),"#));
}
}
lines.push(r#"].into()"#.to_string());
fs::write(&dest_path, lines.join("\n")).unwrap();
}
fn codegen_settings() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("settings.rs");
let mut lines = vec![
r#"#[derive(Config, Default, Debug, Clone, Serialize)]
#[config(partial_attr(derive(Clone, Serialize, Default)))]
pub struct Settings {"#
.to_string(),
];
let settings: toml::Table = fs::read_to_string("settings.toml")
.unwrap()
.parse()
.unwrap();
let props_to_code = |key: &str, props: &toml::Value| {
let mut lines = vec![];
let props = props.as_table().unwrap();
if let Some(description) = props.get("description") {
lines.push(format!(" /// {}", description.as_str().unwrap()));
}
let type_ = props
.get("rust_type")
.map(|rt| rt.as_str().unwrap())
.or(props.get("type").map(|t| match t.as_str().unwrap() {
"Bool" => "bool",
"String" => "String",
"Integer" => "i64",
"Url" => "String",
"Path" => "PathBuf",
"Duration" => "String",
"ListString" => "Vec<String>",
"ListPath" => "Vec<PathBuf>",
"SetString" => "BTreeSet<String>",
"IndexMap<String, String>" => "IndexMap<String, String>",
t => panic!("Unknown type: {t}"),
}));
if let Some(type_) = type_ {
let type_ = if props.get("optional").is_some_and(|v| v.as_bool().unwrap()) {
format!("Option<{type_}>")
} else {
type_.to_string()
};
let mut opts = IndexMap::new();
if let Some(env) = props.get("env") {
opts.insert("env".to_string(), env.to_string());
}
if let Some(default) = props.get("default") {
opts.insert("default".to_string(), default.to_string());
} else if type_ == "bool" {
opts.insert("default".to_string(), "false".to_string());
}
if let Some(parse_env) = props.get("parse_env") {
opts.insert(
"parse_env".to_string(),
parse_env.as_str().unwrap().to_string(),
);
}
if let Some(deserialize_with) = props.get("deserialize_with") {
opts.insert(
"deserialize_with".to_string(),
deserialize_with.as_str().unwrap().to_string(),
);
}
lines.push(format!(
" #[config({})]",
opts.iter()
.map(|(k, v)| format!("{k} = {v}"))
.collect::<Vec<_>>()
.join(", ")
));
lines.push(format!(" pub {key}: {type_},"));
} else {
lines.push(" #[config(nested)]".to_string());
lines.push(format!(
" pub {}: Settings{},",
key,
key.to_upper_camel_case()
));
}
lines.join("\n")
};
for (key, props) in &settings {
lines.push(props_to_code(key, props));
}
lines.push("}".to_string());
let nested_settings = settings
.iter()
.filter(|(_, v)| !v.as_table().unwrap().contains_key("type"))
.collect::<Vec<_>>();
for (child, props) in &nested_settings {
lines.push(format!(
r#"
#[derive(Config, Default, Debug, Clone, Serialize)]
#[config(partial_attr(derive(Clone, Serialize, Default)))]
#[config(partial_attr(serde(deny_unknown_fields)))]
pub struct Settings{name} {{"#,
name = child.to_upper_camel_case()
));
for (key, props) in props.as_table().unwrap() {
lines.push(props_to_code(key, props));
}
lines.push("}".to_string());
}
lines.push(
r#"
pub static SETTINGS_META: Lazy<IndexMap<&'static str, SettingsMeta>> = Lazy::new(|| {
indexmap!{"#
.to_string(),
);
for (name, props) in &settings {
let props = props.as_table().unwrap();
if let Some(type_) = props.get("type").map(|v| v.as_str().unwrap()) {
let meta_type = match type_ {
"IndexMap<String, String>" => "IndexMap",
other => other,
};
lines.push(format!(
r#" "{name}" => SettingsMeta {{
type_: SettingsType::{meta_type},"#,
));
if let Some(description) = props.get("description") {
let description = description.as_str().unwrap().to_string();
lines.push(format!(
" description: {},",
raw_string_literal(&description)
));
}
lines.push(" },".to_string());
}
}
for (name, props) in &nested_settings {
for (key, props) in props.as_table().unwrap() {
let props = props.as_table().unwrap();
if let Some(type_) = props.get("type").map(|v| v.as_str().unwrap()) {
let meta_type = match type_ {
"IndexMap<String, String>" => "IndexMap",
other => other,
};
lines.push(format!(
r#" "{name}.{key}" => SettingsMeta {{
type_: SettingsType::{meta_type},"#,
));
}
if let Some(description) = props.get("description") {
let description = description.as_str().unwrap().to_string();
lines.push(format!(
" description: {},",
raw_string_literal(&description)
));
}
lines.push(" },".to_string());
}
}
lines.push(
r#" }
});
"#
.to_string(),
);
fs::write(&dest_path, lines.join("\n")).unwrap();
}