rusteal-codegen 0.8.0

Code generator: UHT JSON to Rust bindings and C++ wrappers for rusteal
Documentation
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

use crate::config::CodegenConfig;
use crate::context::CodegenContext;

const README: &str = "\
# bindings

Rust bindings for the Unreal Engine modules this project uses, generated by
`rusteal build --all` from the engine's reflection data, together with the C++
wrappers in `Plugins/Rusteal/Source/Rusteal/Generated/`.

Do not edit: every file here is overwritten on the next run. Regenerate after
changing the engine version, the enabled modules or the blocklist in
`rusteal.toml`, and commit the result — the game crate depends on it.
";

pub fn write_crate_files(crate_dir: &Path, ctx: &CodegenContext, config: &CodegenConfig) {
    let manifest = format!(
        "# Generated by rusteal-codegen. Do not edit.\n\
         \n\
         [package]\n\
         name = \"bindings\"\n\
         version = \"0.1.0\"\n\
         edition = \"2024\"\n\
         publish = false\n\
         \n\
         [dependencies]\n\
         rusteal-core = {{ workspace = true }}\n\
         rusteal-ffi = {{ workspace = true }}\n\
         glam = {{ workspace = true }}\n\
         \n\
         {features}\
         \n\
         {lints}",
        features = render_features_section(ctx, config),
        lints = render_lints_section(),
    );

    let manifest_path = crate_dir.join("Cargo.toml");

    crate::write_if_changed(&manifest_path, &manifest)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", manifest_path.display()));

    let readme_path = crate_dir.join("README.md");

    crate::write_if_changed(&readme_path, README)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", readme_path.display()));

    eprintln!("  Cargo.toml: {} features", config.features.len());
}

fn render_features_section(ctx: &CodegenContext, config: &CodegenConfig) -> String {
    let mut feature_to_modules: BTreeMap<String, Vec<String>> = BTreeMap::new();

    for mapping in config.modules.values() {
        feature_to_modules
            .entry(mapping.feature.clone())
            .or_default()
            .push(mapping.module.clone());
    }

    let generated: BTreeSet<String> = config.features.iter().cloned().collect();

    let mut out = String::from("[features]\n");

    out.push_str(&format!(
        "default = {}\n",
        format_dep_array(&generated.iter().cloned().collect::<Vec<_>>())
    ));

    for feature in &generated {
        let mut dep_features: BTreeSet<String> = BTreeSet::new();

        if let Some(modules) = feature_to_modules.get(feature) {
            for module in modules {
                if let Some(module_deps) = ctx.module_deps.get(module) {
                    for dep_module in module_deps {
                        if let Some(dep_feature) = ctx.feature_for_module(dep_module)
                            && dep_feature != feature
                        {
                            dep_features.insert(dep_feature.to_string());
                        }
                    }
                }
            }
        }

        out.push_str(&format!(
            "{feature} = {}\n",
            format_dep_array(&dep_features.iter().cloned().collect::<Vec<_>>())
        ));
    }

    out
}

fn render_lints_section() -> String {
    let features: Vec<String> = super::manual::gating_features()
        .into_iter()
        .map(|f| format!("\"{f}\""))
        .collect();

    format!(
        "[lints.rust]\n\
         unexpected_cfgs = {{ level = \"warn\", check-cfg = ['cfg(feature, values({}))'] }}\n",
        features.join(", ")
    )
}

fn format_dep_array(deps: &[String]) -> String {
    if deps.is_empty() {
        "[]".to_string()
    } else {
        let items: Vec<String> = deps.iter().map(|d| format!("\"{d}\"")).collect();

        format!("[{}]", items.join(", "))
    }
}