//! macOS support for osu! stable running inside a Wine wrapper. //! //! osu! stable is a Windows application, so on macOS it has to run through a //! Wine-based wrapper. The launcher supports Wineskin wrappers (the common //! `osu!.app` wine skin), Whisky bottles and CrossOver bottles. use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::time::{Duration as StdDuration, Instant}; use tokio::process::{Child, Command}; use tokio::time::{Duration, sleep}; use crate::utils::is_valid_osu_folder; /// Round the borderless launcher window and make its corners transparent. /// /// macOS does not round undecorated windows on its own, so the webview has to /// be clipped with the content view's layer. The window itself is made /// transparent so the area outside that clip shows the desktop instead of a /// square background. pub fn round_window(window: &tauri::WebviewWindow, radius: f64) -> Result<(), String> { use objc2::msg_send; use objc2::runtime::AnyObject; let ns_window = window.ns_window().map_err(|error| error.to_string())? as *mut AnyObject; if ns_window.is_null() { return Err("The native window is not available".into()); } unsafe { let _: () = msg_send![ns_window, setOpaque: false]; let clear: *mut AnyObject = msg_send![objc2::class!(NSColor), clearColor]; let _: () = msg_send![ns_window, setBackgroundColor: clear]; let _: () = msg_send![ns_window, setHasShadow: true]; let content_view: *mut AnyObject = msg_send![ns_window, contentView]; if content_view.is_null() { return Err("The native content view is not available".into()); } let _: () = msg_send![content_view, setWantsLayer: true]; let layer: *mut AnyObject = msg_send![content_view, layer]; if layer.is_null() { return Err("The native content view has no layer".into()); } let _: () = msg_send![layer, setCornerRadius: radius]; let _: () = msg_send![layer, setMasksToBounds: true]; let _: () = msg_send![ns_window, invalidateShadow]; } Ok(()) } /// Allow theme media to start without a click. WKWebView otherwise blocks /// autoplay for anything with an audio track, which makes a theme's background /// video sit frozen until the user interacts with the launcher. pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String> { use objc2::msg_send; use objc2::runtime::AnyObject; window .with_webview(|webview| unsafe { let webview = webview.inner() as *mut AnyObject; if webview.is_null() { return; } let configuration: *mut AnyObject = msg_send![webview, configuration]; if configuration.is_null() { return; } // WKAudiovisualMediaTypesNone. This is a public WKWebView setting. let _: () = msg_send![configuration, setMediaTypesRequiringUserActionForPlayback: 0usize]; let current: usize = msg_send![configuration, mediaTypesRequiringUserActionForPlayback]; log::debug!( target: "launcher", "Theme media autoplay policy: {current}" ); }) .map_err(|error| format!("Could not enable theme media autoplay: {error}")) } /// Show a native open panel that accepts `.app` bundles as well as folders. /// /// AppKit treats application bundles as packages (directories) by default, so a /// regular folder picker descends into `osu!.app` instead of selecting it. This /// panel turns that behaviour off, letting the user pick the wrapper itself, /// while still allowing a plain Wine prefix folder to be selected. pub fn pick_osu_installation() -> Result, String> { use objc2::msg_send; use objc2::rc::autoreleasepool; use objc2::runtime::AnyObject; use std::ffi::{CStr, CString}; unsafe fn ns_string(value: &str) -> Result<*mut AnyObject, String> { let value = CString::new(value).map_err(|_| "The path contains a NUL byte".to_string())?; let string: *mut AnyObject = unsafe { msg_send![objc2::class!(NSString), stringWithUTF8String: value.as_ptr()] }; if string.is_null() { return Err("Could not create the native dialog text".to_string()); } Ok(string) } autoreleasepool(|_| unsafe { let panel: *mut AnyObject = msg_send![objc2::class!(NSOpenPanel), openPanel]; if panel.is_null() { return Err("Could not create the native file picker".to_string()); } let _: () = msg_send![panel, setCanChooseFiles: true]; let _: () = msg_send![panel, setCanChooseDirectories: true]; // Keep `.app` bundles selectable instead of navigating into them. let _: () = msg_send![panel, setTreatsFilePackagesAsDirectories: false]; let _: () = msg_send![panel, setAllowsMultipleSelection: false]; let _: () = msg_send![panel, setCanCreateDirectories: false]; let title = ns_string("Select osu!.app")?; let _: () = msg_send![panel, setTitle: title]; let message = ns_string("Pick your osu!.app wrapper, or a Wine prefix folder.")?; let _: () = msg_send![panel, setMessage: message]; let prompt = ns_string("Select")?; let _: () = msg_send![panel, setPrompt: prompt]; let applications = ns_string("/Applications")?; let directory_url: *mut AnyObject = msg_send![objc2::class!(NSURL), fileURLWithPath: applications]; if !directory_url.is_null() { let _: () = msg_send![panel, setDirectoryURL: directory_url]; } // NSModalResponseOK let response: i64 = msg_send![panel, runModal]; if response != 1 { return Ok(None); } let url: *mut AnyObject = msg_send![panel, URL]; if url.is_null() { return Ok(None); } let path: *mut AnyObject = msg_send![url, path]; if path.is_null() { return Ok(None); } let utf8: *const std::os::raw::c_char = msg_send![path, UTF8String]; if utf8.is_null() { return Ok(None); } let path = CStr::from_ptr(utf8).to_string_lossy().into_owned(); Ok(Some(PathBuf::from(path))) }) } pub struct WineRuntime { prefix: PathBuf, wine_bin: PathBuf, wine_root: Option, app_bundle: Option, } #[derive(Clone)] pub struct WineDiagnostics { pub prefix: PathBuf, pub wine_bin: PathBuf, pub app_bundle: Option, pub net8_installed: bool, pub wine_version: Option, pub prefix_arch: Option, } impl WineDiagnostics { /// Inspect the wrapper without starting it, for the launcher's diagnostics panel. pub fn for_osu_folder(osu_folder: &Path) -> Result { let runtime = WineRuntime::for_osu_folder(osu_folder)?; let net8_installed = [ runtime .prefix .join("drive_c/Program Files/dotnet/shared/Microsoft.WindowsDesktop.App"), runtime .prefix .join("drive_c/Program Files (x86)/dotnet/shared/Microsoft.WindowsDesktop.App"), ] .iter() .any(|path| { fs::read_dir(path) .ok() .map(|entries| { entries.filter_map(|entry| entry.ok()).any(|entry| { entry .file_name() .to_str() .map(|name| name.starts_with("8.")) .unwrap_or(false) }) }) .unwrap_or(false) }); Ok(Self { prefix: runtime.prefix.clone(), wine_bin: runtime.wine_bin.clone(), app_bundle: app_bundle_for(&runtime.prefix), net8_installed, wine_version: wine_version_for(&runtime.wine_bin), prefix_arch: prefix_architecture(&runtime.prefix), }) } } impl WineRuntime { /// Build a runtime description for an osu! folder inside a Wine prefix. pub fn for_osu_folder(osu_folder: &Path) -> Result { if !osu_folder.join("osu!.exe").is_file() { return Err(format!( "osu!.exe was not found in {}", osu_folder.display() )); } let prefix = prefix_for(osu_folder).ok_or_else(|| { "This osu! installation is not inside a Wine prefix. On macOS, choose the osu! folder inside your Wineskin, Whisky or CrossOver wrapper.".to_string() })?; let app = app_bundle_for(&prefix); let (wine_bin, wine_root) = find_wine_bin(&prefix, app.as_deref()).ok_or_else(|| { "Could not find a Wine runtime for this osu! installation. Make sure the Wineskin/Whisky/CrossOver wrapper is still installed.".to_string() })?; Ok(Self { prefix, wine_bin, wine_root, app_bundle: app, }) } /// Spawn a Windows executable through the wrapper's Wine runtime. pub fn spawn(&self, exe: &Path, args: &[&str]) -> Result { let loader = self.loader_for(exe); if !loader.is_file() { return Err(format!( "Wine loader not found at {}. The wrapper may be incomplete.", loader.display() )); } let mut command = Command::new(&loader); command.arg(exe).args(args); command.current_dir(exe.parent().unwrap_or(&self.prefix)); command .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); command.env("WINEPREFIX", &self.prefix); command.env("WINEDEBUG", "-all"); command.env("WINEDLLOVERRIDES", "mscoree,mshtml="); command.env("FREETYPE_PROPERTIES", "truetype:interpreter-version=35"); let mut library_paths = Vec::new(); if let Some(app) = &self.app_bundle { for dir in [ app.join("Contents/Frameworks"), app.join("Contents/Frameworks/wswine.bundle/lib"), app.join("Contents/Frameworks/wswine.bundle/lib64"), app.join("Contents/Frameworks/wswine.bundle/lib32on64"), app.join("Contents/SharedSupport/wine/lib"), app.join("Contents/SharedSupport/wine/lib64"), app.join("Contents/SharedSupport/wine/lib32on64"), app.join("Frameworks"), ] { if dir.is_dir() { library_paths.push(dir); } } } for dir in [ self.prefix.join("Frameworks"), self.prefix.join("lib"), self.prefix.join("lib64"), self.prefix.join("lib32on64"), ] { if dir.is_dir() { library_paths.push(dir); } } if let Some(wine_root) = &self.wine_root { for dir in [ wine_root.join("lib"), wine_root.join("lib64"), wine_root.join("lib32on64"), wine_root.clone(), ] { if dir.is_dir() { library_paths.push(dir); } } } for system_dir in [ PathBuf::from("/opt/local/lib"), PathBuf::from("/usr/lib"), PathBuf::from("/usr/libexec"), PathBuf::from("/usr/lib/system"), PathBuf::from("/opt/X11/lib"), ] { if system_dir.is_dir() { library_paths.push(system_dir); } } if let Some(existing) = std::env::var_os("DYLD_FALLBACK_LIBRARY_PATH") { library_paths.extend(std::env::split_paths(&existing)); } if let Ok(library_path) = std::env::join_paths(library_paths) { command.env("DYLD_FALLBACK_LIBRARY_PATH", &library_path); command.env("WINESKIN_LIB_PATH_FOR_FALLBACK", &library_path); } let mut paths = vec![self.wine_bin.clone()]; if let Some(existing) = std::env::var_os("PATH") { paths.extend(std::env::split_paths(&existing)); } if let Ok(path) = std::env::join_paths(paths) { command.env("PATH", path); } let executable_name = exe .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_else(|| "the Windows application".to_string()); command.spawn().map_err(|error| { format!( "Failed to start {executable_name} through Wine ({}): {}", loader.display(), error ) }) } /// Stop every process that belongs to this Wine prefix. pub async fn kill_prefix(&self) { let wineserver = self.wine_bin.join("wineserver"); if !wineserver.is_file() { return; } let mut cmd = Command::new(wineserver); cmd.arg("-k") .env("WINEPREFIX", &self.prefix) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); if let Some(app) = &self.app_bundle { if let Ok(fallback) = std::env::join_paths([ app.join("Contents/Frameworks"), app.join("Frameworks"), ]) { cmd.env("DYLD_FALLBACK_LIBRARY_PATH", fallback); } } let _ = cmd.status().await; } fn loader_for(&self, exe: &Path) -> PathBuf { let is_64_bit = pe_machine(exe) == Some(0x8664); let candidates: &[&str] = if is_64_bit { &["wine64", "wine"] } else { &["wine32on64", "wine64", "wine"] }; candidates .iter() .map(|candidate| self.wine_bin.join(candidate)) .find(|candidate| candidate.is_file()) .unwrap_or_else(|| self.wine_bin.join(candidates[0])) } } /// Run osu! (`-devserver ez-pp.farm`) and wait until the game exits. pub async fn run_osu(osu_folder: &str, patch: bool) -> Result<(), String> { let folder = Path::new(osu_folder); let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| { log::error!(target: "osu", "Could not prepare the Wine runtime: {error}"); error })?; let mut game = runtime .spawn(&folder.join("osu!.exe"), &["-devserver", "ez-pp.farm"]) .map_err(|error| { log::error!(target: "osu", "Could not start osu!: {error}"); error })?; if let Some(pid) = game.id() { log::info!(target: "osu", "osu! started through Wine (pid {pid})"); } crate::logging::capture_osu_output(&mut game); if patch { if let Err(error) = start_patcher(&runtime, folder).await { log::warn!(target: "patcher", "Could not start the patcher: {error}"); } } match game.wait().await { Ok(status) => log::info!(target: "osu", "osu! exited with {status}"), Err(error) => { log::warn!(target: "osu", "Could not wait for osu!: {error}"); return Err(format!("Failed to wait for osu!: {}", error)); } } Ok(()) } /// Start the Windows patcher through the same Wine prefix as osu!. async fn start_patcher(runtime: &WineRuntime, osu_folder: &Path) -> Result<(), String> { let patcher_exe = osu_folder .join("EZPPLauncher") .join("patcher") .join("osu!.patcher.exe"); if !patcher_exe.is_file() { return Err(format!( "Patcher was requested but {} does not exist", patcher_exe.display() )); } wait_for_osu_game().await; log::info!( target: "patcher", "Starting the patcher from {}", patcher_exe.display() ); let mut patcher = runtime.spawn(&patcher_exe, &[])?; if let Some(pid) = patcher.id() { log::info!(target: "patcher", "Patcher started through Wine (pid {pid})"); } crate::logging::capture_patcher_output(&mut patcher); tauri::async_runtime::spawn(async move { match patcher.wait().await { Ok(status) => log::info!(target: "patcher", "Patcher exited with {status}"), Err(error) => log::warn!(target: "patcher", "Could not wait for the patcher: {error}"), } }); Ok(()) } /// Wait until the Wine-hosted osu! process exists, then give it time to create /// its game window. Wine does not expose the window title to the launcher here, /// so the process is the best signal available. async fn wait_for_osu_game() { let mut system = sysinfo::System::new_all(); let deadline = Instant::now() + StdDuration::from_secs(30); let mut detected = false; while Instant::now() < deadline { system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); if system .processes() .values() .any(crate::utils::is_osu_game_process) { detected = true; break; } sleep(Duration::from_millis(500)).await; } if !detected { log::warn!( target: "patcher", "Could not detect the osu! process; starting the patcher anyway" ); } // Wine gives no reliable window-ready signal, so leave the game a moment // to finish initializing before the patcher attaches to it. sleep(Duration::from_secs(8)).await; } /// Run `osu!.exe -repair`, wait for the updater and close the game it starts. pub async fn run_osu_updater(osu_folder: &str) -> Result<(), String> { let folder = Path::new(osu_folder); let runtime = WineRuntime::for_osu_folder(folder).map_err(|error| { log::error!(target: "osu", "Could not prepare the Wine runtime: {error}"); error })?; let mut updater = runtime .spawn(&folder.join("osu!.exe"), &["-repair"]) .map_err(|error| { log::error!(target: "osu", "Could not start the osu! updater: {error}"); error })?; if let Some(pid) = updater.id() { log::info!(target: "osu", "osu! updater started through Wine (pid {pid})"); } crate::logging::capture_osu_output(&mut updater); let mut system = sysinfo::System::new_all(); let deadline = Instant::now() + StdDuration::from_secs(300); loop { if updater .try_wait() .map_err(|error| format!("Failed to check the osu! updater: {}", error))? .is_some() { log::info!(target: "osu", "The osu! updater exited"); break; } system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); if system .processes() .values() .any(crate::utils::is_osu_game_process) { // The updater relaunched osu! to finish the update. Close it so the // launcher can start the game itself with the correct server. log::info!(target: "osu", "The updater relaunched osu!; closing the Wine prefix"); runtime.kill_prefix().await; break; } if Instant::now() >= deadline { log::warn!(target: "osu", "Timed out waiting for the osu! updater; closing the Wine prefix"); runtime.kill_prefix().await; break; } sleep(Duration::from_millis(500)).await; } match updater.wait().await { Ok(status) => log::info!(target: "osu", "osu! updater exited with {status}"), Err(error) => log::warn!(target: "osu", "Could not wait for the osu! updater: {error}"), } Ok(()) } /// Resolve a user-selected path (osu! folder, `.app` bundle, prefix or drive_c) /// to a valid osu! installation folder. pub fn resolve_osu_folder(folder: &Path) -> Option { if is_valid_osu_folder(folder) { return Some(folder.to_path_buf()); } let mut prefixes = Vec::new(); if folder.file_name() == Some(OsStr::new("drive_c")) { if let Some(parent) = canonical_prefix(folder) { prefixes.push(parent); } } if folder.join("drive_c").is_dir() { if let Some(parent) = canonical_prefix(&folder.join("drive_c")) { prefixes.push(parent); } } if folder.extension() == Some(OsStr::new("app")) || folder.join("Contents").is_dir() { for drive_c in [ folder.join("Contents/Resources/drive_c"), folder.join("Contents/SharedSupport/drive_c"), folder.join("Contents/SharedSupport/prefix/drive_c"), ] { if drive_c.is_dir() { if let Some(parent) = canonical_prefix(&drive_c) { prefixes.push(parent); } } } let mut drive_dirs = Vec::new(); find_drive_c_dirs(folder, 5, &mut drive_dirs); prefixes.extend( drive_dirs .iter() .filter_map(|drive_c| drive_c.parent().map(Path::to_path_buf)), ); } if let Some(prefix) = prefix_for(folder) { prefixes.push(prefix); } prefixes.sort(); prefixes.dedup(); prefixes .into_iter() .find_map(|prefix| find_osu_folder_in_prefix(&prefix)) } /// Auto-detect an osu! installation in the usual macOS wrapper locations. pub fn find_osu_installation() -> Option { for app in app_bundles() { if let Some(folder) = resolve_osu_folder(&app) { return Some(folder.to_string_lossy().into_owned()); } } for prefix in bottle_prefixes() { if let Some(folder) = find_osu_folder_in_prefix(&prefix) { return Some(folder.to_string_lossy().into_owned()); } } None } fn find_osu_folder_in_prefix(prefix: &Path) -> Option { let drive_c = prefix.join("drive_c"); if !drive_c.is_dir() { return None; } if let Some(folder) = common_osu_folders(&drive_c) .into_iter() .find(|folder| is_valid_osu_folder(folder)) { return Some(folder); } let mut candidates = Vec::new(); collect_osu_folders(&drive_c, 0, 6, &mut candidates); candidates .into_iter() .find(|folder| is_valid_osu_folder(folder)) } fn common_osu_folders(drive_c: &Path) -> Vec { let mut folders = vec![ drive_c.join("osu!"), drive_c.join("Program Files").join("osu!"), drive_c.join("Program Files (x86)").join("osu!"), ]; if let Ok(users) = fs::read_dir(drive_c.join("users")) { for user in users.flatten() { let user_path = user.path(); if user_path.is_dir() { folders.push(user_path.join("AppData").join("Local").join("osu!")); folders.push(user_path.join("AppData").join("Roaming").join("osu!")); } } } folders } fn collect_osu_folders(dir: &Path, depth: usize, max_depth: usize, out: &mut Vec) { if depth > max_depth { return; } if dir.join("osu!.exe").is_file() { out.push(dir.to_path_buf()); return; } let Ok(entries) = fs::read_dir(dir) else { return; }; for entry in entries.flatten() { let path = entry.path(); let Ok(metadata) = fs::symlink_metadata(&path) else { continue; }; if metadata.file_type().is_symlink() || !metadata.is_dir() { continue; } let name = path .file_name() .unwrap_or_default() .to_string_lossy() .to_ascii_lowercase(); if matches!( name.as_str(), "windows" | "programdata" | "$recycle.bin" | "system volume information" ) { continue; } collect_osu_folders(&path, depth + 1, max_depth, out); } } fn find_drive_c_dirs(dir: &Path, depth: usize, out: &mut Vec) { if depth == 0 { return; } let Ok(entries) = fs::read_dir(dir) else { return; }; for entry in entries.flatten() { let path = entry.path(); let Ok(metadata) = fs::symlink_metadata(&path) else { continue; }; if metadata.file_type().is_symlink() { // Wineskin wrappers often add a convenience drive_c symlink at // the app root. Resolve it to the real prefix so WINEPREFIX points // at Contents/Resources instead of the bundle itself. if path.file_name() == Some(OsStr::new("drive_c")) { if let Ok(canonical) = fs::canonicalize(&path) { out.push(canonical); } } continue; } if !metadata.is_dir() { continue; } if path.file_name() == Some(OsStr::new("drive_c")) { out.push(path); continue; } // Do not descend into nested apps (for example Wineskin.app). if path.extension() == Some(OsStr::new("app")) { continue; } find_drive_c_dirs(&path, depth - 1, out); } } fn prefix_for(osu_folder: &Path) -> Option { let drive_c = osu_folder .ancestors() .find(|ancestor| ancestor.file_name() == Some(OsStr::new("drive_c")))?; canonical_prefix(drive_c) } fn canonical_prefix(drive_c: &Path) -> Option { let canonical = fs::canonicalize(drive_c).ok()?; canonical.parent().map(Path::to_path_buf) } fn app_bundle_for(prefix: &Path) -> Option { prefix .ancestors() .find(|ancestor| ancestor.extension() == Some(OsStr::new("app"))) .map(Path::to_path_buf) } fn home_dir() -> Option { std::env::var_os("HOME").map(PathBuf::from) } fn app_bundles() -> Vec { let mut roots = vec![PathBuf::from("/Applications")]; if let Some(home) = home_dir() { roots.push(home.join("Applications")); } let mut apps = Vec::new(); for root in roots { let Ok(entries) = fs::read_dir(root) else { continue; }; for entry in entries.flatten() { let path = entry.path(); if path.extension() == Some(OsStr::new("app")) { apps.push(path); } } } apps.sort(); apps } fn bottle_prefixes() -> Vec { let Some(home) = home_dir() else { return Vec::new(); }; let bottle_roots = [ home.join("Library/Containers/com.isaacmarovitz.Whisky/Bottles"), home.join("Library/Application Support/CrossOver/Bottles"), home.join("Library/Application Support/Whisky/Bottles"), ]; let mut prefixes = Vec::new(); for root in bottle_roots { let Ok(entries) = fs::read_dir(root) else { continue; }; for entry in entries.flatten() { let path = entry.path(); if path.join("drive_c").is_dir() { prefixes.push(path); } } } prefixes.sort(); prefixes } fn find_wine_bin(prefix: &Path, app: Option<&Path>) -> Option<(PathBuf, Option)> { let mut candidates: Vec<(PathBuf, Option)> = Vec::new(); if let Some(app) = app { for root in [ app.join("Contents/SharedSupport/wine"), app.join("Contents/Resources/WineskinEngine.bundle/Wine"), app.join("Contents/Frameworks/wswine.bundle"), app.join("Wineskin.app/Contents/Resources"), ] { push_wine_root(&mut candidates, root); } let wineskin_res = app.join("Wineskin.app/Contents/Resources"); if has_wine_loader(&wineskin_res) { candidates.push((wineskin_res.clone(), Some(wineskin_res))); } } for root in [ prefix.join("WineskinEngine.bundle/Wine"), prefix.join("Wine"), ] { push_wine_root(&mut candidates, root); } if let Some(home) = home_dir() { for root in [ home.join("Applications/Whisky.app/Contents/Resources/wine"), home.join("Applications/Whisky.app/Contents/Libraries/Wine"), ] { push_wine_root(&mut candidates, root); } } for root in [ PathBuf::from("/Applications/Whisky.app/Contents/Resources/wine"), PathBuf::from("/Applications/Whisky.app/Contents/Libraries/Wine"), PathBuf::from("/Applications/CrossOver.app/Contents/SharedSupport/CrossOver"), ] { push_wine_root(&mut candidates, root); } for bin in [ PathBuf::from("/opt/homebrew/bin"), PathBuf::from("/usr/local/bin"), ] { if has_wine_loader(&bin) { candidates.push((bin, None)); } } for binary in ["wine64", "wine32on64", "wine"] { if let Some(path) = find_in_path(binary) { if let Some(bin) = path.parent() { candidates.push((bin.to_path_buf(), None)); break; } } } candidates.into_iter().find(|(bin, _)| has_wine_loader(bin)) } fn push_wine_root(candidates: &mut Vec<(PathBuf, Option)>, root: PathBuf) { let bin = root.join("bin"); if has_wine_loader(&bin) { candidates.push((bin, Some(root))); } } fn has_wine_loader(bin: &Path) -> bool { ["wine32on64", "wine64", "wine"] .iter() .any(|loader| bin.join(loader).is_file()) } fn wine_version_for(bin: &Path) -> Option { let loader = ["wine64", "wine32on64", "wine"] .iter() .map(|name| bin.join(name)) .find(|path| path.is_file())?; let output = std::process::Command::new(loader) .arg("--version") .env("WINEDEBUG", "-all") .output() .ok()?; if !output.status.success() { return None; } let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); (!version.is_empty()).then_some(version) } fn prefix_architecture(prefix: &Path) -> Option { if prefix.join("drive_c/windows/syswow64").is_dir() { Some("win64".to_string()) } else if prefix.join("drive_c/windows/system32").is_dir() { Some("win32".to_string()) } else { None } } fn find_in_path(binary: &str) -> Option { let path = std::env::var_os("PATH")?; std::env::split_paths(&path) .map(|dir| dir.join(binary)) .find(|candidate| candidate.is_file()) } fn pe_machine(exe: &Path) -> Option { use std::io::{Read, Seek, SeekFrom}; let mut file = fs::File::open(exe).ok()?; let mut signature = [0u8; 2]; file.read_exact(&mut signature).ok()?; if &signature != b"MZ" { return None; } file.seek(SeekFrom::Start(0x3c)).ok()?; let mut offset = [0u8; 4]; file.read_exact(&mut offset).ok()?; file.seek(SeekFrom::Start(u32::from_le_bytes(offset) as u64)) .ok()?; let mut header = [0u8; 6]; file.read_exact(&mut header).ok()?; if &header[..4] != b"PE\0\0" { return None; } Some(u16::from_le_bytes([header[4], header[5]])) }