sgdkx 0.4.4

One-command native SGDK dev environment. Unofficial, cross-platform CLI.
use crate::path;
use clap::Parser;
use std::path::PathBuf;
use std::process::Command;

#[derive(Parser)]
pub struct Args {
    /// Arguments passed straight through to make (e.g. debug, clean, -j8)
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    args: Vec<String>,
}

/// Thin wrapper around `make`: prepend the build tool dirs to PATH, then run `make`
/// (bare) with the given args verbatim. You can also run `make` directly if you put
/// those directories on PATH yourself.
pub fn run(args: &Args) {
    let argv: Vec<&str> = args.args.iter().map(String::as_str).collect();
    let status = make_command(&argv)
        .status()
        .unwrap_or_else(|e| {
            eprintln!("❌ failed to run make: {e}");
            std::process::exit(1);
        });
    std::process::exit(status.code().unwrap_or(1));
}

/// Build a Command that runs `make <make_args>` with PATH prepared.
///
/// On Windows we run make *inside* SGDK's bundled MSYS `sh` (`sh -c "make ..."`).
/// Launched directly via CreateProcess, MSYS make's self-restart (after generating the
/// `.d` includes) and gcc `-flto`'s parallel make fail with quoted argv ('"make":
/// Command not found'). Running under MSYS sh gives the native environment SGDK expects
/// on Windows, where the restart/recursion work. On Unix we exec `make` directly.
pub fn make_command(make_args: &[&str]) -> Command {
    prepend_tool_path();
    #[cfg(target_os = "windows")]
    {
        // Build `make <args>` for the MSYS sh. Single-quote each arg (escaping embedded single
        // quotes the POSIX way: ' -> '\'') so args with spaces or shell metacharacters survive.
        let mut line = String::from("make");
        for a in make_args {
            line.push_str(" '");
            line.push_str(&a.replace('\'', r"'\''"));
            line.push('\'');
        }
        let mut c = Command::new("sh");
        c.arg("-c").arg(line);
        return c;
    }
    #[cfg(not(target_os = "windows"))]
    {
        let mut c = Command::new("make");
        c.args(make_args);
        c
    }
}

/// Prepend the SGDK build-tool directories to THIS process's PATH (inherited by the
/// child make and its recipe commands). On Unix: bundled JRE, gcc toolchain, SGDK/bin.
/// On Windows: bundled JRE (for `java`) + SGDK/bin (bundled MSYS make.exe +
/// sh/rm/cp/mkdir/dlls + the m68k gcc.exe + native tools). All derived from the fixed
/// install layout under `path::config_dir()`.
///
/// We must modify the process PATH (not just the child's env) because on Windows the
/// executable lookup for a bare `make` uses the calling process's PATH. Running make
/// as a bare name (not an absolute path) keeps MSYS make's `$(MAKE)`/SHELL working.
pub fn prepend_tool_path() {
    if !path::is_installed() {
        eprintln!("❌ SGDK not installed. Please run `sgdkx install` first.");
        std::process::exit(1);
    }
    let sgdk_dir = path::sgdk_dir();
    let sgdk_bin = sgdk_dir.join("bin");

    let mut prepend: Vec<PathBuf> = Vec::new();
    if let Some(jre) = path::jre_dir() {
        prepend.push(jre.join("bin"));
    }
    // Unix only: the gcc toolchain is a separate component. On Windows it lives in SGDK/bin,
    // so toolchain_dir() returns None and the SGDK/bin entry below covers it.
    if let Some(tc) = path::toolchain_dir() {
        prepend.push(tc.join("bin"));
    }
    prepend.push(sgdk_bin);

    let mut paths = prepend;
    if let Some(orig) = std::env::var_os("PATH") {
        paths.extend(std::env::split_paths(&orig));
    }
    let new_path = std::env::join_paths(&paths).expect("failed to build PATH");
    // Export GDK so the project's portable Makefile (`GDK ?= ...`) resolves to THIS install on
    // every platform. Forward-slashed because MSYS make on Windows expects `/` paths. (Older
    // Makefiles that hard-assign `GDK = <abs>` keep their value — a makefile `=` beats the env.)
    let gdk = sgdk_dir.to_string_lossy().replace('\\', "/");
    // SAFETY: sgdkx is single-threaded here; set env right before spawning the build.
    unsafe {
        std::env::set_var("GDK", &gdk);
        std::env::set_var("PATH", &new_path);
    }
}