Rust · 25618 bytes Raw Blame History
1 //! Prefix building and initialization
2
3 use crate::error::{Result, WandaError};
4 use crate::steam::ProtonVersion;
5 use futures::StreamExt;
6 use std::collections::HashMap;
7 use std::path::{Path, PathBuf};
8 use std::time::Duration;
9 use tokio::process::Command;
10 use tokio::time::timeout;
11 use tracing::{debug, error, info, warn};
12
13 /// Microsoft .NET Framework 4.8 offline installer (stable URL)
14 const DOTNET48_URL: &str = "https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe";
15 const DOTNET48_FILENAME: &str = "ndp48-x86-x64-allos-enu.exe";
16 /// Timeout for the .NET installer (5 minutes)
17 const DOTNET_INSTALL_TIMEOUT: Duration = Duration::from_secs(300);
18
19 /// Builds and initializes Wine prefixes for WeMod
20 pub struct PrefixBuilder<'a> {
21 /// Base path for the wanda prefix (STEAM_COMPAT_DATA_PATH equivalent)
22 base_path: PathBuf,
23 /// Actual Wine prefix path (pfx subdirectory, used by Proton)
24 prefix_path: PathBuf,
25 /// Proton version to use
26 proton: &'a ProtonVersion,
27 }
28
29 impl<'a> PrefixBuilder<'a> {
30 /// Create a new prefix builder
31 ///
32 /// Note: Proton creates a `pfx` subdirectory inside the base path for the actual
33 /// Wine prefix. We install dependencies (like .NET) into the pfx directory so
34 /// they're available when running apps through Proton.
35 pub fn new(base_path: &Path, proton: &'a ProtonVersion) -> Self {
36 Self {
37 base_path: base_path.to_path_buf(),
38 prefix_path: base_path.join("pfx"),
39 proton,
40 }
41 }
42
43 /// Get the base path (STEAM_COMPAT_DATA_PATH equivalent)
44 pub fn base_path(&self) -> &Path {
45 &self.base_path
46 }
47
48 /// Get the actual Wine prefix path (pfx subdirectory)
49 pub fn wine_prefix_path(&self) -> &Path {
50 &self.prefix_path
51 }
52
53 /// Get path to Wine executable (prefer Proton's bundled wine)
54 fn get_wine_path(&self) -> PathBuf {
55 // Try Proton's wine64 first
56 let proton_wine = self.proton.path.join("files/bin/wine64");
57 debug!("Checking for Proton wine64 at: {}", proton_wine.display());
58 if proton_wine.exists() {
59 debug!("Found Proton wine64");
60 return proton_wine;
61 }
62
63 // Try Proton's wine
64 let proton_wine = self.proton.path.join("files/bin/wine");
65 debug!("Checking for Proton wine at: {}", proton_wine.display());
66 if proton_wine.exists() {
67 debug!("Found Proton wine");
68 return proton_wine;
69 }
70
71 // Fall back to system wine
72 warn!("No Proton wine found, falling back to system wine");
73 PathBuf::from("wine")
74 }
75
76 /// Build environment variables for Wine/Proton commands
77 fn build_env(&self) -> HashMap<String, String> {
78 let mut env = HashMap::new();
79
80 let wine_path = self.get_wine_path();
81 debug!("Building environment for Wine at: {}", wine_path.display());
82
83 // Wine prefix location
84 env.insert(
85 "WINEPREFIX".to_string(),
86 self.prefix_path.to_string_lossy().to_string(),
87 );
88
89 // Use 64-bit Windows
90 env.insert("WINEARCH".to_string(), "win64".to_string());
91
92 // Tell winetricks which wine to use
93 env.insert("WINE".to_string(), wine_path.to_string_lossy().to_string());
94
95 // Tell winetricks which wineserver to use (must match wine version)
96 let wineserver_path = self.proton.path.join("files/bin/wineserver");
97 if wineserver_path.exists() {
98 debug!("Using Proton wineserver: {}", wineserver_path.display());
99 env.insert("WINESERVER".to_string(), wineserver_path.to_string_lossy().to_string());
100 }
101
102 // Proton lib paths for finding dependencies
103 let proton_lib64 = self.proton.path.join("files/lib64");
104 let proton_lib = self.proton.path.join("files/lib");
105 debug!("Checking Proton lib64: {} (exists: {})", proton_lib64.display(), proton_lib64.exists());
106 debug!("Checking Proton lib: {} (exists: {})", proton_lib.display(), proton_lib.exists());
107
108 if proton_lib64.exists() || proton_lib.exists() {
109 let mut ld_path = String::new();
110 if proton_lib64.exists() {
111 ld_path.push_str(&proton_lib64.to_string_lossy());
112 }
113 if proton_lib.exists() {
114 if !ld_path.is_empty() {
115 ld_path.push(':');
116 }
117 ld_path.push_str(&proton_lib.to_string_lossy());
118 }
119 // Append existing LD_LIBRARY_PATH
120 if let Ok(existing) = std::env::var("LD_LIBRARY_PATH") {
121 ld_path.push(':');
122 ld_path.push_str(&existing);
123 }
124 debug!("Setting LD_LIBRARY_PATH: {}", ld_path);
125 env.insert("LD_LIBRARY_PATH".to_string(), ld_path);
126 } else {
127 warn!("No Proton lib directories found - Wine may have trouble finding libraries");
128 }
129
130 // Proton-specific variables
131 env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
132 env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
133
134 // Disable Wine debug output to prevent OOM from massive log accumulation
135 env.insert("WINEDEBUG".to_string(), "-all".to_string());
136
137 // Pass display variables for GUI installers (required for .NET, vcrun, etc.)
138 if let Ok(disp) = std::env::var("DISPLAY") {
139 info!("Captured DISPLAY={}", disp);
140 env.insert("DISPLAY".to_string(), disp);
141 } else {
142 warn!("DISPLAY not set in environment!");
143 }
144 if let Ok(wayland_disp) = std::env::var("WAYLAND_DISPLAY") {
145 info!("Captured WAYLAND_DISPLAY={}", wayland_disp);
146 env.insert("WAYLAND_DISPLAY".to_string(), wayland_disp);
147 }
148 if let Ok(xdg_dir) = std::env::var("XDG_RUNTIME_DIR") {
149 info!("Captured XDG_RUNTIME_DIR={}", xdg_dir);
150 env.insert("XDG_RUNTIME_DIR".to_string(), xdg_dir);
151 }
152
153 // Log all environment variables at trace level
154 for (key, value) in &env {
155 debug!("ENV {}={}", key, value);
156 }
157
158 env
159 }
160
161 /// Build the prefix from scratch
162 pub async fn build(&self) -> Result<()> {
163 info!("Building prefix at {}", self.base_path.display());
164 info!("Wine prefix (pfx): {}", self.prefix_path.display());
165 info!("Using Proton: {}", self.proton.name);
166 info!("Wine path: {}", self.get_wine_path().display());
167
168 // Create directory structure (both base and pfx)
169 std::fs::create_dir_all(&self.base_path)?;
170 std::fs::create_dir_all(&self.prefix_path)?;
171
172 // Initialize with wineboot
173 self.init_wineboot().await?;
174
175 // Patch mscorlib.dll version so WeMod's .NET check passes.
176 // WeMod reads the PE FileVersion from mscorlib.dll and requires >= 4.7.2556.0.
177 // Wine Mono ships 4.0.30319.1 which fails the check. We patch the version
178 // resource to report 4.8.9232.0 (.NET 4.8) — Wine Mono's runtime still works
179 // fine, this is purely metadata in the PE resource section.
180 self.patch_mscorlib_version()?;
181
182 info!("Prefix built successfully");
183 Ok(())
184 }
185
186 /// Initialize the prefix with wineboot via Proton's script
187 ///
188 /// We must run through `./proton run wineboot` rather than calling wine64
189 /// directly so that Proton sets up its own prefix layout: steam.exe stubs,
190 /// DLL symlinks into the Proton distribution, lsteamclient, etc. Without
191 /// this, `proton waitforexitandrun` fails with STATUS_DLL_NOT_FOUND
192 /// (c0000135) because the symlinks point nowhere.
193 async fn init_wineboot(&self) -> Result<()> {
194 info!("Initializing Wine prefix via Proton script...");
195
196 let proton_script = self.proton.path.join("proton");
197 let env = self.build_proton_env();
198
199 info!(
200 "Running: ./proton run wineboot --init (from {})",
201 self.proton.path.display()
202 );
203
204 let output = Command::new(&proton_script)
205 .arg("run")
206 .arg("wineboot")
207 .arg("--init")
208 .current_dir(&self.proton.path)
209 .envs(&env)
210 .output()
211 .await
212 .map_err(|e| WandaError::PrefixCreationFailed {
213 path: self.base_path.clone(),
214 reason: format!("Proton wineboot failed to start: {}", e),
215 })?;
216
217 if !output.status.success() {
218 let stderr = String::from_utf8_lossy(&output.stderr);
219 let stdout = String::from_utf8_lossy(&output.stdout);
220 error!("proton wineboot stdout: {}", stdout);
221 error!("proton wineboot stderr: {}", stderr);
222 return Err(WandaError::PrefixCreationFailed {
223 path: self.base_path.clone(),
224 reason: format!(
225 "Proton wineboot failed with exit code: {:?}",
226 output.status.code()
227 ),
228 });
229 }
230
231 debug!("Proton wineboot completed successfully");
232
233 // Wait for wineserver to finish
234 self.wait_wineserver().await;
235
236 Ok(())
237 }
238
239 /// Build environment for running through the Proton script.
240 ///
241 /// Proton uses STEAM_COMPAT_DATA_PATH (not WINEPREFIX) and creates
242 /// the actual Wine prefix at `$STEAM_COMPAT_DATA_PATH/pfx/`.
243 fn build_proton_env(&self) -> HashMap<String, String> {
244 let mut env = HashMap::new();
245
246 // Proton manages WINEPREFIX internally from this path
247 env.insert(
248 "STEAM_COMPAT_DATA_PATH".to_string(),
249 self.base_path.to_string_lossy().to_string(),
250 );
251
252 // Try to find Steam root from Proton's location for Steam integration
253 if let Some(steam_root) = self.find_steam_root() {
254 debug!("Derived Steam root: {}", steam_root.display());
255 env.insert(
256 "STEAM_COMPAT_CLIENT_INSTALL_PATH".to_string(),
257 steam_root.to_string_lossy().to_string(),
258 );
259 }
260
261 env.insert("PROTON_NO_ESYNC".to_string(), "1".to_string());
262 env.insert("PROTON_NO_FSYNC".to_string(), "1".to_string());
263 env.insert("WINEDEBUG".to_string(), "-all".to_string());
264
265 // Display variables for GUI initialization
266 if let Ok(disp) = std::env::var("DISPLAY") {
267 env.insert("DISPLAY".to_string(), disp);
268 }
269 if let Ok(wayland) = std::env::var("WAYLAND_DISPLAY") {
270 env.insert("WAYLAND_DISPLAY".to_string(), wayland);
271 }
272 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
273 env.insert("XDG_RUNTIME_DIR".to_string(), xdg);
274 }
275
276 env
277 }
278
279 /// Walk up from Proton's path to find the Steam root directory.
280 ///
281 /// Proton lives under either:
282 /// `<steam>/compatibilitytools.d/<version>/` (GE-Proton, custom)
283 /// `<steam>/steamapps/common/<version>/` (official Proton)
284 fn find_steam_root(&self) -> Option<PathBuf> {
285 let mut current = self.proton.path.as_path();
286 for _ in 0..5 {
287 current = current.parent()?;
288 // Steam root has steam.sh (native) or ubuntu12_32 (runtime)
289 if current.join("steam.sh").exists()
290 || current.join("ubuntu12_32").exists()
291 || current.join("steamapps").exists()
292 {
293 return Some(current.to_path_buf());
294 }
295 }
296 None
297 }
298
299 /// Patch mscorlib.dll PE version info so WeMod's .NET check passes.
300 ///
301 /// WeMod parses the VS_FIXEDFILEINFO from mscorlib.dll and requires
302 /// FileVersion >= 4.7.2556.0. Wine Mono ships 4.0.30319.1 which fails.
303 /// We patch dwFileVersionMS/LS and dwProductVersionMS/LS to report
304 /// 4.8.9232.0 (.NET Framework 4.8). This is safe because the version
305 /// resource is metadata only — Wine Mono's actual runtime is unaffected.
306 pub fn patch_mscorlib_version(&self) -> Result<()> {
307 // .NET 4.8 version: 4.8.9232.0
308 let new_file_version_ms: u32 = (4 << 16) | 8; // major=4, minor=8
309 let new_file_version_ls: u32 = (9232 << 16) | 0; // build=9232, revision=0
310
311 let paths = [
312 self.prefix_path.join("drive_c/windows/Microsoft.NET/Framework64/v4.0.30319/mscorlib.dll"),
313 self.prefix_path.join("drive_c/windows/Microsoft.NET/Framework/v4.0.30319/mscorlib.dll"),
314 ];
315
316 let signature: [u8; 4] = 0xFEEF04BDu32.to_le_bytes();
317
318 for path in &paths {
319 if !path.exists() {
320 debug!("mscorlib.dll not found at {}, skipping", path.display());
321 continue;
322 }
323
324 let mut data = std::fs::read(path).map_err(|e| WandaError::PrefixCreationFailed {
325 path: path.clone(),
326 reason: format!("Failed to read mscorlib.dll: {}", e),
327 })?;
328
329 // Find VS_FIXEDFILEINFO signature
330 let pos = data
331 .windows(4)
332 .position(|w| w == signature);
333
334 let pos = match pos {
335 Some(p) => p,
336 None => {
337 warn!("VS_FIXEDFILEINFO signature not found in {}", path.display());
338 continue;
339 }
340 };
341
342 // Verify we have enough bytes after the signature
343 // Layout: signature(4) + struc_version(4) + file_ver_ms(4) + file_ver_ls(4)
344 // + product_ver_ms(4) + product_ver_ls(4)
345 if pos + 24 > data.len() {
346 warn!("VS_FIXEDFILEINFO too close to end of file in {}", path.display());
347 continue;
348 }
349
350 // Read current version for logging
351 let old_ms = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
352 let old_ls = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
353 info!(
354 "Patching {} version {}.{}.{}.{} -> 4.8.9232.0",
355 path.display(),
356 old_ms >> 16, old_ms & 0xFFFF,
357 old_ls >> 16, old_ls & 0xFFFF,
358 );
359
360 // Patch FileVersion (offset +8, +12 from signature)
361 data[pos + 8..pos + 12].copy_from_slice(&new_file_version_ms.to_le_bytes());
362 data[pos + 12..pos + 16].copy_from_slice(&new_file_version_ls.to_le_bytes());
363
364 // Patch ProductVersion (offset +16, +20 from signature)
365 data[pos + 16..pos + 20].copy_from_slice(&new_file_version_ms.to_le_bytes());
366 data[pos + 20..pos + 24].copy_from_slice(&new_file_version_ls.to_le_bytes());
367
368 std::fs::write(path, &data).map_err(|e| WandaError::PrefixCreationFailed {
369 path: path.clone(),
370 reason: format!("Failed to write patched mscorlib.dll: {}", e),
371 })?;
372 }
373
374 info!("mscorlib.dll version patched for WeMod compatibility");
375 Ok(())
376 }
377
378 /// Wait for wineserver to finish
379 async fn wait_wineserver(&self) {
380 let env = self.build_env();
381
382 // Try Proton's wineserver first
383 let proton_wineserver = self.proton.path.join("files/bin/wineserver");
384 let wineserver = if proton_wineserver.exists() {
385 proton_wineserver.to_string_lossy().to_string()
386 } else {
387 "wineserver".to_string()
388 };
389
390 let _ = Command::new(&wineserver)
391 .arg("-w")
392 .envs(&env)
393 .status()
394 .await;
395 }
396
397 /// Install .NET Framework 4.8 (required for WeMod's trainer engine)
398 ///
399 /// Downloads the official Microsoft offline installer and runs it directly
400 /// via Proton — avoids winetricks which corrupts 64-bit Proton prefixes
401 /// by changing the Windows version and removing Wine Mono.
402 pub async fn install_dotnet(&self) -> Result<()> {
403 info!("Installing .NET Framework 4.8 via direct Proton install...");
404
405 // Download the offline installer to cache
406 let cache_dir = dirs::cache_dir()
407 .unwrap_or_else(|| PathBuf::from("/tmp"))
408 .join("wanda/downloads");
409 std::fs::create_dir_all(&cache_dir)?;
410
411 let installer_path = cache_dir.join(DOTNET48_FILENAME);
412
413 if installer_path.exists() {
414 info!("Using cached .NET installer: {}", installer_path.display());
415 } else {
416 info!("Downloading .NET 4.8 offline installer...");
417 self.download_file(DOTNET48_URL, &installer_path).await?;
418 info!("Downloaded to: {}", installer_path.display());
419 }
420
421 // Run the installer via Proton's runinprefix verb.
422 // This uses Proton's wine with proper DLL overrides and library paths
423 // WITHOUT changing Windows version or removing Wine Mono.
424 let proton_script = self.proton.path.join("proton");
425 let env = self.build_proton_env();
426
427 info!("Running .NET installer via Proton (timeout: {:?})...", DOTNET_INSTALL_TIMEOUT);
428
429 let install_future = Command::new(&proton_script)
430 .arg("runinprefix")
431 .arg(&installer_path)
432 .arg("/q") // Quiet mode
433 .arg("/norestart") // Don't try to restart
434 .current_dir(&self.proton.path)
435 .envs(&env)
436 .stdout(std::process::Stdio::inherit())
437 .stderr(std::process::Stdio::inherit())
438 .status();
439
440 match timeout(DOTNET_INSTALL_TIMEOUT, install_future).await {
441 Ok(Ok(status)) => {
442 if status.success() {
443 info!(".NET 4.8 installed successfully");
444 } else {
445 let code = status.code();
446 // .NET installer exit codes:
447 // 0 = success, 1602 = user cancelled, 1603 = fatal error,
448 // 1641/3010 = success (reboot needed), 5100 = prerequisites
449 match code {
450 Some(1641) | Some(3010) => {
451 info!(".NET 4.8 installed (reboot requested but not needed under Wine)");
452 }
453 _ => {
454 warn!(".NET installer exited with code: {:?}", code);
455 return Err(WandaError::WinetricksFailed {
456 reason: format!(".NET 4.8 installer failed with exit code {:?}", code),
457 });
458 }
459 }
460 }
461 }
462 Ok(Err(e)) => {
463 return Err(WandaError::WinetricksFailed {
464 reason: format!("Failed to run .NET installer: {}", e),
465 });
466 }
467 Err(_) => {
468 warn!(".NET installer timed out after {:?}", DOTNET_INSTALL_TIMEOUT);
469 return Err(WandaError::WinetricksFailed {
470 reason: format!(".NET installer timed out after {:?}", DOTNET_INSTALL_TIMEOUT),
471 });
472 }
473 }
474
475 // Wait for wineserver to finish
476 self.wait_wineserver().await;
477
478 Ok(())
479 }
480
481 /// Download a file from a URL to a local path
482 async fn download_file(&self, url: &str, dest: &Path) -> Result<()> {
483 let client = reqwest::Client::builder()
484 .timeout(Duration::from_secs(600))
485 .build()
486 .map_err(|e| WandaError::WinetricksFailed {
487 reason: format!("Failed to create HTTP client: {}", e),
488 })?;
489
490 let response = client.get(url).send().await.map_err(|e| {
491 WandaError::WinetricksFailed {
492 reason: format!("Failed to download {}: {}", url, e),
493 }
494 })?;
495
496 if !response.status().is_success() {
497 return Err(WandaError::WinetricksFailed {
498 reason: format!("Download failed with HTTP {}", response.status()),
499 });
500 }
501
502 let total_size = response.content_length().unwrap_or(0);
503 info!("Downloading {} ({:.1} MB)...", DOTNET48_FILENAME, total_size as f64 / 1_048_576.0);
504
505 let tmp_path = dest.with_extension("tmp");
506 let mut file = tokio::fs::File::create(&tmp_path).await.map_err(|e| {
507 WandaError::WinetricksFailed {
508 reason: format!("Failed to create file: {}", e),
509 }
510 })?;
511
512 let mut stream = response.bytes_stream();
513 while let Some(chunk) = stream.next().await {
514 let chunk = chunk.map_err(|e| WandaError::WinetricksFailed {
515 reason: format!("Download interrupted: {}", e),
516 })?;
517 tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
518 .await
519 .map_err(|e| WandaError::WinetricksFailed {
520 reason: format!("Failed to write file: {}", e),
521 })?;
522 }
523
524 // Atomic rename
525 tokio::fs::rename(&tmp_path, dest).await.map_err(|e| {
526 WandaError::WinetricksFailed {
527 reason: format!("Failed to move downloaded file: {}", e),
528 }
529 })?;
530
531 Ok(())
532 }
533
534 /// Install additional dependencies
535 async fn install_dependencies(&self) -> Result<()> {
536 info!("Installing additional dependencies...");
537
538 // Install common dependencies WeMod needs
539 // - vcrun2019: Visual C++ runtime
540 // - corefonts: Windows fonts
541 // - winhttp/wininet: Network components for WeMod authentication
542 let deps = ["vcrun2019", "corefonts", "winhttp", "wininet"];
543
544 for dep in deps {
545 info!("Installing {}...", dep);
546 match self.install_winetricks_verbose(&[dep]).await {
547 Ok(_) => info!("{} installed successfully", dep),
548 Err(e) => warn!("Failed to install {}: {} (may not be critical)", dep, e),
549 }
550 }
551
552 Ok(())
553 }
554
555 /// Install components via winetricks with verbose output
556 pub async fn install_winetricks_verbose(&self, components: &[&str]) -> Result<()> {
557 let env = self.build_env();
558
559 // Check if winetricks is available
560 let winetricks_check = Command::new("which").arg("winetricks").output().await;
561
562 if winetricks_check.is_err() || !winetricks_check.unwrap().status.success() {
563 return Err(WandaError::WinetricksFailed {
564 reason: "winetricks not found. Please install winetricks.".to_string(),
565 });
566 }
567
568 for component in components {
569 info!("Installing {} via winetricks...", component);
570 debug!("Environment: WINE={}", env.get("WINE").unwrap_or(&"".to_string()));
571 debug!("Environment: WINEPREFIX={}", env.get("WINEPREFIX").unwrap_or(&"".to_string()));
572
573 // Run winetricks with output going directly to terminal (not collected in memory)
574 // This prevents OOM when installing large components like dotnet48
575 let status = Command::new("winetricks")
576 .arg("-q") // Unattended mode - no GUI dialogs
577 .arg("--force") // Force installation even if already installed
578 .arg(component)
579 .envs(&env)
580 .stdout(std::process::Stdio::inherit())
581 .stderr(std::process::Stdio::inherit())
582 .status()
583 .await
584 .map_err(|e| WandaError::WinetricksFailed {
585 reason: format!("Failed to run winetricks: {}", e),
586 })?;
587
588 if !status.success() {
589 error!("winetricks {} failed!", component);
590 return Err(WandaError::WinetricksFailed {
591 reason: format!(
592 "winetricks {} failed with exit code {:?}",
593 component,
594 status.code()
595 ),
596 });
597 }
598 }
599
600 // Wait for wineserver to finish
601 self.wait_wineserver().await;
602
603 Ok(())
604 }
605
606 /// Install components via winetricks (quiet mode)
607 pub async fn install_winetricks(&self, components: &[&str]) -> Result<()> {
608 self.install_winetricks_verbose(components).await
609 }
610
611 /// Run a command in the prefix using Wine
612 pub async fn run_wine_command(&self, args: &[&str]) -> Result<std::process::Output> {
613 let wine_path = self.get_wine_path();
614 let env = self.build_env();
615
616 info!("Running: {} {:?}", wine_path.display(), args);
617
618 let output = Command::new(&wine_path)
619 .args(args)
620 .envs(&env)
621 .output()
622 .await
623 .map_err(|e| WandaError::PrefixCreationFailed {
624 path: self.prefix_path.clone(),
625 reason: format!("Wine command failed: {}", e),
626 })?;
627
628 Ok(output)
629 }
630 }
631
632 #[cfg(test)]
633 mod tests {
634 use super::*;
635
636 #[test]
637 fn test_build_env() {
638 let proton = ProtonVersion {
639 name: "test".to_string(),
640 path: PathBuf::from("/test"),
641 version: (9, 0, 0),
642 is_ge: true,
643 is_experimental: false,
644 compatibility: crate::steam::ProtonCompatibility::Recommended,
645 };
646
647 let builder = PrefixBuilder::new(Path::new("/tmp/test"), &proton);
648 let env = builder.build_env();
649
650 assert_eq!(env.get("WINEPREFIX"), Some(&"/tmp/test/pfx".to_string()));
651 assert_eq!(env.get("WINEARCH"), Some(&"win64".to_string()));
652 }
653 }
654