From f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Wed, 25 Feb 2026 16:10:23 +0100 Subject: Feat: Refactor core download logic with concurrency and async features Implement basic unit testing Implement automatic java executable switching based on game version Split loader module into smaller modules Implement basic documentation --- src/minecraft/launcher.rs | 262 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 199 insertions(+), 63 deletions(-) (limited to 'src/minecraft/launcher.rs') diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs index 2eeaf21..3cca948 100644 --- a/src/minecraft/launcher.rs +++ b/src/minecraft/launcher.rs @@ -1,64 +1,104 @@ -use std::process::Command; - -use log::{debug, info}; +use paths::{client_jar, library_file, natives_dir}; +use serde::Deserialize; +use std::path::Path; +use std::process::Output; +use std::{ + path::PathBuf, + process::{Command, ExitStatus}, +}; +use McError::Process; use crate::{ - config::Config, - errors::McError, - minecraft::manifests::{Library, Version}, - platform::paths, + config::Config, errors::McError, minecraft::manifests::Version, + platform::paths, util::fs::library_allowed, }; -fn build_classpath( - config: &Config, - version: &Version, -) -> Result { - let sep = if cfg!(windows) { ";" } else { ":" }; - let mut entries = Vec::new(); +#[derive(Debug, Clone, Deserialize)] +pub struct JavaRuntime { + pub major: u8, + pub path: PathBuf, +} + +impl JavaRuntime { + fn validate(&self) -> Result<(), McError> { + let output: Output = Command::new(&self.path) + .arg("-version") + .output() + .map_err(|e| { + McError::Runtime(format!( + "Failed to execute Java at {}: {}", + self.path.display(), + e + )) + })?; + + if !output.status.success() { + return Err(McError::Runtime(format!( + "Invalid Java binary: {}", + self.path.display() + ))); + } + + Ok(()) + } +} + +fn required_java_major(java_major: Option) -> u8 { + java_major.unwrap_or(8) +} + +fn resolve_runtime( + required: u8, + runtimes: &[JavaRuntime], +) -> Result { + let mut candidates: Vec<&JavaRuntime> = runtimes + .iter() + .filter(|r| r.major >= required) + .collect(); + + candidates.sort_by_key(|r| r.major); + + let runtime = candidates.first().ok_or_else(|| { + McError::Runtime(format!( + "No suitable Java runtime found (required: Java {})", + required + )) + })?; + + runtime.validate()?; + Ok(runtime.path.clone()) +} + +fn build_classpath(config: &Config, version: &Version) -> String { + let system_separator: &str = if cfg!(windows) { ";" } else { ":" }; + let mut entries: Vec = Vec::new(); for library in &version.libraries { if !library_allowed(library) { continue; } + if let Some(artifact) = &library.downloads.artifact { - let path = paths::library_file(config, &artifact.path)?; + let path = library_file(config, &artifact.path); entries.push(path.to_string_lossy().to_string()); } } - let client_jar = paths::client_jar(config, &version.id)?; - entries.push(client_jar.to_string_lossy().to_string()); + let client: PathBuf = client_jar(config, &version.id); + entries.push(client.to_string_lossy().to_string()); - Ok(entries.join(sep)) + entries.join(system_separator) } -pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { - let java = &config.java_path; - let classpath = build_classpath(config, version)?; - let natives_dir = paths::natives_dir(config, &version.id); - - if !natives_dir.exists() { - return Err(McError::Runtime(format!( - "Natives folder does not exist: {}", - natives_dir.display() - ))); - } - - let asset_index_id = version - .asset_index - .as_ref() - .ok_or_else(|| { - McError::Runtime("Missing assetIndex in version.json".into()) - })? - .id - .clone(); - - info!("Launching Minecraft {}", version.id); - debug!("Classpath: {}", classpath); - debug!("Natives: {}", natives_dir.display()); - debug!("Asset index: {}", asset_index_id); - - let mut cmd = Command::new(java); +fn build_command( + java_path: PathBuf, + config: &Config, + version: &Version, + natives_dir: &Path, + classpath: String, + asset_index_id: String, +) -> Command { + let mut cmd: Command = Command::new(java_path); cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) .arg(format!("-Djava.library.path={}", natives_dir.display())); @@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--version") .arg(&version.id) .arg("--gameDir") - .arg(paths::game_dir(config)) + .arg(paths::game_directory(config)) .arg("--assetsDir") - .arg(paths::assets_dir(config)) + .arg(paths::assets_directory(config)) .arg("--assetIndex") - .arg(&asset_index_id) + .arg(asset_index_id) .arg("--uuid") .arg(&config.uuid) .arg("--userProperties") @@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--userType") .arg("legacy"); - let status = cmd.status()?; + cmd +} + +pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { + let required: u8 = required_java_major( + version + .java_version + .as_ref() + .map(|j| j.major_version), + ); + + let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?; + + let classpath: String = build_classpath(config, version); + let natives_dir: PathBuf = natives_dir(config, &version.id); + + if !natives_dir.exists() { + return Err(McError::Runtime(format!( + "Natives folder does not exist: {}", + natives_dir.display() + ))); + } + + let asset_index_id = version + .asset_index + .as_ref() + .ok_or_else(|| { + McError::Runtime("Missing assetIndex in version.json".into()) + })? + .id + .clone(); + + let mut cmd: Command = build_command( + java_path, + config, + version, + &natives_dir, + classpath, + asset_index_id, + ); + + let status: ExitStatus = cmd.status()?; if !status.success() { - return Err(McError::Process("Minecraft exited with error".into())); + return Err(Process("Minecraft exited with error".into())); } Ok(()) } -fn library_allowed(lib: &Library) -> bool { - let rules = match &lib.rules { - | Some(r) => r, - | None => return true, - }; +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn required_java_major_defaults_to_8() { + assert_eq!(required_java_major(None), 8); + } + + #[test] + fn required_java_major_uses_given_value() { + assert_eq!(required_java_major(Some(17)), 17); + } + + #[test] + fn resolve_runtime_errors_when_no_runtimes() { + let runtimes: Vec = vec![]; + assert!(resolve_runtime(17, &runtimes).is_err()); + } + + #[test] + fn resolve_runtime_picks_lowest_matching_major() { + let runtimes: Vec = vec![ + JavaRuntime { + major: 21, + path: PathBuf::from("/bin/true"), + }, + JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), + }, + ]; + + let result: PathBuf = resolve_runtime(8, &runtimes).unwrap(); + assert_eq!(result, PathBuf::from("/bin/true")); + } - let mut allowed = false; + #[test] + fn resolve_runtime_respects_required_version() { + let runtimes: Vec = vec![ + JavaRuntime { + major: 8, + path: PathBuf::from("/bin/true"), + }, + JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), + }, + ]; + + let result: PathBuf = resolve_runtime(17, &runtimes).unwrap(); + assert_eq!(result, PathBuf::from("/bin/true")); + } - for rule in rules { - let os_match = match &rule.os { - | Some(os) => os.name == "linux", - | None => true, + #[test] + fn validate_succeeds_for_true_binary() { + let runtime = JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), }; - if os_match { - allowed = rule.action == "allow"; - } + + assert!(runtime.validate().is_ok()); } - allowed + #[test] + fn validate_fails_for_false_binary() { + let runtime = JavaRuntime { + major: 17, + path: PathBuf::from("/bin/false"), + }; + + assert!(runtime.validate().is_err()); + } } -- cgit v1.2.3