diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:12 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:25 -0600 |
| commit | 817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch) | |
| tree | 52aed11d6751bb7d20b06acf197d56fac113203d /libs/nak/src/deps | |
| parent | 51a9f8f197727f00896e5de44569b098923527dd (diff) | |
Add FUSE external mapping support, BG3/Oblivion Remastered fixes, fomod-plus and NaK integration
FUSE VFS now deploys non-data-dir mod mappings (Paks, OBSE, UE4SS, etc.)
via real symlinks and injects file-level data-dir mappings (plugins.txt,
loadorder.txt) into the VFS tree. Fixes game launches for Oblivion
Remastered (Root Builder path resolution, script extender support) and
BG3 (Wine prefix documents directory, file mapper symlinks on Linux).
Vendors mo2-fomod-plus plugin and NaK crate for FOMOD installer and
game finder/runtime support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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()) + } +} |
