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