Rust · 14180 bytes Raw Blame History
1 //! WeMod installation into Wine prefixes
2
3 use crate::error::{Result, WandaError};
4 use crate::prefix::WandaPrefix;
5 use crate::steam::ProtonVersion;
6 use std::collections::HashMap;
7 use std::path::Path;
8 use std::process::Stdio;
9 use std::time::Duration;
10 use tokio::process::Command;
11 use tokio::time::timeout;
12 use tracing::{debug, error, info, warn};
13
14 /// Timeout for WeMod installer (2 minutes should be plenty)
15 const INSTALLER_TIMEOUT: Duration = Duration::from_secs(120);
16
17 /// Handles WeMod installation into Wine prefixes
18 pub struct WemodInstaller<'a> {
19 /// The prefix to install into
20 prefix: &'a WandaPrefix,
21 /// Proton version for running installer
22 proton: &'a ProtonVersion,
23 }
24
25 impl<'a> WemodInstaller<'a> {
26 /// Create a new installer
27 pub fn new(prefix: &'a WandaPrefix, proton: &'a ProtonVersion) -> Self {
28 Self { prefix, proton }
29 }
30
31 /// Build environment variables for Wine
32 fn build_env(&self) -> HashMap<String, String> {
33 let mut env = HashMap::new();
34
35 // Use the pfx subdirectory as the Wine prefix (Proton convention)
36 env.insert(
37 "WINEPREFIX".to_string(),
38 self.prefix.pfx_path().to_string_lossy().to_string(),
39 );
40 env.insert("WINEARCH".to_string(), "win64".to_string());
41 // Disable Wine debug output to prevent OOM from massive log accumulation
42 env.insert("WINEDEBUG".to_string(), "-all".to_string());
43
44 // Proton compatibility flags
45 env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
46 env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
47
48 // Pass display variables for GUI (required for WeMod installer)
49 if let Ok(disp) = std::env::var("DISPLAY") {
50 info!("Captured DISPLAY={}", disp);
51 env.insert("DISPLAY".to_string(), disp);
52 } else {
53 warn!("DISPLAY not set in environment!");
54 }
55 if let Ok(wayland_disp) = std::env::var("WAYLAND_DISPLAY") {
56 info!("Captured WAYLAND_DISPLAY={}", wayland_disp);
57 env.insert("WAYLAND_DISPLAY".to_string(), wayland_disp);
58 }
59 if let Ok(xdg_dir) = std::env::var("XDG_RUNTIME_DIR") {
60 info!("Captured XDG_RUNTIME_DIR={}", xdg_dir);
61 env.insert("XDG_RUNTIME_DIR".to_string(), xdg_dir);
62 }
63
64 // Set Proton lib paths for finding dependencies
65 let proton_lib64 = self.proton.path.join("files/lib64");
66 let proton_lib = self.proton.path.join("files/lib");
67 if proton_lib64.exists() || proton_lib.exists() {
68 let mut ld_path = String::new();
69 if proton_lib64.exists() {
70 ld_path.push_str(&proton_lib64.to_string_lossy());
71 }
72 if proton_lib.exists() {
73 if !ld_path.is_empty() {
74 ld_path.push(':');
75 }
76 ld_path.push_str(&proton_lib.to_string_lossy());
77 }
78 if let Ok(existing) = std::env::var("LD_LIBRARY_PATH") {
79 ld_path.push(':');
80 ld_path.push_str(&existing);
81 }
82 env.insert("LD_LIBRARY_PATH".to_string(), ld_path);
83 }
84
85 debug!("Installer environment:");
86 for (key, value) in &env {
87 debug!(" {}={}", key, value);
88 }
89
90 env
91 }
92
93 /// Get the path to wine executable
94 fn wine_exe(&self) -> String {
95 let proton_wine = self.proton.wine_exe();
96 debug!("Checking Proton wine at: {}", proton_wine.display());
97 if proton_wine.exists() {
98 debug!("Using Proton wine");
99 proton_wine.to_string_lossy().to_string()
100 } else {
101 warn!("Proton wine not found, falling back to system wine");
102 "wine".to_string()
103 }
104 }
105
106 /// Get the path to wineserver executable
107 fn wineserver_exe(&self) -> String {
108 let proton_wineserver = self.proton.path.join("files/bin/wineserver");
109 if proton_wineserver.exists() {
110 proton_wineserver.to_string_lossy().to_string()
111 } else {
112 "wineserver".to_string()
113 }
114 }
115
116 /// Install WeMod from the downloaded installer
117 pub async fn install(&self, installer_path: &Path) -> Result<()> {
118 if !installer_path.exists() {
119 return Err(WandaError::WemodInstallFailed {
120 reason: format!("Installer not found at {}", installer_path.display()),
121 });
122 }
123
124 info!("Installing WeMod from {}...", installer_path.display());
125
126 let env = self.build_env();
127 let wine = self.wine_exe();
128
129 info!("Using wine: {}", wine);
130 info!("Running: {} {}", wine, installer_path.display());
131
132 // WeMod uses Squirrel installer - run with silent flag to avoid GUI interaction
133 // Use inherited stdout/stderr so we can see installer output
134 // Add timeout to prevent indefinite hangs
135 info!("Running installer (timeout: {:?})...", INSTALLER_TIMEOUT);
136
137 let install_future = Command::new(&wine)
138 .arg(installer_path)
139 .arg("--silent") // Squirrel silent install flag
140 .envs(&env)
141 .stdout(Stdio::inherit())
142 .stderr(Stdio::inherit())
143 .status();
144
145 match timeout(INSTALLER_TIMEOUT, install_future).await {
146 Ok(Ok(status)) => {
147 if !status.success() {
148 warn!("WeMod installer exited with status: {:?}", status.code());
149 warn!("Installation may still have succeeded - checking...");
150 } else {
151 debug!("Installer completed with success status");
152 }
153 }
154 Ok(Err(e)) => {
155 return Err(WandaError::WemodInstallFailed {
156 reason: format!("Failed to run installer: {}", e),
157 });
158 }
159 Err(_) => {
160 warn!("Installer timed out after {:?}", INSTALLER_TIMEOUT);
161 warn!("Checking if installation succeeded anyway...");
162 }
163 }
164
165 // Wait for wineserver using Proton's wineserver (with timeout)
166 let wineserver = self.wineserver_exe();
167 debug!("Waiting for wineserver: {}", wineserver);
168 let wineserver_wait = Command::new(&wineserver)
169 .arg("-w")
170 .envs(&env)
171 .status();
172
173 if timeout(Duration::from_secs(30), wineserver_wait).await.is_err() {
174 warn!("Wineserver wait timed out - continuing anyway");
175 }
176
177 // Verify installation
178 info!("Verifying WeMod installation...");
179 if self.verify()? {
180 info!("WeMod installed successfully");
181 Ok(())
182 } else {
183 error!("WeMod executable not found after installation");
184 error!("Checked locations:");
185 error!(" - {}", self.prefix.wemod_exe().display());
186 error!(" - {}", self.prefix.user_folder().join("AppData/Local/WeMod/WeMod.exe").display());
187 error!(" - {}", self.prefix.drive_c().join("Program Files/WeMod/WeMod.exe").display());
188 Err(WandaError::WemodInstallFailed {
189 reason: "WeMod executable not found after installation".to_string(),
190 })
191 }
192 }
193
194 /// Verify WeMod installation
195 pub fn verify(&self) -> Result<bool> {
196 // Check if WeMod.exe exists
197 let wemod_exe = self.prefix.wemod_exe();
198 debug!("Checking for WeMod at: {}", wemod_exe.display());
199
200 if wemod_exe.exists() {
201 info!("WeMod found at {}", wemod_exe.display());
202 return Ok(true);
203 }
204
205 // Also check alternative locations (WeMod rebranded to "Wand")
206 let alt_locations = [
207 self.prefix
208 .user_folder()
209 .join("AppData/Local/Wand/WeMod.exe"),
210 self.prefix
211 .user_folder()
212 .join("AppData/Local/WeMod/WeMod.exe"),
213 self.prefix
214 .drive_c()
215 .join("Program Files/WeMod/WeMod.exe"),
216 self.prefix
217 .drive_c()
218 .join("Program Files (x86)/WeMod/WeMod.exe"),
219 ];
220
221 for loc in &alt_locations {
222 debug!("Checking alternative location: {}", loc.display());
223 if loc.exists() {
224 info!("WeMod found at alternative location: {}", loc.display());
225 return Ok(true);
226 }
227 }
228
229 // List what's in the WeMod directory if it exists
230 let wemod_dir = self.prefix.wemod_path();
231 debug!("WeMod directory: {}", wemod_dir.display());
232 if wemod_dir.exists() {
233 debug!("WeMod directory exists, contents:");
234 if let Ok(entries) = std::fs::read_dir(&wemod_dir) {
235 for entry in entries.flatten() {
236 debug!(" - {}", entry.path().display());
237 }
238 }
239 } else {
240 debug!("WeMod directory does not exist");
241 }
242
243 // Also check what's in AppData/Local
244 let local_appdata = self.prefix.user_folder().join("AppData/Local");
245 if local_appdata.exists() {
246 debug!("Listing AppData/Local contents:");
247 if let Ok(entries) = std::fs::read_dir(&local_appdata) {
248 for entry in entries.flatten() {
249 debug!(" - {}", entry.file_name().to_string_lossy());
250 }
251 }
252 }
253
254 debug!("WeMod not found in prefix");
255 Ok(false)
256 }
257
258 /// Get the installed WeMod version (if available)
259 pub fn get_version(&self) -> Option<String> {
260 // Try to read version from WeMod's app data
261 let version_file = self.prefix.wemod_path().join("current");
262 if version_file.exists() {
263 if let Ok(content) = std::fs::read_to_string(&version_file) {
264 // The 'current' file contains the version directory name
265 let version = content.trim().to_string();
266 if !version.is_empty() {
267 return Some(version);
268 }
269 }
270 }
271
272 // Try to detect from installed app directories
273 let wemod_dir = self.prefix.wemod_path();
274 if wemod_dir.exists() {
275 if let Ok(entries) = std::fs::read_dir(&wemod_dir) {
276 for entry in entries.flatten() {
277 let name = entry.file_name().to_string_lossy().to_string();
278 // Look for version directories like "app-8.12.5"
279 if name.starts_with("app-") {
280 return Some(name.strip_prefix("app-").unwrap_or(&name).to_string());
281 }
282 }
283 }
284 }
285
286 None
287 }
288
289 /// Uninstall WeMod from the prefix
290 pub async fn uninstall(&self) -> Result<()> {
291 info!("Uninstalling WeMod...");
292
293 let wemod_dir = self.prefix.wemod_path();
294 if wemod_dir.exists() {
295 tokio::fs::remove_dir_all(&wemod_dir).await?;
296 info!("WeMod uninstalled");
297 } else {
298 info!("WeMod was not installed");
299 }
300
301 Ok(())
302 }
303
304 /// Run WeMod standalone (for testing or manual use)
305 pub async fn run(&self) -> Result<tokio::process::Child> {
306 // Find WeMod executable - check app directory first, then root
307 let wemod_path = self.prefix.wemod_path();
308 let wemod_exe = self.find_wemod_exe(&wemod_path)?;
309
310 let mut env = self.build_env();
311 let wine = self.wine_exe();
312
313 // Add Proton library paths for proper DLL loading
314 let proton_lib64 = self.proton.path.join("files/lib64");
315 let proton_lib = self.proton.path.join("files/lib");
316 let mut ld_path = String::new();
317 if proton_lib64.exists() {
318 ld_path.push_str(&proton_lib64.to_string_lossy());
319 }
320 if proton_lib.exists() {
321 if !ld_path.is_empty() {
322 ld_path.push(':');
323 }
324 ld_path.push_str(&proton_lib.to_string_lossy());
325 }
326 if let Ok(existing) = std::env::var("LD_LIBRARY_PATH") {
327 if !ld_path.is_empty() {
328 ld_path.push(':');
329 }
330 ld_path.push_str(&existing);
331 }
332 if !ld_path.is_empty() {
333 env.insert("LD_LIBRARY_PATH".to_string(), ld_path);
334 }
335
336 info!("Starting WeMod from: {}", wemod_exe.display());
337 info!("Using wine: {}", wine);
338
339 // Run WeMod with minimal flags for Wine/Proton compatibility
340 // Only --no-sandbox is required; additional GPU flags can cause issues
341 let child = Command::new(&wine)
342 .arg(&wemod_exe)
343 .arg("--no-sandbox") // Required for Electron under Wine
344 .envs(&env)
345 .stdout(Stdio::inherit())
346 .stderr(Stdio::inherit())
347 .spawn()
348 .map_err(|e| WandaError::LaunchFailed {
349 reason: format!("Failed to start WeMod: {}", e),
350 })?;
351
352 Ok(child)
353 }
354
355 /// Find the WeMod executable (handles both app-X.Y.Z and root locations)
356 fn find_wemod_exe(&self, wemod_path: &std::path::Path) -> Result<std::path::PathBuf> {
357 // First check for versioned app directory (e.g., app-11.5.0/WeMod.exe)
358 if let Ok(entries) = std::fs::read_dir(wemod_path) {
359 for entry in entries.flatten() {
360 let name = entry.file_name().to_string_lossy().to_string();
361 if name.starts_with("app-") && entry.path().is_dir() {
362 let app_exe = entry.path().join("WeMod.exe");
363 if app_exe.exists() {
364 debug!("Found WeMod in app directory: {}", app_exe.display());
365 return Ok(app_exe);
366 }
367 }
368 }
369 }
370
371 // Fall back to root WeMod.exe (launcher)
372 let root_exe = wemod_path.join("WeMod.exe");
373 if root_exe.exists() {
374 debug!("Using root WeMod.exe: {}", root_exe.display());
375 return Ok(root_exe);
376 }
377
378 Err(WandaError::WemodNotInstalled)
379 }
380 }
381