epik/launcher/
wine_tools.rs
1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use walkdir::WalkDir;
6
7use crate::{Error, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct WinePrefixInfo {
11 pub name: String,
12 pub path: PathBuf,
13 pub size_bytes: u64,
14}
15
16pub fn list_prefixes(base_dir: &Path) -> Result<Vec<WinePrefixInfo>> {
18 if !base_dir.exists() {
19 return Ok(Vec::new());
20 }
21
22 let mut prefixes = Vec::new();
23 for entry in fs::read_dir(base_dir).map_err(|e| Error::Other(e.to_string()))? {
24 let entry = entry.map_err(|e| Error::Other(e.to_string()))?;
25 let path = entry.path();
26 if path.is_dir() && path.join("drive_c").exists() {
27 let name = entry
28 .file_name()
29 .to_string_lossy()
30 .to_string();
31 let size_bytes = dir_size(&path)?;
32 prefixes.push(WinePrefixInfo {
33 name,
34 path: path.clone(),
35 size_bytes,
36 });
37 }
38 }
39 Ok(prefixes)
40}
41
42pub fn run_winetricks(prefix: &Path, verbs: &[&str]) -> Result<()> {
44 if verbs.is_empty() {
45 return Ok(());
46 }
47
48 let status = Command::new("winetricks")
49 .env("WINEPREFIX", prefix)
50 .args(verbs)
51 .status()
52 .map_err(|e| Error::Other(format!("Failed to start winetricks: {}", e)))?;
53
54 if status.success() {
55 Ok(())
56 } else {
57 Err(Error::Other(format!(
58 "winetricks exited with status {:?}",
59 status.code()
60 )))
61 }
62}
63
64pub fn tail_wine_log(prefix: &Path, lines: usize) -> Result<String> {
66 let candidates = vec![
67 prefix.join("drive_c/users/Public/wine.log"),
68 prefix.join("drive_c/users/Public/Temp/wine.log"),
69 prefix.join("drive_c/users/root/wine.log"),
70 prefix.join("drive_c/users/root/Temp/wine.log"),
71 ];
72
73 let log_path = candidates.into_iter().find(|p| p.exists()).ok_or_else(|| {
74 Error::Other(format!(
75 "No wine log found inside prefix {}",
76 prefix.display()
77 ))
78 })?;
79
80 let content = fs::read_to_string(&log_path)
81 .map_err(|e| Error::Other(format!("Failed to read log: {}", e)))?;
82 let mut lines_iter: Vec<&str> = content.lines().collect();
83 if lines_iter.len() > lines {
84 lines_iter = lines_iter[lines_iter.len() - lines..].to_vec();
85 }
86 Ok(lines_iter.join("\n"))
87}
88
89pub fn backup_prefix(prefix: &Path, dest: &Path) -> Result<()> {
91 if !prefix.exists() {
92 return Err(Error::Other(format!("Prefix not found: {}", prefix.display())));
93 }
94 copy_dir(prefix, dest)
95}
96
97pub fn restore_prefix(backup: &Path, target: &Path) -> Result<()> {
99 if target.exists() {
100 fs::remove_dir_all(target)
101 .map_err(|e| Error::Other(format!("Failed to clear target: {}", e)))?;
102 }
103 copy_dir(backup, target)
104}
105
106fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
107 for entry in WalkDir::new(src) {
108 let entry = entry.map_err(|e| Error::Other(e.to_string()))?;
109 let path = entry.path();
110 let relative = path
111 .strip_prefix(src)
112 .map_err(|e| Error::Other(e.to_string()))?;
113 let target = dst.join(relative);
114
115 if entry.file_type().is_dir() {
116 fs::create_dir_all(&target)
117 .map_err(|e| Error::Other(format!("Failed to create dir {}: {}", target.display(), e)))?;
118 } else if entry.file_type().is_file() {
119 if let Some(parent) = target.parent() {
120 fs::create_dir_all(parent)
121 .map_err(|e| Error::Other(format!("Failed to create dir {}: {}", parent.display(), e)))?;
122 }
123 fs::copy(path, &target)
124 .map_err(|e| Error::Other(format!("Failed to copy {}: {}", path.display(), e)))?;
125 }
126 }
127 Ok(())
128}
129
130fn dir_size(path: &Path) -> Result<u64> {
131 let mut size = 0u64;
132 for entry in WalkDir::new(path) {
133 let entry = entry.map_err(|e| Error::Other(e.to_string()))?;
134 if entry.file_type().is_file() {
135 size += entry.metadata().map_err(|e| Error::Other(e.to_string()))?.len();
136 }
137 }
138 Ok(size)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::fs::File;
145 use std::io::Write;
146 use tempfile::tempdir;
147
148 #[test]
149 fn test_backup_and_restore_prefix() {
150 let src = tempdir().unwrap();
151 let dest = tempdir().unwrap();
152 let restored = tempdir().unwrap();
153
154 let nested = src.path().join("drive_c/users/Public");
155 fs::create_dir_all(&nested).unwrap();
156 let file_path = nested.join("wine.log");
157 let mut file = File::create(&file_path).unwrap();
158 writeln!(file, "hello").unwrap();
159
160 backup_prefix(src.path(), dest.path()).unwrap();
161 restore_prefix(dest.path(), restored.path()).unwrap();
162
163 let restored_file = restored.path().join("drive_c/users/Public/wine.log");
164 assert!(restored_file.exists());
165 }
166
167 #[test]
168 fn test_tail_wine_log() {
169 let dir = tempdir().unwrap();
170 let log_dir = dir.path().join("drive_c/users/Public");
171 fs::create_dir_all(&log_dir).unwrap();
172 let log_path = log_dir.join("wine.log");
173 fs::write(&log_path, "line1\nline2\nline3").unwrap();
174
175 let output = tail_wine_log(dir.path(), 2).unwrap();
176 assert!(output.contains("line2"));
177 assert!(output.contains("line3"));
178 }
179}