use heck::{ToPascalCase, ToSnakeCase};
use quote::{format_ident, quote};
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct CompilerConfig {
name: String,
description: String,
working_dir: String,
parameters: Vec<ParameterConfig>,
}
#[derive(Debug, Deserialize, Clone)]
struct ParameterConfig {
name: String,
description: String,
argument: String,
value_type: ValueType,
default_value: Option<String>,
#[serde(default)]
is_default: bool,
constraints: Option<ConstraintsConfig>,
}
#[derive(Debug, Deserialize, Clone)]
struct ConstraintsConfig {
compatible_games: Option<Vec<u32>>,
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
enum ValueType {
Flag,
Float,
Integer,
String,
Path,
}
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("generated_compilers.rs");
println!("cargo:rerun-if-changed=configs/");
let mut compiler_modules = Vec::new();
let mut compiler_metadata = Vec::new();
let manifest_dir = std::path::PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let compilers_configs_dir = manifest_dir.join("compiler_configs");
if !compilers_configs_dir.exists() || !compilers_configs_dir.is_dir() {
panic!("'compiler_configs' directory not found at {:?} or is not a directory.", compilers_configs_dir);
}
for entry in fs::read_dir(&compilers_configs_dir)
.unwrap_or_else(|_| panic!("Failed to read compilers directory at {:?}", compilers_configs_dir))
{
let entry = entry.expect("Failed to read entry");
let path = entry.path();
if !path.is_file() || path.extension().and_then(|s| s.to_str()) != Some("toml") {
continue;
}
let toml_content = fs::read_to_string(&path).expect("Failed to read TOML file");
let config: CompilerConfig = match toml::from_str::<CompilerConfig>(&toml_content) {
Ok(c) => c,
Err(e) => {
panic!(
"\n\n\
===============================================================\n\
[BUILD SCRIPT ERROR] Invalid TOML Configuration File\n\
===============================================================\n\
File: {}\n\
Error: {}\n\
---------------------------------------------------------------\n",
path.display(),
e
)
},
};
if config.parameters.is_empty() {
eprintln!("File: {} does not contain any parameters. Skipping.", path.display());
continue;
}
let module_name_str = config.name.to_snake_case();
let struct_name_str = config.name.to_pascal_case();
compiler_modules.push(generate_compiler_module(&config));
compiler_metadata.push((struct_name_str, module_name_str));
}
let compiler_enum_token_stream = generate_compiler_enum(&compiler_metadata);
let final_code = quote! {
#[comment = "THIS FILE IS AUTO-GENERATED BY BUILD.RS. DO NOT EDIT."]
#(#compiler_modules)*
#compiler_enum_token_stream
};
let syntax_tree = match syn::parse2::<syn::File>(final_code) {
Ok(tree) => tree,
Err(e) => {
panic!(
"Failed to parse generated code: {}\n",
e
);
}
};
let formatted_code = prettyplease::unparse(&syntax_tree);
fs::write(&dest_path, formatted_code).unwrap();
println!("cargo:rerun-if-changed=build.rs");
}
fn generate_compiler_module(config: &CompilerConfig) -> proc_macro2::TokenStream {
let module_name = format_ident!("{}", config.name.to_snake_case());
let struct_name = format_ident!("{}", config.name.to_pascal_case());
let arg_enum_name = format_ident!("{}Arg", config.name.to_pascal_case());
let name_str = &config.name;
let description_str = &config.description;
let working_dir_template = &config.working_dir;
let arg_doc_comment = format!("Enum of arguments for {}", struct_name);
let arg_variants = config.parameters.iter().map(|param| {
let variant_name = format_ident!("{}", param.name.to_pascal_case());
let doc_comment = ¶m.description;
match param.value_type {
ValueType::Flag => quote! { #[doc = #doc_comment] #variant_name, },
ValueType::Float => quote! { #[doc = #doc_comment] #variant_name(f32), },
ValueType::Integer => quote! { #[doc = #doc_comment] #variant_name(i64), },
ValueType::String => quote! { #[doc = #doc_comment] #variant_name(String), },
ValueType::Path => quote! { #[doc = #doc_comment] #variant_name(std::path::PathBuf), },
}
});
let default_arg_initializers = config.parameters.iter()
.filter(|p| p.is_default)
.map(|p| {
let variant_ident = format_ident!("{}", p.name.to_pascal_case());
match p.value_type {
ValueType::Flag => quote! { #arg_enum_name::#variant_ident },
_ => {
let default_val_str = p.default_value.as_ref()
.expect(&format!("Parameter '{}' needs a default_value", p.name));
let value = match p.value_type {
ValueType::Float => {
let val: f32 = default_val_str.parse().expect("Invalid float");
quote! { #val }
},
ValueType::Integer => {
let val: i64 = default_val_str.parse().expect("Invalid integer");
quote! { #val }
},
ValueType::Path => quote! { std::path::PathBuf::from(#default_val_str) },
ValueType::String => quote! { #default_val_str.to_string() },
ValueType::Flag => unreachable!(),
};
quote! { #arg_enum_name::#variant_ident(#value) }
}
}
});
let name_arms = config.parameters.iter().map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
let name_str = &p.name;
quote! { Self::#variant { .. } => #name_str, }
});
let desc_arms = config.parameters.iter().map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
let desc_str = &p.description;
quote! { Self::#variant { .. } => #desc_str, }
});
let value_type_arms = config.parameters.iter()
.filter(|p| p.value_type != ValueType::Flag)
.map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
let vt_ident = match p.value_type {
ValueType::Float => format_ident!("Float"),
ValueType::Integer => format_ident!("Integer"),
ValueType::String => format_ident!("String"),
ValueType::Path => format_ident!("Path"),
ValueType::Flag => unreachable!(), };
quote! { Self::#variant { .. } => ValueType::#vt_ident, }
});
let default_value_arms = config.parameters.iter().filter_map(|p| {
let default_value = p.default_value.as_ref()?;
let variant = format_ident!("{}", p.name.to_pascal_case());
let value = match p.value_type {
ValueType::Float => {
let val: f32 = default_value.parse().expect("Invalid float default value");
quote! { #val }
}
ValueType::Integer => {
let val: i64 = default_value.parse().expect("Invalid integer default value");
quote! { #val }
}
ValueType::String => quote! { #default_value.to_string() },
ValueType::Flag => return None, ValueType::Path => quote! { std::path::PathBuf::from(#default_value) },
};
Some(quote! { Self::#variant { .. } => Some(#arg_enum_name::#variant(#value)), })
});
let as_arg_arms = config.parameters.iter().map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
let argument = &p.argument;
if p.value_type == ValueType::Flag {
return quote! { Self::#variant => (#argument, None), }
}
let value_expr = if p.value_type == ValueType::Path {
quote! { val.to_string_lossy() }
} else {
quote! { val }
};
quote! { Self::#variant(val) => (#argument, Some(#value_expr.to_string())), }
});
let is_default_arms = config.parameters.iter()
.filter(|p| p.is_default)
.map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
quote! { Self::#variant { .. } => true, }
});
let compatible_games_arms = config.parameters.iter()
.filter_map(|p| {
let variant = format_ident!("{}", p.name.to_pascal_case());
p.constraints.as_ref().and_then(|c| c.compatible_games.as_ref()).map(|games| {
quote! { Self::#variant { .. } => Some(&[#(#games),*]), }
})
});
let try_from_arms = config.parameters.iter().map(|p| {
let arg_str = &p.argument;
let variant_ident = format_ident!("{}", p.name.to_pascal_case());
match p.value_type {
ValueType::Flag => quote! {
#arg_str => {
if value_opt.is_some() {
return Err(ParseArgError::UnexpectedValue(#arg_str));
}
Ok(Self::#variant_ident)
}
},
_ => {
let parse_logic = match p.value_type {
ValueType::Float => quote! { value_str.parse::<f32>() },
ValueType::Integer => quote! { value_str.parse::<i64>() },
ValueType::String => quote! { Ok::<_, ()>(value_str.to_string()) },
ValueType::Path => quote! { Ok::<_, ()>(std::path::PathBuf::from(value_str)) },
ValueType::Flag => unreachable!(),
};
quote! {
#arg_str => {
let value_str = value_opt.ok_or(ParseArgError::MissingValue(#arg_str))?;
let parsed_val = #parse_logic.map_err(|_| ParseArgError::InvalidValue {
argument: #arg_str,
value: value_str.to_string(),
})?;
Ok(Self::#variant_ident(parsed_val))
}
}
}
}
});
quote! {
#[doc = "This module is auto-generated by build.rs."]
pub mod #module_name {
use crate::{Compiler, CompilerArg, ValueType, ParseArgError};
use std::fmt;
#[doc = #description_str]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
pub struct #struct_name {
pub selected_args: Vec<#arg_enum_name>,
}
impl Compiler for #struct_name {
type Arg = #arg_enum_name;
fn name(&self) -> &'static str { #name_str }
fn description(&self) -> &'static str { #description_str }
fn working_dir_template(&self) -> &'static str { #working_dir_template }
fn get_args(&self) -> &[Self::Arg] { &self.selected_args }
fn add_arg(&mut self, arg: Self::Arg) { self.selected_args.push(arg); }
fn clear_args(&mut self) { self.selected_args.clear(); }
}
impl Default for #struct_name {
fn default() -> Self {
Self {
selected_args: vec![ #(#default_arg_initializers),* ],
}
}
}
impl #struct_name {
pub fn new() -> Self {
Self {
selected_args: Vec::new()
}
}
}
#[doc = #arg_doc_comment]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "enum_iter", derive(strum_macros::EnumIter))]
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
pub enum #arg_enum_name {
#(#arg_variants)*
}
impl CompilerArg for #arg_enum_name {
fn name(&self) -> &'static str {
match self { #(#name_arms)* }
}
fn description(&self) -> &'static str {
match self { #(#desc_arms)* }
}
fn value_type(&self) -> ValueType {
match self {
#(#value_type_arms)*
_ => ValueType::Flag,
}
}
fn get_default_value(&self) -> Option<Self> {
match self {
#(#default_value_arms)*
_ => None,
}
}
fn as_arg(&self) -> (&'static str, Option<String>) {
match self { #(#as_arg_arms)* }
}
fn is_default(&self) -> bool {
match self {
#(#is_default_arms)*
_ => false,
}
}
fn compatible_games(&self) -> Option<&'static [u32]> {
match self {
#(#compatible_games_arms)*
_ => None,
}
}
}
impl fmt::Display for #arg_enum_name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl<'a> TryFrom<&'a str> for #arg_enum_name {
type Error = ParseArgError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
let (key, value_opt) =
if let Some((k, v)) = value.split_once(' ') {
(k, Some(v))
} else {
(value, None)
};
match key {
#(#try_from_arms,)*
_ => Err(ParseArgError::UnknownArgument(key.to_string())),
}
}
}
impl TryFrom<String> for #arg_enum_name {
type Error = ParseArgError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
}
}
}
fn generate_compiler_enum(metadata: &[(String, String)]) -> proc_macro2::TokenStream {
let variants = metadata.iter().map(|(struct_name, module_name)| {
let struct_ident = format_ident!("{}", struct_name);
let module_ident = format_ident!("{}", module_name);
quote! { #struct_ident(#module_ident::#struct_ident), }
});
let name_arms = metadata.iter().map(|(struct_name, _)| {
let struct_ident = format_ident!("{}", struct_name);
quote! { Self::#struct_ident(inner) => inner.name(), }
});
let description_arms = metadata.iter().map(|(struct_name, _)| {
let struct_ident = format_ident!("{}", struct_name);
quote! { Self::#struct_ident(inner) => inner.description(), }
});
let build_args_arms = metadata.iter().map(|(struct_name, _)| {
let struct_ident = format_ident!("{}", struct_name);
quote! { Self::#struct_ident(inner) => inner.build_args(), }
});
let build_command_arms = metadata.iter().map(|(struct_name, _)| {
let struct_ident = format_ident!("{}", struct_name);
quote! { Self::#struct_ident(inner) => inner.build_command(context, executable), }
});
let use_statements = metadata.iter().map(|(_, module_name)| {
let mod_name = format_ident!("{}", module_name);
quote! { pub use #mod_name::*; }
});
quote! {
#(#use_statements)*
#[derive(Debug, Clone)]
#[cfg_attr(feature = "enum_iter", derive(strum_macros::EnumIter))]
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialization", serde(tag = "compiler_type"))]
pub enum CompilerEnum {
#(#variants)*
}
impl CompilerEnum {
pub fn name(&self) -> &'static str {
match self { #(#name_arms)* }
}
pub fn description(&self) -> &'static str {
match self { #(#description_arms)* }
}
pub fn build_args(&self) -> Vec<String> {
match self { #(#build_args_arms)* }
}
pub fn build_command(&self, context: &CompilerContext, executable: Option<PathBuf>) -> CommandInfo {
match self { #(#build_command_arms)* }
}
}
}
}