javamc 0.1.0

Cross-platform CLI to download and run the official Mojang Java runtimes bundled with Minecraft.
use clap::{Parser, Subcommand};
use piston_mc::java::{JavaManifest, JavaRuntime};
use simple_download_utility::MultiDownloadProgress;
use std::path::{Path, PathBuf};

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct MCJavaArgs {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand, Debug)]
enum Command {
    /// Lists all available java versions
    List,

    /// Downloads a java version to the given directory
    Download {
        /// The java version to download
        version: String,

        /// The directory to download the version into
        directory: PathBuf,
    },

    /// Downloads (if needed) and executes a java version, forwarding any extra args
    Execute {
        /// The java version to execute
        version: String,

        /// The directory to download the version into
        directory: PathBuf,

        /// Arguments passed through to the java executable
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        java_args: Vec<String>,
    },
}

#[tokio::main]
async fn main() {
    let args = MCJavaArgs::parse();

    match args.command {
        Command::List => {
            let list = list().await;
            println!("Available Java versions:");
            for rt in list {
                println!("{:<20} ({})", rt.version.name, rt.version.released)
            }
        }
        Command::Download { version, directory } => {
            download(version, directory).await;
        }
        Command::Execute {
            version,
            directory,
            java_args,
        } => {
            execute(version, directory, java_args).await;
        }
    }
}

async fn download(version: String, directory: PathBuf) {
    // Get the gamma runtime (Java 17) for Windows x64
    let list = list().await;
    if let Some(runtime) = list.iter().find(|v| v.version.name == version) {
        let (sender, mut receiver) = tokio::sync::mpsc::channel::<MultiDownloadProgress>(16);
        let task = runtime.install(directory, 100, Some(sender));

        tokio::spawn(async move {
            while let Some(progress) = receiver.recv().await {
                let percent =
                    (progress.files_downloaded as f32 / progress.files_total as f32) * 100.0;

                let mb_per_sec = progress.bytes_per_second as f32 / 1024.0 / 1024.0;

                println!(
                    "Installing Java: {:.2}% ({}/{} files) {:.2} MB/s",
                    percent, progress.files_downloaded, progress.files_total, mb_per_sec
                );
            }
        });

        task.await.expect("Failed to install Java runtime");

        println!("Java runtime installed successfully");
    }
}

async fn execute(version: String, directory: PathBuf, java_args: Vec<String>) {
    if !Path::exists(&directory.join("bin")) {
        download(version.clone(), directory.clone()).await;
    }

    // Resolve the platform-specific java executable inside the runtime's bin directory.
    let java_exe = if cfg!(windows) { "java.exe" } else { "java" };
    let java_path = directory.join("bin").join(java_exe);

    // Hand control over to java: stdio is inherited by default, so Minecraft's
    // stdin/stdout (the server console) works exactly as if java were run directly.
    // The child inherits our working directory, so relative paths like `server.jar`
    // resolve against the caller's cwd, matching a direct java invocation.
    let status = tokio::process::Command::new(&java_path)
        .args(&java_args)
        .status()
        .await;

    match status {
        Ok(status) => std::process::exit(status.code().unwrap_or(0)),
        Err(e) => {
            eprintln!("Failed to execute Java at {}: {}", java_path.display(), e);
            std::process::exit(1);
        }
    }
}

async fn list() -> Vec<JavaRuntime> {
    let manifest = match JavaManifest::fetch().await {
        Ok(items) => items,
        Err(e) => {
            eprintln!("Failed to get the list of java versions from Mojang.");
            eprintln!("{}", e);
            std::process::exit(1);
        }
    };
    let runtime = {
        #[cfg(all(target_os = "windows", target_arch = "x86"))]
        {
            manifest.windows_x86
        }
        #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
        {
            manifest.windows_x64
        }

        #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
        {
            manifest.macos
        }
        #[cfg(all(target_os = "macos", target_arch = "arm"))]
        {
            manifest.macos_arm64
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        {
            manifest.linux
        }
        #[cfg(all(target_os = "linux", target_arch = "arm"))]
        {
            manifest.linux_i386
        }
    };
    let mut runtimes: Vec<JavaRuntime> = vec![];
    runtimes.extend(runtime.alpha);
    runtimes.extend(runtime.beta);
    runtimes.extend(runtime.gamma);
    runtimes.extend(runtime.delta);
    runtimes.extend(runtime.gamma_snapshot);
    runtimes.extend(runtime.epsilon);
    runtimes.extend(runtime.legacy);
    runtimes.sort_by(|a, b| b.version.released.cmp(&a.version.released));
    runtimes
}