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
16// List Wine prefixes located in the given base directory.
17pub 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
42// Execute winetricks with the given verbs inside a prefix.
43pub 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
64// Tail a Wine log file inside the prefix (best-effort path discovery).
65pub 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
89// Backup a Wine prefix to the destination directory (will create destination).
90pub 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
97// Restore a Wine prefix from a backup directory (overwrites target if exists).
98pub 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}