nap_core/server/
install.rs1use crate::PINNED_LORE_VERSION;
9use crate::server::error_ids;
10use anyhow::{Context, Result};
11use std::fs;
12use std::process::Command;
13use tracing::{error, info};
14use which;
15
16pub struct LoreInstaller {
18 install_dir: Option<std::path::PathBuf>,
19 repo: String,
20 version: String,
21}
22
23impl LoreInstaller {
24 pub fn new(install_dir: Option<std::path::PathBuf>) -> Self {
26 Self {
27 install_dir,
28 repo: "EpicGames/lore".to_string(),
29 version: PINNED_LORE_VERSION.to_string(),
30 }
31 }
32
33 pub fn with_repo(mut self, repo: &str) -> Self {
35 self.repo = repo.to_string();
36 self
37 }
38
39 pub fn with_version(mut self, version: &str) -> Self {
41 self.version = version.to_string();
42 self
43 }
44
45 fn tag_version(&self) -> String {
51 if self.version.starts_with('v') {
52 self.version.clone()
53 } else {
54 format!("v{}", self.version)
55 }
56 }
57
58 pub fn install_cli(&self) -> Result<()> {
60 if let Ok(verification) = self.verify_installation()
62 && verification.cli_installed
63 && let Some(installed_version) = &verification.cli_version
64 {
65 let installed_version_clean = installed_version
67 .split('+')
68 .next()
69 .unwrap_or(installed_version);
70 if installed_version_clean == self.version {
71 info!(
72 "Lore CLI already installed with correct version {}",
73 installed_version
74 );
75 return Ok(());
76 }
77 info!(
78 "Lore CLI installed but version mismatch: installed {}, required {}",
79 installed_version, self.version
80 );
81 }
82
83 info!(
84 "Installing Lore CLI from {} version {}",
85 self.repo, self.version
86 );
87
88 self.run_install_script(&["--version", &self.tag_version()])?;
89
90 info!("Lore CLI installed successfully");
91 Ok(())
92 }
93
94 pub fn install_server(&self) -> Result<()> {
96 if let Ok(verification) = self.verify_installation()
98 && verification.server_installed
99 && let Some(installed_version) = &verification.server_version
100 {
101 let installed_version_clean = installed_version
103 .split('+')
104 .next()
105 .unwrap_or(installed_version);
106 if installed_version_clean == self.version {
107 info!(
108 "Lore server already installed with correct version {}",
109 installed_version
110 );
111 return Ok(());
112 }
113 info!(
114 "Lore server installed but version mismatch: installed {}, required {}",
115 installed_version, self.version
116 );
117 }
118
119 info!(
120 "Installing Lore server from {} version {}",
121 self.repo, self.version
122 );
123
124 self.run_install_script(&["--server", "--version", &self.tag_version()])?;
125
126 info!("Lore server installed successfully");
127 Ok(())
128 }
129
130 pub fn install_all(&self) -> Result<()> {
132 info!(
133 "Checking Lore installation status for version {}",
134 self.version
135 );
136
137 self.install_cli()?;
142 self.install_server()?;
143
144 info!("Lore CLI and server installation verified");
145 Ok(())
146 }
147
148 fn run_install_script(&self, args: &[&str]) -> Result<()> {
150 let script_url = format!(
151 "https://raw.githubusercontent.com/{}/main/scripts/install.sh",
152 self.repo
153 );
154
155 let script_path = self.download_script(&script_url)?;
157
158 #[cfg(unix)]
160 {
161 use std::os::unix::fs::PermissionsExt;
162 let mut perms = fs::metadata(&script_path)?.permissions();
163 perms.set_mode(0o755);
164 fs::set_permissions(&script_path, perms)?;
165 }
166
167 let mut cmd_args = vec![script_path.to_str().unwrap()];
169 if let Some(dir) = &self.install_dir {
170 cmd_args.push("--install-dir");
171 cmd_args.push(dir.to_str().unwrap());
172 }
173 cmd_args.extend(args.iter().copied());
174
175 let output = Command::new("bash")
177 .args(&cmd_args)
178 .output()
179 .context(format!(
180 "[{}] Failed to execute Lore install script",
181 error_ids::ERR_LORE_INSTALL_FAILED
182 ))?;
183
184 if !output.status.success() {
185 let stderr = String::from_utf8_lossy(&output.stderr);
186 error!(
187 "[{}] Lore install script failed: {}",
188 error_ids::ERR_LORE_INSTALL_FAILED,
189 stderr
190 );
191 anyhow::bail!(
192 "[{}] Lore install script failed with status: {}",
193 error_ids::ERR_LORE_INSTALL_FAILED,
194 output.status
195 );
196 }
197
198 fs::remove_file(&script_path)?;
200
201 Ok(())
202 }
203
204 fn download_script(&self, url: &str) -> Result<std::path::PathBuf> {
206 let response = reqwest::blocking::get(url).context(format!(
207 "[{}] Failed to download Lore install script",
208 error_ids::ERR_LORE_DOWNLOAD_FAILED
209 ))?;
210
211 if !response.status().is_success() {
212 anyhow::bail!(
213 "[{}] Failed to download script: HTTP {}",
214 error_ids::ERR_LORE_DOWNLOAD_FAILED,
215 response.status()
216 );
217 }
218
219 let script_content = response.text().context(format!(
220 "[{}] Failed to read script content",
221 error_ids::ERR_LORE_DOWNLOAD_FAILED
222 ))?;
223
224 let temp_dir = std::env::temp_dir();
226 let script_path = temp_dir.join("lore-install.sh");
227 fs::write(&script_path, script_content).context(format!(
228 "[{}] Failed to write install script",
229 error_ids::ERR_LORE_DOWNLOAD_FAILED
230 ))?;
231
232 Ok(script_path)
233 }
234
235 pub fn verify_installation(&self) -> Result<VerificationResult> {
237 let cli_installed = self.check_binary("lore");
238 let server_installed = self.check_binary("loreserver");
239
240 let cli_version = if cli_installed {
241 self.get_binary_version("lore").ok()
242 } else {
243 None
244 };
245
246 let server_version = if server_installed {
247 self.get_binary_version("loreserver").ok()
248 } else {
249 None
250 };
251
252 Ok(VerificationResult {
253 cli_installed,
254 cli_version,
255 server_installed,
256 server_version,
257 })
258 }
259
260 fn check_binary(&self, name: &str) -> bool {
262 if let Some(dir) = &self.install_dir {
263 let binary_path = dir.join(name);
264 binary_path.exists() && binary_path.is_file()
265 } else {
266 which::which(name).is_ok()
268 }
269 }
270
271 fn get_binary_version(&self, name: &str) -> Result<String> {
279 let binary_path = if let Some(dir) = &self.install_dir {
280 dir.join(name).to_str().unwrap().to_string()
281 } else {
282 name.to_string() };
284
285 let output = Command::new(&binary_path)
286 .arg("--version")
287 .output()
288 .context(format!("Failed to execute {} --version", binary_path))?;
289
290 if !output.status.success() {
291 anyhow::bail!("{} --version failed", name);
292 }
293
294 let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
295 Ok(parse_version_output(&raw))
296 }
297
298 pub fn add_to_path(&self) -> Result<()> {
300 let install_dir = if let Some(dir) = &self.install_dir {
301 dir
302 } else {
303 return Ok(()); };
305
306 let install_dir_str = install_dir
307 .to_str()
308 .context("Install directory path is not valid UTF-8")?;
309
310 if let Ok(current_path) = std::env::var("PATH")
312 && current_path.contains(install_dir_str)
313 {
314 info!("Install directory already in PATH");
315 return Ok(());
316 }
317
318 let new_path = format!(
320 "{}:{}",
321 install_dir_str,
322 std::env::var("PATH").unwrap_or_default()
323 );
324 unsafe {
325 std::env::set_var("PATH", &new_path);
326 }
327
328 info!("Added {} to PATH for current process", install_dir_str);
329 Ok(())
330 }
331}
332
333pub fn parse_version_output(raw: &str) -> String {
340 let raw = raw.trim();
341 if let Some(pos) = raw.rfind(' ') {
342 raw[pos + 1..].to_string()
344 } else {
345 raw.to_string()
346 }
347}
348
349#[derive(Debug, Clone)]
351pub struct VerificationResult {
352 pub cli_installed: bool,
353 pub cli_version: Option<String>,
354 pub server_installed: bool,
355 pub server_version: Option<String>,
356}
357
358impl VerificationResult {
359 pub fn is_complete(&self) -> bool {
361 self.cli_installed && self.server_installed
362 }
363
364 pub fn status_message(&self) -> String {
366 let mut parts = vec![];
367
368 if self.cli_installed {
369 parts.push(format!(
370 "Lore CLI installed ({})",
371 self.cli_version.as_deref().unwrap_or("unknown")
372 ));
373 } else {
374 parts.push("Lore CLI not installed".to_string());
375 }
376
377 if self.server_installed {
378 parts.push(format!(
379 "Lore server installed ({})",
380 self.server_version.as_deref().unwrap_or("unknown")
381 ));
382 } else {
383 parts.push("Lore server not installed".to_string());
384 }
385
386 parts.join("; ")
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use tempfile::TempDir;
394
395 #[test]
396 fn test_installer_creation() {
397 let temp_dir = TempDir::new().unwrap();
398 let installer = LoreInstaller::new(Some(temp_dir.path().to_path_buf()));
399 assert_eq!(installer.repo, "EpicGames/lore");
400 assert_eq!(installer.version, PINNED_LORE_VERSION);
401 assert_eq!(installer.tag_version(), format!("v{}", PINNED_LORE_VERSION));
403 }
404
405 #[test]
406 fn test_tag_version_prefix() {
407 let temp_dir = TempDir::new().unwrap();
408 let installer = LoreInstaller::new(Some(temp_dir.path().to_path_buf()));
409 assert_eq!(installer.tag_version(), "v0.8.4");
410
411 let installer2 =
413 LoreInstaller::new(Some(temp_dir.path().to_path_buf())).with_version("v1.0.0");
414 assert_eq!(installer2.tag_version(), "v1.0.0");
415 }
416
417 #[test]
418 fn test_parse_version_output() {
419 assert_eq!(parse_version_output("0.8.4+283"), "0.8.4+283");
420 assert_eq!(parse_version_output("lore 0.8.4+283"), "0.8.4+283");
421 assert_eq!(parse_version_output("loreserver 0.8.4+283"), "0.8.4+283");
422 assert_eq!(parse_version_output("my-tool 1.2.3"), "1.2.3");
423 assert_eq!(parse_version_output("some-tool"), "some-tool");
424 }
425
426 #[test]
427 fn test_installer_custom_repo() {
428 let temp_dir = TempDir::new().unwrap();
429 let installer = LoreInstaller::new(Some(temp_dir.path().to_path_buf()))
430 .with_repo("custom/repo")
431 .with_version("v1.0.0");
432 assert_eq!(installer.repo, "custom/repo");
433 assert_eq!(installer.version, "v1.0.0");
434 }
435
436 #[test]
437 fn test_verification_result() {
438 let result = VerificationResult {
439 cli_installed: true,
440 cli_version: Some("0.8.4".to_string()),
441 server_installed: false,
442 server_version: None,
443 };
444
445 assert!(!result.is_complete());
446 assert!(result.status_message().contains("Lore CLI installed"));
447 assert!(
448 result
449 .status_message()
450 .contains("Lore server not installed")
451 );
452 }
453}