rusteal 0.6.0

Rust bindings for Unreal Engine 5.8.2
use std::path::Path;

use crate::project_version::{self, PINNED, PLUGINS, Pins, Scope, Version};
use crate::{build_cmd, global_config, setup};

pub fn run_upgrade(root: &Path) {
    let cli = Version::cli();

    let current = match project_version::read_pins(root).unwrap_or_else(|e| fail(&e)) {
        Pins::Exact(version) => version,
        Pins::Checkout(dir) => fail(&format!(
            "this project follows the Rusteal checkout at {dir}; there is no version to move.\n  \
             After updating the checkout, reinstall the plugins with its CLI:\n  \
             cargo run --manifest-path {manifest} -p rusteal -- setup {root}",
            dir = dir.display(),
            manifest = dir.join("Cargo.toml").display(),
            root = root.display(),
        )),
    };

    if current > cli {
        fail(&format!(
            "this project is at Rusteal {current}, newer than this CLI ({cli}).\n  \
             Install its version: cargo install rusteal@{current}"
        ));
    }

    if current == cli && project_version::check(root, Scope::PinsAndPlugins).is_ok() {
        eprintln!("rusteal upgrade: the project is already at {cli}.");
        return;
    }

    let engine = global_config::engine_path();

    if current == cli {
        eprintln!(
            "rusteal upgrade: the project is at {cli}, its plugins are not; reinstalling them"
        );
    } else {
        eprintln!("rusteal upgrade: {current} -> {cli}");
    }

    let workspaces = std::iter::once(root.join("Rust")).chain(
        rusteal_codegen::config::find_plugins(root)
            .into_iter()
            .map(|p| p.rust_workspace()),
    );

    for workspace in workspaces {
        let manifest_path = workspace.join("Cargo.toml");

        let manifest = std::fs::read_to_string(&manifest_path)
            .unwrap_or_else(|e| fail(&format!("cannot read {}: {e}", manifest_path.display())));

        std::fs::write(&manifest_path, rewrite_pins(&manifest, cli))
            .unwrap_or_else(|e| fail(&format!("cannot write {}: {e}", manifest_path.display())));

        eprintln!(
            "  {}: {} pinned to ={cli}",
            manifest_path.display(),
            PINNED.join(", ")
        );
    }

    for plugin in PLUGINS {
        let dir = root.join("Plugins").join(plugin);

        if dir.exists() {
            std::fs::remove_dir_all(&dir)
                .unwrap_or_else(|e| fail(&format!("cannot remove {}: {e}", dir.display())));
        }
    }

    setup::run_setup(root, &engine);
    build_cmd::run_build(root, &engine, None, 1, false, None);

    eprintln!("\nrusteal upgrade: done, the project is at {cli}.");
}

fn fail(message: &str) -> ! {
    eprintln!("Error: {message}");
    std::process::exit(1);
}

pub fn rewrite_pins(manifest: &str, version: Version) -> String {
    let mut in_deps = false;
    let mut out = String::with_capacity(manifest.len());

    for line in manifest.split_inclusive('\n') {
        let trimmed = line.trim_start();

        if trimmed.starts_with('[') {
            in_deps = trimmed.starts_with("[workspace.dependencies]");
        }

        let pinned = if in_deps {
            PINNED.iter().find(|name| {
                trimmed
                    .strip_prefix(**name)
                    .is_some_and(|rest| rest.trim_start().starts_with('='))
            })
        } else {
            None
        };

        match pinned {
            Some(name) => {
                let indent = &line[..line.len() - trimmed.len()];
                let newline = if line.ends_with('\n') { "\n" } else { "" };
                out.push_str(&format!("{indent}{name} = \"={version}\"{newline}"));
            }
            None => out.push_str(line),
        }
    }

    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::project_version::parse_pins;

    fn v(text: &str) -> Version {
        Version::parse(text).unwrap()
    }

    const EXACT: &str = r#"# comment
[workspace]
members = ["game", "bindings"]

[workspace.dependencies]
rusteal-runtime = "=0.2.1"
rusteal-core = "=0.2.1"
rusteal-ffi = "=0.2.1"
glam = "0.33.8"
"#;

    #[test]
    fn rewrite_keeps_everything_but_the_pins() {
        let rewritten = rewrite_pins(EXACT, v("0.3.0"));
        assert_eq!(rewritten, EXACT.replace("=0.2.1", "=0.3.0"));
        assert_eq!(parse_pins(&rewritten), Ok(Pins::Exact(v("0.3.0"))));

        let table = EXACT.replace(
            "rusteal-core = \"=0.2.1\"",
            "rusteal-core = { version = \"=0.2.1\" }",
        );

        assert_eq!(
            rewrite_pins(&table, v("0.3.0")),
            EXACT.replace("=0.2.1", "=0.3.0")
        );

        let other = "[dependencies]\nrusteal-core = \"=0.2.1\"\n";
        assert_eq!(rewrite_pins(other, v("0.3.0")), other);
    }
}