From 8557ced3b51600f26b3df1d58b8fe057813bc068 Mon Sep 17 00:00:00 2001 From: ederson-shaw Date: Sat, 26 Sep 2026 00:20:19 -0300 Subject: [PATCH 1/4] fix: play on fresh steam accounts (create shortcuts.vdf for the signed-in user), no wine options or proton mapping on windows, windows-safe folder names, human launch errors, tests run on windows ci Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 17 +-- app/src-tauri/Cargo.lock | 64 ++++++++++- app/src-tauri/Cargo.toml | 2 + app/src-tauri/src/commands.rs | 2 +- app/src-tauri/src/game_process.rs | 10 +- app/src-tauri/src/helpers.rs | 118 +++++++++----------- app/src-tauri/src/helpers_tests.rs | 45 ++++++++ app/src-tauri/src/launch.rs | 49 ++++++--- app/src-tauri/src/launch_monitor.rs | 2 +- app/src-tauri/src/lib.rs | 3 + app/src-tauri/src/library.rs | 2 +- app/src-tauri/src/pipeline_http.rs | 2 +- app/src-tauri/src/pipeline_torrent.rs | 2 +- app/src-tauri/src/quiet_command.rs | 24 ++++ app/src-tauri/src/steam_client.rs | 37 +++++-- app/src-tauri/src/steam_ipc.rs | 2 +- app/src-tauri/src/steam_root.rs | 72 +++++------- app/src-tauri/src/steam_user.rs | 153 ++++++++++++++++++++++++++ app/src-tauri/src/steam_user_tests.rs | 84 ++++++++++++++ app/src-tauri/src/user_error.rs | 32 ++++++ core/src/compat_tool.rs | 20 ++++ core/src/extract.rs | 17 +-- core/src/main.rs | 2 +- core/src/shortcut_add.rs | 8 ++ 24 files changed, 598 insertions(+), 171 deletions(-) create mode 100644 app/src-tauri/src/helpers_tests.rs create mode 100644 app/src-tauri/src/quiet_command.rs create mode 100644 app/src-tauri/src/steam_user.rs create mode 100644 app/src-tauri/src/steam_user_tests.rs create mode 100644 app/src-tauri/src/user_error.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e8805a..c6a2908 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,17 +57,10 @@ jobs: env: RUSTFLAGS: -D warnings - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - uses: swatinem/rust-cache@v2 - with: - workspaces: core - - name: Core tests working-directory: core - run: cargo test --verbose + run: cargo test + + - name: App tests + working-directory: app/src-tauri + run: cargo test --lib diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index fead197..2ebba1b 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -83,9 +83,11 @@ version = "0.2.1" dependencies = [ "bytes", "fix-core", + "keyvalues-parser", "librqbit", "serde", "serde_json", + "steamlocate", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -1254,7 +1256,7 @@ dependencies = [ "rustc_version", "toml 1.1.6+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2547,6 +2549,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyvalues-parser" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded51a3b2cdcdc173e23683fd49f72e8151160b279f80c0378f7661a2ae10b7" +dependencies = [ + "pest", + "serde_core", +] + +[[package]] +name = "keyvalues-serde" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff212d2f9b588884a1334844de9901809e00e41b720ed4a7555eacbff234383" +dependencies = [ + "keyvalues-parser", + "serde_core", +] + [[package]] name = "leaky-bucket" version = "1.1.2" @@ -3527,6 +3549,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +dependencies = [ + "memchr", + "ucd-trie", +] + [[package]] name = "phf" version = "0.10.1" @@ -4750,6 +4782,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "steamlocate" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ad4e054e19e854821d66d67a38493220f2b75361fbe6c6e04bddd228d0e771" +dependencies = [ + "crc", + "keyvalues-parser", + "keyvalues-serde", + "serde", + "serde_derive", + "winreg 0.56.0", +] + [[package]] name = "string_cache" version = "0.8.9" @@ -5697,6 +5743,12 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.2.1" @@ -6613,6 +6665,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 0b5daa1..d722219 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -28,6 +28,8 @@ fix-core = { path = "../../core" } librqbit = "8" bytes = "1" tokio = { version = "1", features = ["time"] } +steamlocate = "2.1" +keyvalues-parser = "0.2" [dev-dependencies] tempfile = "3" diff --git a/app/src-tauri/src/commands.rs b/app/src-tauri/src/commands.rs index 7feeac8..eb7bc13 100644 --- a/app/src-tauri/src/commands.rs +++ b/app/src-tauri/src/commands.rs @@ -146,7 +146,7 @@ pub async fn game_assets(title: String) -> Result, pub async fn cancel_download(app: tauri::AppHandle, engine: tauri::State<'_, DownloadEngine>, title: String) -> Result { - let safe_title = title.replace('/', "_"); + let safe_title = crate::safe_title(&title); let http_found = { diff --git a/app/src-tauri/src/game_process.rs b/app/src-tauri/src/game_process.rs index f58def9..aa84619 100644 --- a/app/src-tauri/src/game_process.rs +++ b/app/src-tauri/src/game_process.rs @@ -1,5 +1,3 @@ -use std::process::Command; - #[cfg(not(windows))] use std::process::Stdio; @@ -13,7 +11,7 @@ pub fn game_launch_started(appid: u32) -> bool { let pattern = format!("reaper SteamLaunch AppId={}", appid); - Command::new("pgrep") + crate::quiet_command::quiet_command("pgrep") .args(["-f", &pattern]) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -79,7 +77,7 @@ fn parse_ps_game_pid(output: &str, exe_basename: &str) -> Option { #[cfg(not(windows))] pub fn find_game_pid(exe_basename: &str) -> Option { - let output = Command::new("ps").args(["-eo", "pid=,args="]).output().ok()?; + let output = crate::quiet_command::quiet_command("ps").args(["-eo", "pid=,args="]).output().ok()?; parse_ps_game_pid(&String::from_utf8_lossy(&output.stdout), exe_basename) @@ -113,7 +111,7 @@ pub fn find_game_pid(exe_basename: &str) -> Option { let filter = format!("IMAGENAME eq {}", exe_basename); - let output = Command::new("tasklist").args(["/FI", &filter, "/FO", "CSV", "/NH"]).output().ok()?; + let output = crate::quiet_command::quiet_command("tasklist").args(["/FI", &filter, "/FO", "CSV", "/NH"]).output().ok()?; parse_tasklist_csv_pid(&String::from_utf8_lossy(&output.stdout), exe_basename) @@ -131,7 +129,7 @@ pub fn pid_alive(pid: u32) -> bool { let filter = format!("PID eq {}", pid); - Command::new("tasklist") + crate::quiet_command::quiet_command("tasklist") .args(["/FI", &filter, "/NH"]) .output() .map(|output| crate::steam_client::tasklist_reports_running(&String::from_utf8_lossy(&output.stdout), &pid.to_string())) diff --git a/app/src-tauri/src/helpers.rs b/app/src-tauri/src/helpers.rs index ae1d143..d4e61c1 100644 --- a/app/src-tauri/src/helpers.rs +++ b/app/src-tauri/src/helpers.rs @@ -91,89 +91,72 @@ pub fn games_root() -> Option { } -pub fn game_folder(title: &str) -> Option { - - games_root().map(|root| root.join(title).to_string_lossy().to_string()) - -} - -pub fn find_shortcuts_vdf() -> Option { - - let mut roots: Vec = Vec::new(); - - if let Some(home) = home_dir() { - - let home_path = std::path::PathBuf::from(&home); - - roots.push(home_path.join(".steam/steam/userdata")); - - roots.push(home_path.join(".local/share/Steam/userdata")); - - } - - if let Ok(x86) = std::env::var("ProgramFiles(x86)") { +const WINDOWS_RESERVED_NAMES: [&str; 22] = [ + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", +]; + +// One folder name on every OS, valid on the strictest (Windows): catalog titles carry ':' and '?' +// ("Subtitle: Edition"), which create_dir_all rejects there. +pub fn safe_title(title: &str) -> String { + + let replaced: String = title + .chars() + .map(|character| if "<>:\"/\\|?*".contains(character) || character.is_control() { '_' } else { character }) + .collect(); - roots.push(std::path::PathBuf::from(&x86).join("Steam/userdata")); + let trimmed = replaced.trim_end_matches(['.', ' ']).trim_start(); - } + let stem = trimmed.split('.').next().unwrap_or("").to_uppercase(); - if let Ok(program_files) = std::env::var("ProgramFiles") { + if trimmed.is_empty() { - roots.push(std::path::PathBuf::from(&program_files).join("Steam/userdata")); + return String::from("_"); } - if let Ok(steam) = std::env::var("STEAM_PATH") { + if WINDOWS_RESERVED_NAMES.contains(&stem.as_str()) { - roots.push(std::path::PathBuf::from(steam).join("userdata")); + return format!("{}_", trimmed); } - for userdata in roots { - - let Ok(entries) = std::fs::read_dir(&userdata) else { - - continue; + trimmed.to_string() - }; +} - for entry in entries.flatten() { +// Linux installs made before safe_title keep their raw folder (':' was legal there). +pub fn game_folder(title: &str) -> Option { - let candidate = entry.path().join("config").join("shortcuts.vdf"); + let root = games_root()?; - if candidate.exists() { + let legacy = root.join(title); - return candidate.to_str().map(String::from); + let folder = if legacy.is_dir() { legacy } else { root.join(safe_title(title)) }; - } + Some(folder.to_string_lossy().to_string()) - } +} - } +pub(crate) fn start_dir_of(exe: &str) -> String { - None + std::path::Path::new(exe) + .parent() + .map(|parent| format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR)) + .unwrap_or_default() } pub fn write_stable_shortcut(title: &str, exe: &str, appid: u32) -> Result<(), String> { - let Some(vdf) = find_shortcuts_vdf() else { - - return Err(String::from("steam shortcuts.vdf not found")); - - }; + let vdf = crate::steam_user::shortcuts_vdf()?; let _ = fix_core::remove_steam_shortcut(&vdf, title); - let start_dir = std::path::Path::new(exe) + let start_dir = start_dir_of(exe); - .parent() - - .map(|parent| format!("{}/", parent.to_string_lossy())) - - .unwrap_or_default(); - - fix_core::add_shortcut_with_appid(&vdf, title, exe, &start_dir, fix_core::ONLINE_FIX_LAUNCH_OPTIONS, appid)?; + fix_core::add_shortcut_with_appid(&vdf, title, exe, &start_dir, fix_core::launch_options(), appid)?; crate::flog(&format!("[STEAM] {}: stable shortcut written, appid {}", title, appid)); @@ -182,11 +165,18 @@ pub fn write_stable_shortcut(title: &str, exe: &str, appid: u32) -> Result<(), S } pub fn add_game_to_steam(title: &str, folder: &str) -> bool { - let Some(vdf) = find_shortcuts_vdf() else { - flog(&format!("[DL] {}: steam shortcuts.vdf not found", title)); + let vdf = match crate::steam_user::shortcuts_vdf() { - return false; + Ok(vdf) => vdf, + + Err(error) => { + + flog(&format!("[DL] {}: not added to Steam: {}", title, error)); + + return false; + + } }; @@ -206,15 +196,9 @@ pub fn add_game_to_steam(title: &str, folder: &str) -> bool { }; - let start_dir = std::path::Path::new(&exe) + let start_dir = start_dir_of(&exe); - .parent() - - .map(|parent| format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR)) - - .unwrap_or_default(); - - match fix_core::add_steam_shortcut(&vdf, title, &exe, &start_dir, fix_core::ONLINE_FIX_LAUNCH_OPTIONS) { + match fix_core::add_steam_shortcut(&vdf, title, &exe, &start_dir, fix_core::launch_options()) { Ok(index) => { @@ -276,3 +260,7 @@ pub fn log_fail(title: &str, step: &str, error: String) -> String { } + +#[cfg(test)] +#[path = "helpers_tests.rs"] +mod helpers_tests; diff --git a/app/src-tauri/src/helpers_tests.rs b/app/src-tauri/src/helpers_tests.rs new file mode 100644 index 0000000..ec63472 --- /dev/null +++ b/app/src-tauri/src/helpers_tests.rs @@ -0,0 +1,45 @@ +use super::*; + +#[test] +fn safe_title_strips_every_char_windows_rejects() { + + assert_eq!(safe_title("Subtitle: Edition?"), "Subtitle_ Edition_"); + + assert_eq!(safe_title("A/B\\C|D*E\"FH"), "A_B_C_D_E_F_G_H"); + +} + +#[test] +fn safe_title_keeps_legal_titles_untouched() { + + assert_eq!(safe_title("BOMBANANA!"), "BOMBANANA!"); + + assert_eq!(safe_title("Tom Clancy's Rainbow Six® Siege"), "Tom Clancy's Rainbow Six® Siege"); + +} + +#[test] +fn safe_title_fixes_trailing_dots_and_reserved_names() { + + assert_eq!(safe_title("Wait..."), "Wait"); + + assert_eq!(safe_title("CON"), "CON_"); + + assert_eq!(safe_title("nul.txt"), "nul.txt_"); + + assert_eq!(safe_title("???"), "___"); + + assert_eq!(safe_title("..."), "_"); + +} + +#[test] +fn start_dir_ends_with_the_platform_separator() { + + let exe = std::path::Path::new("games").join("Foo").join("Foo.exe"); + + let expected = format!("{}{}", std::path::Path::new("games").join("Foo").display(), std::path::MAIN_SEPARATOR); + + assert_eq!(start_dir_of(&exe.to_string_lossy()), expected); + +} diff --git a/app/src-tauri/src/launch.rs b/app/src-tauri/src/launch.rs index 27e0993..15f2534 100644 --- a/app/src-tauri/src/launch.rs +++ b/app/src-tauri/src/launch.rs @@ -1,5 +1,4 @@ use crate::flog; -use crate::find_shortcuts_vdf; // Steam is the only thing that can hand the game a real session: `reaper SteamLaunch AppId=...` // registers the process with the running client, which is what makes online-fix's replaced @@ -11,9 +10,9 @@ use crate::find_shortcuts_vdf; // exit, so registering a game costs one client restart. `-silent` makes that restart invisible. pub(crate) fn steam_paths() -> Result<(String, std::path::PathBuf), String> { - let vdf = find_shortcuts_vdf().ok_or_else(|| String::from("steam shortcuts.vdf not found"))?; + let root = crate::steam_client::steam_root().ok_or_else(|| String::from(crate::user_error::STEAM_NOT_INSTALLED))?; - let root = crate::steam_client::steam_root().ok_or_else(|| String::from("steam root not found"))?; + let vdf = crate::steam_user::shortcuts_vdf()?; Ok((vdf, root)) @@ -110,7 +109,7 @@ async fn cef_launch_flow(app: &tauri::AppHandle, title: &str, exe: &str) -> Resu let emitted = crate::steam_ipc::cef_add_shortcut(title, exe).await?; - crate::steam_ipc::cef_set_launch_options(emitted, fix_core::ONLINE_FIX_LAUNCH_OPTIONS).await?; + crate::steam_ipc::cef_set_launch_options(emitted, fix_core::launch_options()).await?; let _ = crate::steam_ipc::cef_remove_shortcut(emitted).await; @@ -120,12 +119,6 @@ async fn cef_launch_flow(app: &tauri::AppHandle, title: &str, exe: &str) -> Resu fix_core::ensure_compat_tool(&root.to_string_lossy(), emitted, fix_core::DEFAULT_COMPAT_TOOL)?; - for orphan in [3496708822u32, 3805198223, 3665257087, 3148280713] { - - let _ = fix_core::remove_compat_tool(&root.to_string_lossy(), orphan); - - } - crate::launch_progress::emit_progress(app, title, "starting-steam", ""); crate::steam_client::start_silent()?; @@ -282,7 +275,7 @@ async fn legacy_flow(app: &tauri::AppHandle, title: &str, folder: &str) -> Resul if !crate::launch_progress::wait_with_progress(app, title) { - let message = String::from("steam did not come up"); + let message = String::from(crate::user_error::STEAM_DID_NOT_START); flog(&format!("[LAUNCH] {}: {}", title, message)); @@ -320,7 +313,7 @@ async fn legacy_flow(app: &tauri::AppHandle, title: &str, folder: &str) -> Resul crate::launch_progress::emit_progress(app, title, "launching", ""); - let exe = fix_core::find_game_exe(folder).ok_or_else(|| String::from("game exe not found"))?; + let exe = fix_core::find_game_exe(folder).ok_or_else(|| String::from(crate::user_error::GAME_EXE_MISSING))?; let (vdf, _) = steam_paths()?; @@ -414,11 +407,9 @@ async fn run_launch(app: &tauri::AppHandle, title: &str) -> Result Result Ok(result), + Err(cef_error) if crate::user_error::needs_person(&cef_error) => { + + flog(&format!("[LAUNCH] {}: {}", title, cef_error)); + + Err(cef_error) + + } + Err(cef_error) => { flog(&format!("[LAUNCH] {}: CEF flow failed ({}), falling back to legacy vdf flow", title, cef_error)); - legacy_flow(app, title, &folder).await + let legacy = legacy_flow(app, title, &folder).await; + + if let Err(legacy_error) = &legacy { + + flog(&format!("[LAUNCH] {}: legacy flow failed ({})", title, legacy_error)); + + } + + legacy } @@ -454,7 +461,13 @@ async fn run_launch(app: &tauri::AppHandle, title: &str) -> Result Result { - let result = run_launch(&app, &title).await; + let result = run_launch(&app, &title).await.map_err(|error| { + + flog(&format!("[LAUNCH] {}: failed: {}", title, error)); + + crate::user_error::for_person(error) + + }); if let Err(error) = &result { diff --git a/app/src-tauri/src/launch_monitor.rs b/app/src-tauri/src/launch_monitor.rs index 04f808f..6c4d4c3 100644 --- a/app/src-tauri/src/launch_monitor.rs +++ b/app/src-tauri/src/launch_monitor.rs @@ -243,7 +243,7 @@ fn fix_site_connection_open() -> bool { } - let Ok(output) = std::process::Command::new("ss") + let Ok(output) = crate::quiet_command::quiet_command("ss") .args(["-tunp", "state", "established"]) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index b248495..57145ce 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -14,9 +14,12 @@ mod launch_progress; mod library; mod pipeline_http; mod plugin; +mod quiet_command; mod steam_client; pub mod steam_ipc; mod steam_root; +mod steam_user; +mod user_error; mod pipeline_torrent; mod state; diff --git a/app/src-tauri/src/library.rs b/app/src-tauri/src/library.rs index 2759d41..c0b830a 100644 --- a/app/src-tauri/src/library.rs +++ b/app/src-tauri/src/library.rs @@ -242,7 +242,7 @@ pub fn open_game_folder(folder: String) -> Result<(), String> { #[cfg(all(unix, not(target_os = "macos")))] let opener = "xdg-open"; - std::process::Command::new(opener) + crate::quiet_command::quiet_command(opener) .arg(&folder) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) diff --git a/app/src-tauri/src/pipeline_http.rs b/app/src-tauri/src/pipeline_http.rs index 3df98a7..11d0afc 100644 --- a/app/src-tauri/src/pipeline_http.rs +++ b/app/src-tauri/src/pipeline_http.rs @@ -18,7 +18,7 @@ pub async fn start_http_download(app: tauri::AppHandle, title: String, lane_url: .ok_or_else(|| log_fail(&title, "mirror resolve", format!("no direct mirror in hosters lane {}", lane_url)))?; - let safe_title = title.replace('/', "_"); + let safe_title = crate::safe_title(&title); let folder = crate::game_folder(&safe_title) diff --git a/app/src-tauri/src/pipeline_torrent.rs b/app/src-tauri/src/pipeline_torrent.rs index 5f8dabd..ca80189 100644 --- a/app/src-tauri/src/pipeline_torrent.rs +++ b/app/src-tauri/src/pipeline_torrent.rs @@ -24,7 +24,7 @@ pub async fn start_torrent_download(app: tauri::AppHandle, engine: tauri::State< .map_err(|error| log_fail(&title, "torrent fetch", error))?; - let safe_title = title.replace('/', "_"); + let safe_title = crate::safe_title(&title); if let Ok(active) = engine.torrents.lock() { diff --git a/app/src-tauri/src/quiet_command.rs b/app/src-tauri/src/quiet_command.rs new file mode 100644 index 0000000..a1e7a75 --- /dev/null +++ b/app/src-tauri/src/quiet_command.rs @@ -0,0 +1,24 @@ +use std::process::Command; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +// A GUI-subsystem app that spawns a console program (tasklist, reg, cmd) gets a console window +// flashed per call unless CREATE_NO_WINDOW is set; tasklist runs every 500ms while Steam boots. +pub fn quiet_command(program: impl AsRef) -> Command { + + #[allow(unused_mut)] + let mut command = Command::new(program); + + #[cfg(windows)] + { + + use std::os::windows::process::CommandExt; + + command.creation_flags(CREATE_NO_WINDOW); + + } + + command + +} diff --git a/app/src-tauri/src/steam_client.rs b/app/src-tauri/src/steam_client.rs index ee597eb..922aaea 100644 --- a/app/src-tauri/src/steam_client.rs +++ b/app/src-tauri/src/steam_client.rs @@ -32,7 +32,7 @@ pub use crate::steam_root::steam_root; #[cfg(not(windows))] fn process_alive(name: &str) -> bool { - Command::new("pgrep") + crate::quiet_command::quiet_command("pgrep") .args(["-x", name]) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -50,7 +50,7 @@ fn process_alive(image_name: &str) -> bool { let filter = format!("IMAGENAME eq {}", image_name); - Command::new("tasklist") + crate::quiet_command::quiet_command("tasklist") .args(["/FI", &filter, "/NH"]) .output() .map(|output| tasklist_reports_running(&String::from_utf8_lossy(&output.stdout), image_name)) @@ -239,7 +239,7 @@ fn steam_executable() -> Result { steam_root() .map(|root| root.join("steam.exe")) - .ok_or_else(|| String::from("steam root not found")) + .ok_or_else(|| String::from(crate::user_error::STEAM_NOT_INSTALLED)) } @@ -248,7 +248,7 @@ pub fn start_silent() -> Result<(), String> { let exe = steam_executable()?; - let mut command = Command::new(exe); + let mut command = crate::quiet_command::quiet_command(exe); command.arg("-silent"); @@ -256,10 +256,29 @@ pub fn start_silent() -> Result<(), String> { } +#[cfg(not(windows))] +fn steam_command() -> Command { + + let flatpak = steam_root().map(|root| crate::steam_root::is_flatpak_steam(&root)).unwrap_or(false); + + if !flatpak { + + return crate::quiet_command::quiet_command("steam"); + + } + + let mut command = crate::quiet_command::quiet_command("flatpak"); + + command.args(["run", "com.valvesoftware.Steam"]); + + command + +} + #[cfg(not(windows))] pub fn start_silent() -> Result<(), String> { - let mut command = Command::new("steam"); + let mut command = steam_command(); command.arg("-silent"); @@ -272,7 +291,7 @@ pub fn shutdown() -> Result<(), String> { let exe = steam_executable()?; - let mut command = Command::new(exe); + let mut command = crate::quiet_command::quiet_command(exe); command.arg("-shutdown"); @@ -283,7 +302,7 @@ pub fn shutdown() -> Result<(), String> { #[cfg(not(windows))] pub fn shutdown() -> Result<(), String> { - let mut command = Command::new("steam"); + let mut command = steam_command(); command.arg("-shutdown"); @@ -324,7 +343,7 @@ pub fn open_url(url: &str) -> Result { #[cfg(windows)] let mut command = { - let mut command = Command::new("cmd"); + let mut command = crate::quiet_command::quiet_command("cmd"); command.args(["/C", "start", "", url]); @@ -335,7 +354,7 @@ pub fn open_url(url: &str) -> Result { #[cfg(not(windows))] let mut command = { - let mut command = Command::new("steam"); + let mut command = steam_command(); command.arg(url); diff --git a/app/src-tauri/src/steam_ipc.rs b/app/src-tauri/src/steam_ipc.rs index ae6cb80..f157d70 100644 --- a/app/src-tauri/src/steam_ipc.rs +++ b/app/src-tauri/src/steam_ipc.rs @@ -11,7 +11,7 @@ pub fn cef_flag_path() -> Option { pub fn ensure_cef_flag() -> Result { - let path = cef_flag_path().ok_or_else(|| String::from("steam root not found"))?; + let path = cef_flag_path().ok_or_else(|| String::from(crate::user_error::STEAM_NOT_INSTALLED))?; if path.exists() { diff --git a/app/src-tauri/src/steam_root.rs b/app/src-tauri/src/steam_root.rs index ff47b61..5d25333 100644 --- a/app/src-tauri/src/steam_root.rs +++ b/app/src-tauri/src/steam_root.rs @@ -1,35 +1,46 @@ use std::path::PathBuf; -#[cfg(windows)] -use std::process::Command; +const FLATPAK_MARKER: &str = "com.valvesoftware.Steam"; -#[cfg(not(windows))] -pub fn steam_root() -> Option { +// steamlocate covers native, Flatpak, Snap and debian-installation on Linux and the HKLM +// InstallPath on Windows; HKCU SteamPath is Steam's own per-launch record and catches installs +// the machine-wide key misses. +fn candidates() -> Vec { - let home = PathBuf::from(crate::home_dir()?); + #[cfg_attr(not(windows), allow(unused_mut))] + let mut found: Vec = steamlocate::locate_all() + .map(|dirs| dirs.into_iter().map(|dir| dir.path().to_path_buf()).collect()) + .unwrap_or_default(); - for candidate in [home.join(".local/share/Steam"), home.join(".steam/steam")] { + #[cfg(windows)] + if let Some(path) = steam_root_from_registry() { - if candidate.join("config/config.vdf").is_file() { + found.push(path); - return Some(candidate); + } - } + found - } +} - None +pub fn steam_root() -> Option { + + candidates() + .into_iter() + .find(|candidate| candidate.join("config/config.vdf").is_file()) + +} + +pub fn is_flatpak_steam(root: &std::path::Path) -> bool { + + root.to_string_lossy().contains(FLATPAK_MARKER) } -// Steam writes its own install path to the registry on every launch (SteamPath, REG_SZ), so that -// is checked before the Program Files defaults — a custom install drive would otherwise never be -// found. Confirmed against public SteamPath registry documentation; not exercised against a real -// Windows registry from this machine. #[cfg(windows)] fn steam_root_from_registry() -> Option { - let output = Command::new("reg") + let output = crate::quiet_command::quiet_command("reg") .args(["query", r"HKCU\Software\Valve\Steam", "/v", "SteamPath"]) .output() .ok()?; @@ -79,35 +90,6 @@ fn parse_reg_sz_value(output: &str, value_name: &str) -> Option { } -#[cfg(windows)] -pub fn steam_root() -> Option { - - let mut candidates: Vec = Vec::new(); - - if let Some(path) = steam_root_from_registry() { - - candidates.push(path); - - } - - if let Ok(x86) = std::env::var("ProgramFiles(x86)") { - - candidates.push(PathBuf::from(x86).join("Steam")); - - } - - if let Ok(program_files) = std::env::var("ProgramFiles") { - - candidates.push(PathBuf::from(program_files).join("Steam")); - - } - - candidates - .into_iter() - .find(|candidate| candidate.join("config/config.vdf").is_file()) - -} - #[cfg(test)] #[path = "steam_root_tests.rs"] mod steam_root_tests; diff --git a/app/src-tauri/src/steam_user.rs b/app/src-tauri/src/steam_user.rs new file mode 100644 index 0000000..b8f7330 --- /dev/null +++ b/app/src-tauri/src/steam_user.rs @@ -0,0 +1,153 @@ +use std::path::Path; + +use std::path::PathBuf; + +const STEAMID64_BASE: u64 = 76_561_197_960_265_728; + +// Steam writes shortcuts.vdf only once the account adds its first non-Steam game, so a fresh +// account has none (measured 2026-09-25 on the owner's Windows: userdata//config held +// localconfig.vdf and no shortcuts.vdf; every Play died "steam shortcuts.vdf not found"). +const EMPTY_SHORTCUTS_VDF: &[u8] = b"\x00shortcuts\x00\x08\x08"; + +struct LoginUser { + account_id: u32, + most_recent: bool, + timestamp: u64, +} + +fn read_login_users(text: &str) -> Vec { + + let Ok(parsed) = keyvalues_parser::parse(text) else { + + return Vec::new(); + + }; + + let Some(users) = parsed.value.get_obj() else { + + return Vec::new(); + + }; + + let mut found = Vec::new(); + + for (steam_id, values) in users.iter() { + + let Ok(id64) = steam_id.parse::() else { continue }; + + let Some(fields) = values.first().and_then(|value| value.get_obj()) else { continue }; + + let field = |name: &str| { + + fields + .get(name) + .and_then(|values| values.first()) + .and_then(|value| value.get_str()) + .map(String::from) + + }; + + found.push(LoginUser { + account_id: id64.saturating_sub(STEAMID64_BASE) as u32, + most_recent: field("MostRecent").as_deref() == Some("1"), + timestamp: field("Timestamp").and_then(|text| text.parse().ok()).unwrap_or(0), + }); + + } + + found + +} + +// Newer clients stopped writing MostRecent (the owner's Windows loginusers.vdf carries only +// Timestamp), so the latest Timestamp decides when no entry is flagged. +pub(crate) fn active_account_id(login_users_vdf: &str) -> Option { + + let users = read_login_users(login_users_vdf); + + if let Some(flagged) = users.iter().find(|user| user.most_recent) { + + return Some(flagged.account_id); + + } + + users.iter().max_by_key(|user| user.timestamp).map(|user| user.account_id) + +} + +fn newest_userdata_dir(userdata: &Path) -> Option { + + std::fs::read_dir(userdata) + .ok()? + .flatten() + .map(|entry| entry.path()) + .filter(|path| { + + let name = path.file_name().and_then(|name| name.to_str()).unwrap_or(""); + + name != "0" && name.parse::().is_ok() && path.join("config").is_dir() + + }) + .max_by_key(|path| path.join("config").metadata().and_then(|meta| meta.modified()).ok()) + +} + +pub(crate) fn account_dir(root: &Path) -> Option { + + let userdata = root.join("userdata"); + + let login_users = std::fs::read_to_string(root.join("config/loginusers.vdf")).unwrap_or_default(); + + if let Some(account) = active_account_id(&login_users) { + + let dir = userdata.join(account.to_string()); + + if dir.is_dir() { + + return Some(dir); + + } + + } + + newest_userdata_dir(&userdata) + +} + +pub(crate) fn ensure_shortcuts_vdf(account_dir: &Path) -> Result { + + let config = account_dir.join("config"); + + let vdf = config.join("shortcuts.vdf"); + + if vdf.is_file() { + + return Ok(vdf); + + } + + std::fs::create_dir_all(&config).map_err(|error| format!("create {}: {}", config.display(), error))?; + + std::fs::write(&vdf, EMPTY_SHORTCUTS_VDF).map_err(|error| format!("create {}: {}", vdf.display(), error))?; + + crate::flog(&format!("[STEAM] created empty shortcuts.vdf at {}", vdf.display())); + + Ok(vdf) + +} + +pub fn shortcuts_vdf() -> Result { + + let root = crate::steam_root::steam_root().ok_or_else(|| String::from(crate::user_error::STEAM_NOT_INSTALLED))?; + + let account = account_dir(&root).ok_or_else(|| String::from(crate::user_error::STEAM_NEVER_LOGGED_IN))?; + + let vdf = ensure_shortcuts_vdf(&account)?; + + Ok(vdf.to_string_lossy().into_owned()) + +} + +#[cfg(test)] +#[path = "steam_user_tests.rs"] +mod steam_user_tests; diff --git a/app/src-tauri/src/steam_user_tests.rs b/app/src-tauri/src/steam_user_tests.rs new file mode 100644 index 0000000..c030097 --- /dev/null +++ b/app/src-tauri/src/steam_user_tests.rs @@ -0,0 +1,84 @@ +use super::*; + +const OWNER_WINDOWS_LOGINUSERS: &str = "\"users\"\n{\n\t\"76561198216286741\"\n\t{\n\t\t\"AccountName\"\t\t\"someone\"\n\t\t\"PersonaName\"\t\t\"someone\"\n\t\t\"RememberPassword\"\t\t\"1\"\n\t\t\"AutoLogin\"\t\t\"1\"\n\t\t\"Timestamp\"\t\t\"1790381748\"\n\t}\n}\n"; + +#[test] +fn active_account_reads_timestamp_only_file_from_real_windows_client() { + + assert_eq!(active_account_id(OWNER_WINDOWS_LOGINUSERS), Some(256_021_013)); + +} + +#[test] +fn most_recent_flag_beats_newer_timestamp() { + + let text = "\"users\"\n{\n\"76561198000000001\"\n{\n\"MostRecent\" \"1\"\n\"Timestamp\" \"10\"\n}\n\"76561198000000002\"\n{\n\"MostRecent\" \"0\"\n\"Timestamp\" \"99\"\n}\n}\n"; + + assert_eq!(active_account_id(text), Some((76_561_198_000_000_001u64 - STEAMID64_BASE) as u32)); + +} + +#[test] +fn latest_timestamp_wins_when_nothing_flagged() { + + let text = "\"users\"\n{\n\"76561198000000001\"\n{\n\"Timestamp\" \"10\"\n}\n\"76561198000000002\"\n{\n\"Timestamp\" \"99\"\n}\n}\n"; + + assert_eq!(active_account_id(text), Some((76_561_198_000_000_002u64 - STEAMID64_BASE) as u32)); + +} + +#[test] +fn garbage_loginusers_yields_no_account() { + + assert_eq!(active_account_id(""), None); + + assert_eq!(active_account_id("not vdf {{{"), None); + +} + +#[test] +fn account_dir_picks_logged_in_user_over_other_userdata() { + + let root = tempfile::tempdir().unwrap(); + + std::fs::create_dir_all(root.path().join("config")).unwrap(); + + std::fs::write(root.path().join("config/loginusers.vdf"), OWNER_WINDOWS_LOGINUSERS).unwrap(); + + std::fs::create_dir_all(root.path().join("userdata/111/config")).unwrap(); + + std::fs::create_dir_all(root.path().join("userdata/256021013/config")).unwrap(); + + assert_eq!(account_dir(root.path()), Some(root.path().join("userdata/256021013"))); + +} + +#[test] +fn fresh_account_gets_a_shortcuts_file_steam_code_can_append_to() { + + let root = tempfile::tempdir().unwrap(); + + let account = root.path().join("userdata/256021013"); + + std::fs::create_dir_all(account.join("config")).unwrap(); + + let vdf = ensure_shortcuts_vdf(&account).unwrap(); + + let path = vdf.to_str().unwrap(); + + let appid = fix_core::add_steam_shortcut(path, "BOMBANANA!", "C:\\Users\\Eder\\games\\BOMBANANA!\\Bombanana.exe", "C:\\Users\\Eder\\games\\BOMBANANA!\\", "").unwrap(); + + assert_eq!(fix_core::find_shortcut_appid(path, "BOMBANANA!"), Some(appid)); + + assert_eq!(ensure_shortcuts_vdf(&account).unwrap(), vdf); + + let independent: Vec = steamlocate::SteamDir::from_dir(root.path()) + .unwrap() + .shortcuts() + .unwrap() + .map(|shortcut| shortcut.unwrap().app_name) + .collect(); + + assert_eq!(independent, vec![String::from("BOMBANANA!")]); + +} diff --git a/app/src-tauri/src/user_error.rs b/app/src-tauri/src/user_error.rs new file mode 100644 index 0000000..321fb18 --- /dev/null +++ b/app/src-tauri/src/user_error.rs @@ -0,0 +1,32 @@ +pub const STEAM_NOT_INSTALLED: &str = "Steam is not installed. Install Steam from store.steampowered.com, sign in once, then press Play again."; + +pub const STEAM_NEVER_LOGGED_IN: &str = "Steam has no signed-in account yet. Open Steam, sign in, then press Play again."; + +pub const STEAM_DID_NOT_START: &str = "Steam did not finish starting. Open Steam yourself, wait until the library shows, then press Play again."; + +pub const STEAM_LAUNCH_FAILED: &str = "Steam could not start the game. Close Steam completely, open it again, then press Play. Details are in the app log."; + +pub const GAME_EXE_MISSING: &str = "The game files are incomplete (no game .exe found). Uninstall and download it again."; + +const USER_FACING: [&str; 4] = [STEAM_NOT_INSTALLED, STEAM_NEVER_LOGGED_IN, STEAM_DID_NOT_START, GAME_EXE_MISSING]; + +// These conditions fail identically on every launch path, so the fallback path is skipped and the +// person gets the step they must take instead of a second, unrelated failure. +pub fn needs_person(message: &str) -> bool { + + USER_FACING.contains(&message) + +} + +// Internal failures (CEF, vdf, process) mean nothing to a player; the raw text stays in the log. +pub fn for_person(message: String) -> String { + + if needs_person(&message) { + + return message; + + } + + String::from(STEAM_LAUNCH_FAILED) + +} diff --git a/core/src/compat_tool.rs b/core/src/compat_tool.rs index 6b14ef4..d45f936 100644 --- a/core/src/compat_tool.rs +++ b/core/src/compat_tool.rs @@ -56,6 +56,12 @@ pub(crate) fn insert_mapping(body: &str, appid: u32, tool: &str) -> Option bool { + if !compat_tool_needed() { + + return true; + + } + std::fs::read_to_string(config_vdf_path(steam_root)) .map(|body| has_mapping(&body, appid)) @@ -64,8 +70,22 @@ pub fn is_compat_tool_mapped(steam_root: &str, appid: u32) -> bool { } +// Windows runs the .exe natively; a Proton mapping there is dead config and, read as "unmapped", +// forced a Steam restart on every Play. +pub fn compat_tool_needed() -> bool { + + !cfg!(windows) + +} + pub fn ensure_compat_tool(steam_root: &str, appid: u32, tool: &str) -> Result { + if !compat_tool_needed() { + + return Ok(false); + + } + let path = config_vdf_path(steam_root); let body = std::fs::read_to_string(&path).map_err(|error| format!("read {}: {}", path, error))?; diff --git a/core/src/extract.rs b/core/src/extract.rs index 4b38999..7018370 100644 --- a/core/src/extract.rs +++ b/core/src/extract.rs @@ -10,7 +10,10 @@ pub fn install_plugin(archive_path: &str, game_dir: &str) -> Result if data.starts_with(b"MZ") { - let name = archive_path.rsplit('/').next().unwrap_or("plugin.dll"); + let name = std::path::Path::new(archive_path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| String::from("plugin.dll")); let plugins_dir = format!("{}/BepInEx/plugins", game_dir); @@ -162,17 +165,15 @@ fn place_extracted(source: &str, game_dir: &str) -> Result { } - let relative = path + let relative_path = path .strip_prefix(source) - .map_err(|error| format!("relative: {}", error))? + .map_err(|error| format!("relative: {}", error))?; - .to_string_lossy() + let relative = relative_path.to_string_lossy().to_string(); - .to_string(); - - let base = relative.rsplit('/').next().unwrap_or("").to_lowercase(); + let base = relative_path.file_name().map(|name| name.to_string_lossy().to_lowercase()).unwrap_or_default(); if PLUGIN_META_FILES.contains(&base.as_str()) { @@ -180,7 +181,7 @@ fn place_extracted(source: &str, game_dir: &str) -> Result { } - let root_dll = !relative.contains('/') && relative.to_lowercase().ends_with(".dll"); + let root_dll = relative_path.components().count() == 1 && relative.to_lowercase().ends_with(".dll"); let dest = if root_dll { diff --git a/core/src/main.rs b/core/src/main.rs index 1042333..a6c474e 100644 --- a/core/src/main.rs +++ b/core/src/main.rs @@ -43,7 +43,7 @@ fn main() { "/home/eder/games/Friendly Steps/Friendly Steps/", - fix_core::ONLINE_FIX_LAUNCH_OPTIONS, + fix_core::launch_options(), ) { diff --git a/core/src/shortcut_add.rs b/core/src/shortcut_add.rs index 1f43cfd..85d11cf 100644 --- a/core/src/shortcut_add.rs +++ b/core/src/shortcut_add.rs @@ -10,6 +10,14 @@ pub const ONLINE_FIX_DLL_OVERRIDES: &str = "winhttp=n,b;WINMM=n,b;SteamOverlay64 pub const ONLINE_FIX_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"winhttp=n,b;OnlineFix64=n;SteamOverlay64=n;winmm=n,b;dnet=n;steam_api64=n\" %command%"; +// WINEDLLOVERRIDES only means something under Wine/Proton; native Windows loads the game-folder +// DLLs by itself. +pub fn launch_options() -> &'static str { + + if cfg!(windows) { "" } else { ONLINE_FIX_LAUNCH_OPTIONS } + +} + pub fn add_steam_shortcut(vdf_path: &str, app_name: &str, exe_path: &str, start_dir: &str, launch_options: &str) -> Result { let data = std::fs::read(vdf_path).map_err(|error| format!("read {}: {}", vdf_path, error))?; From a2f0524816f6423153c94b7fdd23fc827eb6cb28 Mon Sep 17 00:00:00 2001 From: ederson-shaw Date: Sat, 26 Sep 2026 00:34:08 -0300 Subject: [PATCH 2/4] fix: flatpak steam helpers are linux-only (windows ci dead code) Co-Authored-By: Claude Opus 5.5 --- app/src-tauri/src/steam_root.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src-tauri/src/steam_root.rs b/app/src-tauri/src/steam_root.rs index 5d25333..6282914 100644 --- a/app/src-tauri/src/steam_root.rs +++ b/app/src-tauri/src/steam_root.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +#[cfg(not(windows))] const FLATPAK_MARKER: &str = "com.valvesoftware.Steam"; // steamlocate covers native, Flatpak, Snap and debian-installation on Linux and the HKLM @@ -31,6 +32,7 @@ pub fn steam_root() -> Option { } +#[cfg(not(windows))] pub fn is_flatpak_steam(root: &std::path::Path) -> bool { root.to_string_lossy().contains(FLATPAK_MARKER) From e0717eff714f1c411b76c374aa9628063c694c46 Mon Sep 17 00:00:00 2001 From: ederson-shaw Date: Sat, 26 Sep 2026 00:45:04 -0300 Subject: [PATCH 3/4] test: gate unix-only tests so the app suite compiles on windows Co-Authored-By: Claude Opus 5.5 --- app/src-tauri/src/launch_monitor_tests.rs | 1 + app/src-tauri/src/library_tests.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src-tauri/src/launch_monitor_tests.rs b/app/src-tauri/src/launch_monitor_tests.rs index 1f29dfa..88879e1 100644 --- a/app/src-tauri/src/launch_monitor_tests.rs +++ b/app/src-tauri/src/launch_monitor_tests.rs @@ -70,6 +70,7 @@ fn poll_until_some_returns_the_value_once_the_check_finds_it() { } +#[cfg(not(windows))] #[test] #[ignore = "needs python3, ss and outbound https reach (live network)"] fn connection_signal_detects_browser_named_established_socket() { diff --git a/app/src-tauri/src/library_tests.rs b/app/src-tauri/src/library_tests.rs index eff03af..8354b10 100644 --- a/app/src-tauri/src/library_tests.rs +++ b/app/src-tauri/src/library_tests.rs @@ -74,6 +74,7 @@ fn uninstall_game_rejects_a_missing_folder() { } +#[cfg(unix)] #[test] fn uninstall_game_rejects_a_symlink_and_deletes_nothing() { From b2ec90590722b9c1ad772171c993f4ff484708a9 Mon Sep 17 00:00:00 2001 From: ederson-shaw Date: Sat, 26 Sep 2026 00:53:45 -0300 Subject: [PATCH 4/4] test: bundle-path env stripping is linux-only, tested only there Co-Authored-By: Claude Opus 5.5 --- app/src-tauri/src/steam_client.rs | 4 ++-- app/src-tauri/src/steam_client_tests.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src-tauri/src/steam_client.rs b/app/src-tauri/src/steam_client.rs index 922aaea..081a880 100644 --- a/app/src-tauri/src/steam_client.rs +++ b/app/src-tauri/src/steam_client.rs @@ -116,7 +116,7 @@ fn bundle_markers() -> Vec { } -#[cfg(any(not(windows), test))] +#[cfg(not(windows))] pub(crate) fn strip_bundle_paths(value: &str, markers: &[String]) -> String { let kept: Vec = std::env::split_paths(value) @@ -145,7 +145,7 @@ pub(crate) fn strip_bundle_paths(value: &str, markers: &[String]) -> String { } -#[cfg(any(not(windows), test))] +#[cfg(not(windows))] pub(crate) fn carries_bundle_path(value: &str, markers: &[String]) -> bool { markers.iter().any(|marker| value.contains(marker.as_str())) diff --git a/app/src-tauri/src/steam_client_tests.rs b/app/src-tauri/src/steam_client_tests.rs index 14cd057..13d5213 100644 --- a/app/src-tauri/src/steam_client_tests.rs +++ b/app/src-tauri/src/steam_client_tests.rs @@ -34,6 +34,7 @@ fn tasklist_reports_running_is_case_insensitive() { } +#[cfg(not(windows))] #[test] fn strip_bundle_paths_removes_appimage_entries_and_keeps_system_paths() { @@ -47,6 +48,7 @@ fn strip_bundle_paths_removes_appimage_entries_and_keeps_system_paths() { } +#[cfg(not(windows))] #[test] fn strip_bundle_paths_keeps_a_clean_path_intact() { @@ -58,6 +60,7 @@ fn strip_bundle_paths_keeps_a_clean_path_intact() { } +#[cfg(not(windows))] #[test] fn strip_bundle_paths_empties_a_fully_polluted_value() { @@ -69,6 +72,7 @@ fn strip_bundle_paths_empties_a_fully_polluted_value() { } +#[cfg(not(windows))] #[test] fn strip_bundle_paths_strips_every_ld_library_entry_of_the_bundle() { @@ -82,6 +86,7 @@ fn strip_bundle_paths_strips_every_ld_library_entry_of_the_bundle() { } +#[cfg(not(windows))] #[test] fn carries_bundle_path_detects_pythonhome_pointing_into_bundle() { @@ -93,6 +98,7 @@ fn carries_bundle_path_detects_pythonhome_pointing_into_bundle() { } +#[cfg(not(windows))] #[test] fn carries_bundle_path_false_for_system_values() {