nap_core/server/
install.rs1use anyhow::{Context, Result};
9use std::fs;
10use std::path::Path;
11use std::process::Command;
12use tracing::{error, info};
13
14pub struct LoreInstaller {
16 install_dir: std::path::PathBuf,
17 repo: String,
18 version: String,
19}
20
21impl LoreInstaller {
22 pub fn new(install_dir: &Path) -> Self {
24 Self {
25 install_dir: install_dir.to_path_buf(),
26 repo: "EpicGames/lore".to_string(),
27 version: "latest".to_string(),
28 }
29 }
30
31 pub fn with_repo(mut self, repo: &str) -> Self {
33 self.repo = repo.to_string();
34 self
35 }
36
37 pub fn with_version(mut self, version: &str) -> Self {
39 self.version = version.to_string();
40 self
41 }
42
43 pub fn install_cli(&self) -> Result<()> {
45 info!(
46 "Installing Lore CLI from {} version {}",
47 self.repo, self.version
48 );
49
50 self.run_install_script(&["--version", &self.version])?;
51
52 info!("Lore CLI installed successfully");
53 Ok(())
54 }
55
56 pub fn install_server(&self) -> Result<()> {
58 info!(
59 "Installing Lore server from {} version {}",
60 self.repo, self.version
61 );
62
63 self.run_install_script(&["--server", "--version", &self.version])?;
64
65 info!("Lore server installed successfully");
66 Ok(())
67 }
68
69 pub fn install_all(&self) -> Result<()> {
71 info!(
72 "Installing Lore CLI and server from {} version {}",
73 self.repo, self.version
74 );
75
76 self.run_install_script(&["--version", &self.version])?;
77
78 info!("Lore CLI and server installed successfully");
79 Ok(())
80 }
81
82 fn run_install_script(&self, args: &[&str]) -> Result<()> {
84 let script_url = format!(
85 "https://raw.githubusercontent.com/{}/main/scripts/install.sh",
86 self.repo
87 );
88
89 let script_path = self.download_script(&script_url)?;
91
92 #[cfg(unix)]
94 {
95 use std::os::unix::fs::PermissionsExt;
96 let mut perms = fs::metadata(&script_path)?.permissions();
97 perms.set_mode(0o755);
98 fs::set_permissions(&script_path, perms)?;
99 }
100
101 let mut cmd_args = vec![
103 script_path.to_str().unwrap(),
104 "--install-dir",
105 self.install_dir.to_str().unwrap(),
106 ];
107 cmd_args.extend(args.iter().copied());
108
109 let output = Command::new("bash")
111 .args(&cmd_args)
112 .output()
113 .context("Failed to execute Lore install script")?;
114
115 if !output.status.success() {
116 let stderr = String::from_utf8_lossy(&output.stderr);
117 error!("Lore install script failed: {}", stderr);
118 anyhow::bail!("Lore install script failed with status: {}", output.status);
119 }
120
121 fs::remove_file(&script_path)?;
123
124 Ok(())
125 }
126
127 fn download_script(&self, url: &str) -> Result<std::path::PathBuf> {
129 let response =
130 reqwest::blocking::get(url).context("Failed to download Lore install script")?;
131
132 if !response.status().is_success() {
133 anyhow::bail!("Failed to download script: HTTP {}", response.status());
134 }
135
136 let script_content = response.text().context("Failed to read script content")?;
137
138 let temp_dir = std::env::temp_dir();
140 let script_path = temp_dir.join("lore-install.sh");
141 fs::write(&script_path, script_content).context("Failed to write install script")?;
142
143 Ok(script_path)
144 }
145
146 pub fn verify_installation(&self) -> Result<VerificationResult> {
148 let cli_installed = self.check_binary("lore");
149 let server_installed = self.check_binary("loreserver");
150
151 let cli_version = if cli_installed {
152 self.get_binary_version("lore").ok()
153 } else {
154 None
155 };
156
157 let server_version = if server_installed {
158 self.get_binary_version("loreserver").ok()
159 } else {
160 None
161 };
162
163 Ok(VerificationResult {
164 cli_installed,
165 cli_version,
166 server_installed,
167 server_version,
168 })
169 }
170
171 fn check_binary(&self, name: &str) -> bool {
173 let binary_path = self.install_dir.join(name);
174 binary_path.exists() && binary_path.is_file()
175 }
176
177 fn get_binary_version(&self, name: &str) -> Result<String> {
179 let binary_path = self.install_dir.join(name);
180 let output = Command::new(&binary_path)
181 .arg("--version")
182 .output()
183 .context(format!("Failed to execute {} --version", name))?;
184
185 if !output.status.success() {
186 anyhow::bail!("{} --version failed", name);
187 }
188
189 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
190 }
191
192 pub fn add_to_path(&self) -> Result<()> {
194 let install_dir_str = self
195 .install_dir
196 .to_str()
197 .context("Install directory path is not valid UTF-8")?;
198
199 if let Ok(current_path) = std::env::var("PATH")
201 && current_path.contains(install_dir_str)
202 {
203 info!("Install directory already in PATH");
204 return Ok(());
205 }
206
207 let new_path = format!(
209 "{}:{}",
210 install_dir_str,
211 std::env::var("PATH").unwrap_or_default()
212 );
213 unsafe {
214 std::env::set_var("PATH", &new_path);
215 }
216
217 info!("Added {} to PATH for current process", install_dir_str);
218 Ok(())
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct VerificationResult {
225 pub cli_installed: bool,
226 pub cli_version: Option<String>,
227 pub server_installed: bool,
228 pub server_version: Option<String>,
229}
230
231impl VerificationResult {
232 pub fn is_complete(&self) -> bool {
234 self.cli_installed && self.server_installed
235 }
236
237 pub fn status_message(&self) -> String {
239 let mut parts = vec![];
240
241 if self.cli_installed {
242 parts.push(format!(
243 "Lore CLI installed ({})",
244 self.cli_version.as_deref().unwrap_or("unknown")
245 ));
246 } else {
247 parts.push("Lore CLI not installed".to_string());
248 }
249
250 if self.server_installed {
251 parts.push(format!(
252 "Lore server installed ({})",
253 self.server_version.as_deref().unwrap_or("unknown")
254 ));
255 } else {
256 parts.push("Lore server not installed".to_string());
257 }
258
259 parts.join("; ")
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use tempfile::TempDir;
267
268 #[test]
269 fn test_installer_creation() {
270 let temp_dir = TempDir::new().unwrap();
271 let installer = LoreInstaller::new(temp_dir.path());
272 assert_eq!(installer.repo, "EpicGames/lore");
273 assert_eq!(installer.version, "latest");
274 }
275
276 #[test]
277 fn test_installer_custom_repo() {
278 let temp_dir = TempDir::new().unwrap();
279 let installer = LoreInstaller::new(temp_dir.path())
280 .with_repo("custom/repo")
281 .with_version("v1.0.0");
282 assert_eq!(installer.repo, "custom/repo");
283 assert_eq!(installer.version, "v1.0.0");
284 }
285
286 #[test]
287 fn test_verification_result() {
288 let result = VerificationResult {
289 cli_installed: true,
290 cli_version: Some("0.8.5-nightly".to_string()),
291 server_installed: false,
292 server_version: None,
293 };
294
295 assert!(!result.is_complete());
296 assert!(result.status_message().contains("Lore CLI installed"));
297 assert!(
298 result
299 .status_message()
300 .contains("Lore server not installed")
301 );
302 }
303}