diff options
Diffstat (limited to 'libs/nak/src/deps')
| -rw-r--r-- | libs/nak/src/deps/mod.rs | 177 | ||||
| -rw-r--r-- | libs/nak/src/deps/tools.rs | 174 |
2 files changed, 351 insertions, 0 deletions
diff --git a/libs/nak/src/deps/mod.rs b/libs/nak/src/deps/mod.rs new file mode 100644 index 0000000..a055eb7 --- /dev/null +++ b/libs/nak/src/deps/mod.rs @@ -0,0 +1,177 @@ +//! Dependency management via winetricks +//! +//! Uses winetricks for all Windows dependency installation. + +pub mod tools; + +use std::error::Error; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use crate::config::AppConfig; +use crate::logging::{log_error, log_install}; +use crate::runtime_wrap; +use crate::steam::SteamProton; + +// Re-export tools +pub use tools::{check_command_available, ensure_cabextract, ensure_winetricks, get_winetricks_path}; + +/// Standard winetricks verbs for MO2 prefix +pub const STANDARD_VERBS: &[&str] = &[ + "vcrun2022", // Visual C++ 2015-2022 Runtime + "dotnet6", // .NET 6.0 + "dotnet7", // .NET 7.0 + "dotnet8", // .NET 8.0 + "dotnetdesktop6", // .NET Desktop Runtime 6.0 + "d3dcompiler_47", // DirectX Compiler 47 + "d3dcompiler_43", // DirectX Compiler 43 + "d3dx9", // DirectX 9 (all versions) + "d3dx11_43", // DirectX 11 + "xact", // XACT Audio (32-bit) + "xact_x64", // XACT Audio (64-bit) +]; + + +/// Run winetricks to install dependencies +pub fn run_winetricks( + prefix_path: &Path, + proton: &SteamProton, + verbs: &[&str], + log_callback: impl Fn(String), +) -> Result<(), Box<dyn Error>> { + if verbs.is_empty() { + return Ok(()); + } + + let winetricks_path = ensure_winetricks()?; + ensure_cabextract()?; + + let Some(wine_bin) = proton.wine_binary() else { + return Err("Wine binary not found in Proton".into()); + }; + + let Some(wineserver_bin) = proton.wineserver_binary() else { + return Err("Wineserver binary not found in Proton".into()); + }; + + let cache_dir = AppConfig::get_default_cache_dir(); + std::fs::create_dir_all(&cache_dir)?; + + let verbs_str = verbs.join(" "); + log_callback(format!("Installing dependencies via winetricks: {}", verbs_str)); + log_install(&format!("Running winetricks with verbs: {}", verbs_str)); + + let nak_bin = tools::get_nak_bin_path(); + let current_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", nak_bin.display(), current_path); + + let envs: Vec<(&str, String)> = vec![ + ("PATH", new_path), + ("WINE", wine_bin.display().to_string()), + ("WINESERVER", wineserver_bin.display().to_string()), + ("WINEPREFIX", prefix_path.display().to_string()), + ("WINETRICKS_CACHE", cache_dir.display().to_string()), + ]; + let status = runtime_wrap::build_command(&winetricks_path, &envs) + .arg("-q") + .args(verbs) + .status()?; + + if !status.success() { + let err_msg = format!("Winetricks failed with exit code: {:?}", status.code()); + log_error(&err_msg); + return Err(err_msg.into()); + } + + log_install("Winetricks completed successfully"); + Ok(()) +} + +/// Install all standard dependencies to a prefix +pub fn install_standard_deps( + prefix_path: &Path, + proton: &SteamProton, + log_callback: impl Fn(String), +) -> Result<(), Box<dyn Error>> { + run_winetricks(prefix_path, proton, STANDARD_VERBS, log_callback) +} + +/// Run winetricks with cancellation support. +pub fn run_winetricks_cancellable( + prefix_path: &Path, + proton: &SteamProton, + verbs: &[&str], + log_callback: impl Fn(String), + cancel_flag: &Arc<AtomicBool>, +) -> Result<(), Box<dyn Error>> { + if verbs.is_empty() { + return Ok(()); + } + + let winetricks_path = ensure_winetricks()?; + ensure_cabextract()?; + + let Some(wine_bin) = proton.wine_binary() else { + return Err("Wine binary not found in Proton".into()); + }; + + let Some(wineserver_bin) = proton.wineserver_binary() else { + return Err("Wineserver binary not found in Proton".into()); + }; + + let cache_dir = AppConfig::get_default_cache_dir(); + std::fs::create_dir_all(&cache_dir)?; + + let verbs_str = verbs.join(" "); + log_callback(format!("Installing dependencies via winetricks: {}", verbs_str)); + log_install(&format!("Running winetricks with verbs: {}", verbs_str)); + + let nak_bin = tools::get_nak_bin_path(); + let current_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", nak_bin.display(), current_path); + + let envs: Vec<(&str, String)> = vec![ + ("PATH", new_path), + ("WINE", wine_bin.display().to_string()), + ("WINESERVER", wineserver_bin.display().to_string()), + ("WINEPREFIX", prefix_path.display().to_string()), + ("WINETRICKS_CACHE", cache_dir.display().to_string()), + ]; + let mut child = runtime_wrap::build_command(&winetricks_path, &envs) + .arg("-q") + .args(verbs) + .spawn()?; + + loop { + match child.try_wait()? { + Some(status) => { + if !status.success() { + let err_msg = format!("Winetricks failed with exit code: {:?}", status.code()); + log_error(&err_msg); + return Err(err_msg.into()); + } + log_install("Winetricks completed successfully"); + return Ok(()); + } + None => { + if cancel_flag.load(Ordering::Relaxed) { + let _ = child.kill(); + let _ = child.wait(); + return Err("Cancelled".into()); + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + } + } +} + +/// Install standard deps with cancellation support +pub fn install_standard_deps_cancellable( + prefix_path: &Path, + proton: &SteamProton, + log_callback: impl Fn(String), + cancel_flag: &Arc<AtomicBool>, +) -> Result<(), Box<dyn Error>> { + run_winetricks_cancellable(prefix_path, proton, STANDARD_VERBS, log_callback, cancel_flag) +} diff --git a/libs/nak/src/deps/tools.rs b/libs/nak/src/deps/tools.rs new file mode 100644 index 0000000..c97247c --- /dev/null +++ b/libs/nak/src/deps/tools.rs @@ -0,0 +1,174 @@ +//! Linux tool management (winetricks, cabextract) +//! +//! Handles downloading and managing Linux CLI tools. +//! Tools are stored in ~/.var/app/com.fluorine.manager/bin/ for Fluorine Manager. + +use std::error::Error; +use std::fs; +use std::io::Read; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use std::process::Command; + +use crate::logging::{log_error, log_info, log_warning}; + +// ============================================================================ +// NaK Bin Directory (~/.var/app/com.fluorine.manager/bin/) +// ============================================================================ + +/// Get the tool bin directory path (~/.var/app/com.fluorine.manager/bin/) +/// This is accessible from both native and Flatpak environments. +pub fn get_nak_bin_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join(".var/app/com.fluorine.manager/bin") +} + +/// Check if a command exists (either in system PATH or tool bin) +pub fn check_command_available(cmd: &str) -> bool { + // Check system PATH first + if Command::new("which") + .arg(cmd) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + return true; + } + + // Check tool bin directory + let nak_bin = get_nak_bin_path().join(cmd); + nak_bin.exists() +} + +// ============================================================================ +// Winetricks +// ============================================================================ + +const WINETRICKS_URL: &str = + "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"; + +/// Get the path to winetricks (without downloading) +pub fn get_winetricks_path() -> PathBuf { + get_nak_bin_path().join("winetricks") +} + +/// Ensures winetricks is downloaded and up-to-date. +pub fn ensure_winetricks() -> Result<PathBuf, Box<dyn Error>> { + let bin_dir = get_nak_bin_path(); + let winetricks_path = bin_dir.join("winetricks"); + + fs::create_dir_all(&bin_dir)?; + + log_info("Checking for winetricks updates..."); + + match ureq::get(WINETRICKS_URL).call() { + Ok(response) => { + let mut new_content = Vec::new(); + response.into_reader().read_to_end(&mut new_content)?; + + let should_update = if winetricks_path.exists() { + let existing = fs::read(&winetricks_path).unwrap_or_default(); + existing != new_content + } else { + true + }; + + if should_update { + fs::write(&winetricks_path, &new_content)?; + + let mut perms = fs::metadata(&winetricks_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&winetricks_path, perms)?; + + if winetricks_path.exists() { + log_info("Winetricks updated to latest version"); + } else { + log_info(&format!("Winetricks downloaded to {:?}", winetricks_path)); + } + } + } + Err(e) => { + if winetricks_path.exists() { + log_warning(&format!("Failed to check winetricks updates: {}", e)); + } else { + return Err(format!("Failed to download winetricks: {}", e).into()); + } + } + } + + Ok(winetricks_path) +} + +// ============================================================================ +// Cabextract (required by winetricks for DirectX cabs) +// ============================================================================ + +const CABEXTRACT_URL: &str = + "https://github.com/SulfurNitride/NaK/releases/download/Cabextract/cabextract-linux-x86_64.zip"; + +/// Ensures cabextract is available (either system or downloaded). +pub fn ensure_cabextract() -> Result<PathBuf, Box<dyn Error>> { + // First check if system has cabextract + if Command::new("which") + .arg("cabextract") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + return Ok(PathBuf::from("cabextract")); + } + + // Check if we already downloaded it + let bin_dir = get_nak_bin_path(); + let cabextract_path = bin_dir.join("cabextract"); + + if cabextract_path.exists() { + return Ok(cabextract_path); + } + + // Download cabextract zip + log_warning("System cabextract not found, downloading..."); + fs::create_dir_all(&bin_dir)?; + + let response = ureq::get(CABEXTRACT_URL).call().map_err(|e| { + format!( + "Failed to download cabextract: {}. Please install cabextract manually.", + e + ) + })?; + + let zip_path = bin_dir.join("cabextract.zip"); + let mut zip_file = fs::File::create(&zip_path)?; + std::io::copy(&mut response.into_reader(), &mut zip_file)?; + + let status = Command::new("unzip") + .arg("-o") + .arg(&zip_path) + .arg("-d") + .arg(&bin_dir) + .status()?; + + if !status.success() { + let _ = Command::new("python3") + .arg("-c") + .arg(format!( + "import zipfile; zipfile.ZipFile('{}').extractall('{}')", + zip_path.display(), + bin_dir.display() + )) + .status(); + } + + let _ = fs::remove_file(&zip_path); + + if cabextract_path.exists() { + let mut perms = fs::metadata(&cabextract_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&cabextract_path, perms)?; + log_info(&format!("cabextract downloaded to {:?}", cabextract_path)); + Ok(cabextract_path) + } else { + log_error("Failed to extract cabextract from zip"); + Err("Failed to extract cabextract from zip".into()) + } +} |
