rusteal-codegen 0.2.1

Code generator: UHT JSON to Rust bindings and C++ wrappers for rusteal
Documentation
// Writes the generated `bindings` crate's `Cargo.toml` and `README.md`.
//
// The manifest is fully generated: the `[features]` table comes from the
// cross-module reference graph, and the dependencies are taken from the game's
// Cargo workspace (`{ workspace = true }`), so the generated crate never needs
// to know where the runtime comes from.

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` 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.
";

/// Write `Cargo.toml` and `README.md` for the generated crate at `crate_dir`.
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}",
        features = render_features_section(ctx, config),
    );

    let manifest_path = crate_dir.join("Cargo.toml");
    std::fs::write(&manifest_path, manifest)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", manifest_path.display()));

    let readme_path = crate_dir.join("README.md");
    std::fs::write(&readme_path, README)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", readme_path.display()));

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

/// Render the `[features]` table: one entry per feature generated in this run,
/// with dependencies from the cross-module reference graph, and `default`
/// turning all of them on.
fn render_features_section(ctx: &CodegenContext, config: &CodegenConfig) -> String {
    // Reverse map: feature name → modules. Some features (e.g. "slate")
    // map from multiple modules (SlateCore + Slate); we union their deps.
    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 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(", "))
    }
}