aboutsummaryrefslogtreecommitdiffstats
path: root/src/minecraft/launcher.rs
diff options
context:
space:
mode:
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}