aboutsummaryrefslogtreecommitdiffstats
path: root/src/minecraft/launcher.rs
diff options
context:
space:
mode:
authorFilip Wandzio <contact@philw.dev>2026-02-25 16:10:23 +0100
committerFilip Wandzio <contact@philw.dev>2026-02-25 16:10:23 +0100
commitf7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a (patch)
treec96432be342b02bc0409e5b78b6b5d54afcc7cd6 /src/minecraft/launcher.rs
parent2e10b0713f5369f489d2ababd70108cc359c5d2d (diff)
downloaddml-f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a.tar.gz
dml-f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a.zip
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
Diffstat (limited to '')
-rw-r--r--src/minecraft/launcher.rs262
1 files changed, 199 insertions, 63 deletions
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 @@
1use std::process::Command; 1use paths::{client_jar, library_file, natives_dir};
2 2use serde::Deserialize;
3use log::{debug, info}; 3use std::path::Path;
4use std::process::Output;
5use std::{
6 path::PathBuf,
7 process::{Command, ExitStatus},
8};
9use McError::Process;
4 10
5use crate::{ 11use crate::{
6 config::Config, 12 config::Config, errors::McError, minecraft::manifests::Version,
7 errors::McError, 13 platform::paths, util::fs::library_allowed,
8 minecraft::manifests::{Library, Version},
9 platform::paths,
10}; 14};
11 15
12fn build_classpath( 16#[derive(Debug, Clone, Deserialize)]
13 config: &Config, 17pub struct JavaRuntime {
14 version: &Version, 18 pub major: u8,
15) -> Result<String, McError> { 19 pub path: PathBuf,
16 let sep = if cfg!(windows) { ";" } else { ":" }; 20}
17 let mut entries = Vec::new(); 21
22impl JavaRuntime {
23 fn validate(&self) -> Result<(), McError> {
24 let output: Output = Command::new(&self.path)
25 .arg("-version")
26 .output()
27 .map_err(|e| {
28 McError::Runtime(format!(
29 "Failed to execute Java at {}: {}",
30 self.path.display(),
31 e
32 ))
33 })?;
34
35 if !output.status.success() {
36 return Err(McError::Runtime(format!(
37 "Invalid Java binary: {}",
38 self.path.display()
39 )));
40 }
41
42 Ok(())
43 }
44}
45
46fn required_java_major(java_major: Option<u8>) -> u8 {
47 java_major.unwrap_or(8)
48}
49
50fn resolve_runtime(
51 required: u8,
52 runtimes: &[JavaRuntime],
53) -> Result<PathBuf, McError> {
54 let mut candidates: Vec<&JavaRuntime> = runtimes
55 .iter()
56 .filter(|r| r.major >= required)
57 .collect();
58
59 candidates.sort_by_key(|r| r.major);
60
61 let runtime = candidates.first().ok_or_else(|| {
62 McError::Runtime(format!(
63 "No suitable Java runtime found (required: Java {})",
64 required
65 ))
66 })?;
67
68 runtime.validate()?;
69 Ok(runtime.path.clone())
70}
71
72fn build_classpath(config: &Config, version: &Version) -> String {
73 let system_separator: &str = if cfg!(windows) { ";" } else { ":" };
74 let mut entries: Vec<String> = Vec::new();
18 75
19 for library in &version.libraries { 76 for library in &version.libraries {
20 if !library_allowed(library) { 77 if !library_allowed(library) {
21 continue; 78 continue;
22 } 79 }
80
23 if let Some(artifact) = &library.downloads.artifact { 81 if let Some(artifact) = &library.downloads.artifact {
24 let path = paths::library_file(config, &artifact.path)?; 82 let path = library_file(config, &artifact.path);
25 entries.push(path.to_string_lossy().to_string()); 83 entries.push(path.to_string_lossy().to_string());
26 } 84 }
27 } 85 }
28 86
29 let client_jar = paths::client_jar(config, &version.id)?; 87 let client: PathBuf = client_jar(config, &version.id);
30 entries.push(client_jar.to_string_lossy().to_string()); 88 entries.push(client.to_string_lossy().to_string());
31 89
32 Ok(entries.join(sep)) 90 entries.join(system_separator)
33} 91}
34 92
35pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { 93fn build_command(
36 let java = &config.java_path; 94 java_path: PathBuf,
37 let classpath = build_classpath(config, version)?; 95 config: &Config,
38 let natives_dir = paths::natives_dir(config, &version.id); 96 version: &Version,
39 97 natives_dir: &Path,
40 if !natives_dir.exists() { 98 classpath: String,
41 return Err(McError::Runtime(format!( 99 asset_index_id: String,
42 "Natives folder does not exist: {}", 100) -> Command {
43 natives_dir.display() 101 let mut cmd: Command = Command::new(java_path);
44 )));
45 }
46
47 let asset_index_id = version
48 .asset_index
49 .as_ref()
50 .ok_or_else(|| {
51 McError::Runtime("Missing assetIndex in version.json".into())
52 })?
53 .id
54 .clone();
55
56 info!("Launching Minecraft {}", version.id);
57 debug!("Classpath: {}", classpath);
58 debug!("Natives: {}", natives_dir.display());
59 debug!("Asset index: {}", asset_index_id);
60
61 let mut cmd = Command::new(java);
62 102
63 cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) 103 cmd.arg(format!("-Xmx{}M", config.max_memory_mb))
64 .arg(format!("-Djava.library.path={}", natives_dir.display())); 104 .arg(format!("-Djava.library.path={}", natives_dir.display()));
@@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
76 .arg("--version") 116 .arg("--version")
77 .arg(&version.id) 117 .arg(&version.id)
78 .arg("--gameDir") 118 .arg("--gameDir")
79 .arg(paths::game_dir(config)) 119 .arg(paths::game_directory(config))
80 .arg("--assetsDir") 120 .arg("--assetsDir")
81 .arg(paths::assets_dir(config)) 121 .arg(paths::assets_directory(config))
82 .arg("--assetIndex") 122 .arg("--assetIndex")
83 .arg(&asset_index_id) 123 .arg(asset_index_id)
84 .arg("--uuid") 124 .arg("--uuid")
85 .arg(&config.uuid) 125 .arg(&config.uuid)
86 .arg("--userProperties") 126 .arg("--userProperties")
@@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
90 .arg("--userType") 130 .arg("--userType")
91 .arg("legacy"); 131 .arg("legacy");
92 132
93 let status = cmd.status()?; 133 cmd
134}
135
136pub fn launch(config: &Config, version: &Version) -> Result<(), McError> {
137 let required: u8 = required_java_major(
138 version
139 .java_version
140 .as_ref()
141 .map(|j| j.major_version),
142 );
143
144 let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?;
145
146 let classpath: String = build_classpath(config, version);
147 let natives_dir: PathBuf = natives_dir(config, &version.id);
148
149 if !natives_dir.exists() {
150 return Err(McError::Runtime(format!(
151 "Natives folder does not exist: {}",
152 natives_dir.display()
153 )));
154 }
155
156 let asset_index_id = version
157 .asset_index
158 .as_ref()
159 .ok_or_else(|| {
160 McError::Runtime("Missing assetIndex in version.json".into())
161 })?
162 .id
163 .clone();
164
165 let mut cmd: Command = build_command(
166 java_path,
167 config,
168 version,
169 &natives_dir,
170 classpath,
171 asset_index_id,
172 );
173
174 let status: ExitStatus = cmd.status()?;
94 175
95 if !status.success() { 176 if !status.success() {
96 return Err(McError::Process("Minecraft exited with error".into())); 177 return Err(Process("Minecraft exited with error".into()));
97 } 178 }
98 179
99 Ok(()) 180 Ok(())
100} 181}
101 182
102fn library_allowed(lib: &Library) -> bool { 183#[cfg(test)]
103 let rules = match &lib.rules { 184mod tests {
104 | Some(r) => r, 185 use super::*;
105 | None => return true, 186 use std::path::PathBuf;
106 }; 187
188 #[test]
189 fn required_java_major_defaults_to_8() {
190 assert_eq!(required_java_major(None), 8);
191 }
192
193 #[test]
194 fn required_java_major_uses_given_value() {
195 assert_eq!(required_java_major(Some(17)), 17);
196 }
197
198 #[test]
199 fn resolve_runtime_errors_when_no_runtimes() {
200 let runtimes: Vec<JavaRuntime> = vec![];
201 assert!(resolve_runtime(17, &runtimes).is_err());
202 }
203
204 #[test]
205 fn resolve_runtime_picks_lowest_matching_major() {
206 let runtimes: Vec<JavaRuntime> = vec![
207 JavaRuntime {
208 major: 21,
209 path: PathBuf::from("/bin/true"),
210 },
211 JavaRuntime {
212 major: 17,
213 path: PathBuf::from("/bin/true"),
214 },
215 ];
216
217 let result: PathBuf = resolve_runtime(8, &runtimes).unwrap();
218 assert_eq!(result, PathBuf::from("/bin/true"));
219 }
107 220
108 let mut allowed = false; 221 #[test]
222 fn resolve_runtime_respects_required_version() {
223 let runtimes: Vec<JavaRuntime> = vec![
224 JavaRuntime {
225 major: 8,
226 path: PathBuf::from("/bin/true"),
227 },
228 JavaRuntime {
229 major: 17,
230 path: PathBuf::from("/bin/true"),
231 },
232 ];
233
234 let result: PathBuf = resolve_runtime(17, &runtimes).unwrap();
235 assert_eq!(result, PathBuf::from("/bin/true"));
236 }
109 237
110 for rule in rules { 238 #[test]
111 let os_match = match &rule.os { 239 fn validate_succeeds_for_true_binary() {
112 | Some(os) => os.name == "linux", 240 let runtime = JavaRuntime {
113 | None => true, 241 major: 17,
242 path: PathBuf::from("/bin/true"),
114 }; 243 };
115 if os_match { 244
116 allowed = rule.action == "allow"; 245 assert!(runtime.validate().is_ok());
117 }
118 } 246 }
119 247
120 allowed 248 #[test]
249 fn validate_fails_for_false_binary() {
250 let runtime = JavaRuntime {
251 major: 17,
252 path: PathBuf::from("/bin/false"),
253 };
254
255 assert!(runtime.validate().is_err());
256 }
121} 257}