rusteal 0.6.0

Rust bindings for Unreal Engine 5.8.2
use std::env;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

const EXCLUDED_DIRS: &[&str] = &["Generated", "Binaries", "Intermediate", "obj"];

fn main() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());

    let workspace_path = manifest_dir.join("..").join("ue_plugin");
    let embed_path = manifest_dir.join("ue_plugin_embed");

    let source_root = if workspace_path.join("Rusteal").exists() {
        workspace_path
    } else if embed_path.join("Rusteal").exists() {
        embed_path
    } else {
        panic!(
            "Cannot find UE plugin sources. \
             Expected either {workspace} or {embed} to contain Rusteal/",
            workspace = workspace_path.display(),
            embed = embed_path.display(),
        );
    };

    let source_root = fs::canonicalize(&source_root)
        .unwrap_or_else(|e| panic!("Failed to canonicalize {}: {e}", source_root.display()));

    println!("cargo:rerun-if-changed={}", source_root.display());

    let mut files: Vec<(String, PathBuf)> = Vec::new();
    collect_files(&source_root, &source_root, &mut files);
    files.sort_by(|a, b| a.0.cmp(&b.0));

    let out_file = out_dir.join("plugin_files.rs");

    let mut f = fs::File::create(&out_file)
        .unwrap_or_else(|e| panic!("Failed to create {}: {e}", out_file.display()));

    writeln!(f, "pub const PLUGIN_FILES: &[(&str, &[u8])] = &[").unwrap();

    for (rel_path, abs_path) in &files {
        let abs_str = abs_path.to_str().expect("non-UTF8 path").replace('\\', "/");
        writeln!(f, "    ({rel_path:?}, include_bytes!({abs_str:?})),").unwrap();
    }

    writeln!(f, "];").unwrap();

    eprintln!(
        "rusteal build.rs: embedded {} plugin files from {}",
        files.len(),
        source_root.display()
    );

    embed_templates(
        &manifest_dir,
        &out_dir,
        "templates",
        "TEMPLATE_FILES",
        "template_files.rs",
    );

    embed_templates(
        &manifest_dir,
        &out_dir,
        "plugin_templates",
        "PLUGIN_TEMPLATE_FILES",
        "plugin_template_files.rs",
    );
}

fn embed_templates(
    manifest_dir: &Path,
    out_dir: &Path,
    dir: &str,
    const_name: &str,
    out_name: &str,
) {
    let templates_dir = manifest_dir.join(dir);
    println!("cargo:rerun-if-changed={}", templates_dir.display());

    let mut files: Vec<(String, PathBuf)> = Vec::new();
    collect_files(&templates_dir, &templates_dir, &mut files);
    files.sort_by(|a, b| a.0.cmp(&b.0));

    let mut out = format!("pub const {const_name}: &[(&str, &[u8])] = &[\n");

    for (rel_path, path) in &files {
        let abs = path.to_str().expect("non-UTF8 path").replace('\\', "/");
        writeln!(out, "    ({rel_path:?}, include_bytes!({abs:?})),").unwrap();
    }

    out.push_str("];\n");

    let out_file = out_dir.join(out_name);

    fs::write(&out_file, out)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_file.display()));

    eprintln!(
        "rusteal build.rs: embedded {} files from {dir}",
        files.len()
    );
}

fn collect_files(root: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) {
    let entries =
        fs::read_dir(dir).unwrap_or_else(|e| panic!("Failed to read {}: {e}", dir.display()));

    for entry in entries {
        let entry = entry.unwrap();
        let path = entry.path();
        let file_name = entry.file_name();
        let name = file_name.to_string_lossy();

        if path.is_dir() {
            if EXCLUDED_DIRS.contains(&name.as_ref()) {
                continue;
            }

            collect_files(root, &path, out);
        } else {
            if name.ends_with(".csproj.props") {
                continue;
            }

            let rel = path.strip_prefix(root).unwrap();

            let rel_str = rel.to_str().expect("non-UTF8 path").replace('\\', "/");
            out.push((rel_str, path.clone()));
        }
    }
}