From 45aefb63e84fd3c75be7b24264885a53f3fb44b8 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 1 Apr 2026 15:31:43 -0500 Subject: Fix SLR download failures and CMake rebuild detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace SHA256SUMS verification with Content-Length check for SLR downloads. Valve's CDN frequently serves stale SHA256SUMS that don't match the current archive, causing persistent checksum mismatches. Fix CMake DEPENDS for nak_ffi to glob all .rs sources instead of hardcoding a few files — previously changes to slr.rs (and most other Rust files) were silently ignored by ninja. Also wrap prefix init and runtime commands through SLR pressure-vessel container, and expose Proton install directories to the container. Co-Authored-By: Claude Opus 4.6 (1M context) --- libs/nak/src/installers/prefix_setup.rs | 6 +- libs/nak/src/runtime_wrap.rs | 47 ++- libs/nak/src/slr.rs | 490 +++++++++++++++----------------- libs/nak_ffi/CMakeLists.txt | 9 +- 4 files changed, 283 insertions(+), 269 deletions(-) (limited to 'libs') diff --git a/libs/nak/src/installers/prefix_setup.rs b/libs/nak/src/installers/prefix_setup.rs index 468aaa2..6c48b09 100644 --- a/libs/nak/src/installers/prefix_setup.rs +++ b/libs/nak/src/installers/prefix_setup.rs @@ -284,9 +284,9 @@ fn initialize_prefix_with_proton( log_install(&format!("STEAM_COMPAT_DATA_PATH={:?}", compat_data_path)); - // Collect all env vars upfront so build_command can forward them - // via --env= flags in Flatpak mode. - let mut envs: Vec<(&str, String)> = vec![ + // Collect all env vars upfront so build_command can set them on the + // process (inherited by SLR/pressure-vessel into the container). + let envs: Vec<(&str, String)> = vec![ ("STEAM_COMPAT_CLIENT_INSTALL_PATH", steam_root.clone()), ("STEAM_COMPAT_DATA_PATH", compat_data_path.display().to_string()), ("SteamAppId", app_id.to_string()), diff --git a/libs/nak/src/runtime_wrap.rs b/libs/nak/src/runtime_wrap.rs index 37980d3..943a329 100644 --- a/libs/nak/src/runtime_wrap.rs +++ b/libs/nak/src/runtime_wrap.rs @@ -2,15 +2,54 @@ use std::ffi::OsStr; use std::process::Command; /// Build a command to run `exe` with the given environment variables. +/// +/// If SLR (Steam Linux Runtime) is installed, the command is wrapped inside +/// the pressure-vessel container via the SLR `run` script. Environment +/// variables are set on the process and inherited by pressure-vessel into the +/// container (matching how game launches work via QProcess). This ensures +/// Wine/Proton commands use the container's libraries instead of potentially +/// broken host libraries. pub fn build_command>( exe: impl AsRef, envs: &[(&str, S)], ) -> Command { - let mut cmd = Command::new(exe); - for (key, value) in envs { - cmd.env(key, value.as_ref()); + if let Some(slr_script) = crate::slr::get_slr_run_script() { + let mut cmd = Command::new(&slr_script); + // Set env vars on the process — pressure-vessel inherits them + for (key, value) in envs { + cmd.env(key, value.as_ref()); + } + // Expose the executable's parent directory to the container — + // needed for system-installed Protons (e.g. /usr/share/steam/...) + // whose files may not be visible inside the container by default. + if let Some(parent) = std::path::Path::new(exe.as_ref()).parent() { + if parent.exists() { + let mut flag = std::ffi::OsString::from("--filesystem="); + flag.push(parent.as_os_str()); + cmd.arg(flag); + } + } + // Also expose WINEPREFIX if set in env vars + for (key, value) in envs { + if *key == "WINEPREFIX" || *key == "STEAM_COMPAT_DATA_PATH" { + let path = std::path::Path::new(value.as_ref()); + if path.exists() || path.parent().map_or(false, |p| p.exists()) { + let mut flag = std::ffi::OsString::from("--filesystem="); + flag.push(value.as_ref()); + cmd.arg(flag); + } + } + } + cmd.arg("--"); + cmd.arg(exe); + cmd + } else { + let mut cmd = Command::new(exe); + for (key, value) in envs { + cmd.env(key, value.as_ref()); + } + cmd } - cmd } /// Build a command with no extra environment variables. diff --git a/libs/nak/src/slr.rs b/libs/nak/src/slr.rs index 30dca38..3fe7d92 100644 --- a/libs/nak/src/slr.rs +++ b/libs/nak/src/slr.rs @@ -1,259 +1,231 @@ -//! Steam Linux Runtime (SLR) download and management for Fluorine Manager. -//! -//! Downloads SteamLinuxRuntime_sniper from Valve's official repo and stores it -//! at `~/.local/share/fluorine/steamrt/`. The `run` script is then used to -//! wrap game launches inside the pressure-vessel container, providing -//! GStreamer, 32-bit libs, and an FHS-compliant environment for non-FHS -//! distros (NixOS, etc.). - -use std::error::Error; -use std::fs; -use std::io::{self, Read, Write}; -use std::path::PathBuf; -use std::process::Command; -use std::sync::atomic::{AtomicI32, Ordering}; - -use crate::logging::{log_info, log_warning}; - -const BASE_URL: &str = - "https://repo.steampowered.com/steamrt3/images/latest-public-beta"; -const ARCHIVE_NAME: &str = "SteamLinuxRuntime_sniper.tar.xz"; -const EXTRACTED_DIR: &str = "SteamLinuxRuntime_sniper"; - -/// Directory where SLR is installed: `~/.local/share/fluorine/steamrt/` -pub fn slr_install_dir() -> PathBuf { - crate::paths::data_dir().join("steamrt") -} - -/// Path to the `run` script inside the extracted SLR. -pub fn slr_run_script() -> PathBuf { - slr_install_dir().join(EXTRACTED_DIR).join("run") -} - -/// Path where we store the remote BUILD_ID for update checks. -fn local_build_id_path() -> PathBuf { - slr_install_dir().join("BUILD_ID.txt") -} - -/// Returns true if the SLR `run` script is present and executable. -pub fn is_slr_installed() -> bool { - let script = slr_run_script(); - if !script.exists() { - return false; - } - // Verify it's actually executable - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Ok(meta) = fs::metadata(&script) { - return meta.permissions().mode() & 0o111 != 0; - } - return false; - } - #[cfg(not(unix))] - true -} - -/// Returns the path to the `run` script, or None if SLR is not installed. -pub fn get_slr_run_script() -> Option { - if is_slr_installed() { - Some(slr_run_script()) - } else { - None - } -} - -/// Fetch the remote BUILD_ID as a string. -fn fetch_remote_build_id() -> Result> { - let url = format!("{}/BUILD_ID.txt", BASE_URL); - let resp = ureq::get(&url).call()?; - let mut body = String::new(); - resp.into_reader().read_to_string(&mut body)?; - Ok(body.trim().to_string()) -} - -/// Read the locally cached BUILD_ID, if any. -fn read_local_build_id() -> Option { - fs::read_to_string(local_build_id_path()) - .ok() - .map(|s| s.trim().to_string()) -} - -/// Fetch the expected SHA256 hash for the archive from the remote SHA256SUMS file. -fn fetch_expected_sha256() -> Result> { - let url = format!("{}/SHA256SUMS", BASE_URL); - let resp = ureq::get(&url).call()?; - let mut body = String::new(); - resp.into_reader().read_to_string(&mut body)?; - - for line in body.lines() { - // Format: " " or " *" - let parts: Vec<&str> = line.splitn(2, ' ').collect(); - if parts.len() == 2 { - let hash = parts[0].trim(); - let name = parts[1].trim().trim_start_matches('*'); - if name == ARCHIVE_NAME { - return Ok(hash.to_string()); - } - } - } - Err(format!("SHA256 hash for {} not found in SHA256SUMS", ARCHIVE_NAME).into()) -} - -/// Verify a file's SHA256 hash using the system `sha256sum` command. -fn verify_sha256(file: &std::path::Path, expected: &str) -> Result<(), Box> { - let output = Command::new("sha256sum").arg(file).output()?; - if !output.status.success() { - return Err("sha256sum command failed".into()); - } - let stdout = String::from_utf8_lossy(&output.stdout); - let actual = stdout.split_whitespace().next().unwrap_or("").trim(); - if actual != expected { - return Err(format!( - "SHA256 mismatch: expected {}, got {}", - expected, actual - ) - .into()); - } - Ok(()) -} - -/// Download the archive with streaming progress. -/// -/// `progress_cb` receives values in 0.0..=1.0. -/// `cancel_flag` is polled each chunk — set to non-zero to abort. -fn download_archive( - dest: &std::path::Path, - progress_cb: &impl Fn(f32), - cancel_flag: &AtomicI32, -) -> Result<(), Box> { - let url = format!("{}/{}", BASE_URL, ARCHIVE_NAME); - let resp = ureq::get(&url).call()?; - - // Try to get Content-Length for progress reporting - let total_bytes: Option = resp - .header("Content-Length") - .and_then(|v| v.parse().ok()); - - let mut reader = resp.into_reader(); - let mut file = fs::File::create(dest)?; - let mut buf = vec![0u8; 64 * 1024]; // 64 KiB chunks - let mut downloaded: u64 = 0; - - loop { - if cancel_flag.load(Ordering::Relaxed) != 0 { - drop(file); - let _ = fs::remove_file(dest); - return Err("Download cancelled".into()); - } - - let n = reader.read(&mut buf)?; - if n == 0 { - break; - } - file.write_all(&buf[..n])?; - downloaded += n as u64; - - if let Some(total) = total_bytes { - if total > 0 { - progress_cb((downloaded as f32) / (total as f32)); - } - } - } - - file.flush()?; - Ok(()) -} - -/// Download and install the Steam Linux Runtime (sniper). -/// -/// - Skips download if already at the latest BUILD_ID. -/// - Calls `status_cb` with human-readable status strings. -/// - Calls `progress_cb` with 0.0..=1.0 during the download phase. -/// - Polls `cancel_flag`; returns an error if it becomes non-zero. -pub fn download_slr( - progress_cb: impl Fn(f32), - status_cb: impl Fn(&str), - cancel_flag: &AtomicI32, -) -> Result<(), Box> { - // Check for updates - status_cb("Checking Steam Linux Runtime version..."); - let remote_build_id = fetch_remote_build_id()?; - let local_build_id = read_local_build_id(); - - if local_build_id.as_deref() == Some(remote_build_id.as_str()) && is_slr_installed() { - log_info("Steam Linux Runtime is already up to date"); - status_cb("Steam Linux Runtime is already up to date"); - progress_cb(1.0); - return Ok(()); - } - - log_info(&format!( - "Downloading Steam Linux Runtime (BUILD_ID: {})", - remote_build_id - )); - - let install_dir = slr_install_dir(); - fs::create_dir_all(&install_dir)?; - - // Temp file in the install dir - let archive_path = install_dir.join(ARCHIVE_NAME); - - // Download - status_cb("Downloading Steam Linux Runtime (sniper, ~180 MB)..."); - download_archive(&archive_path, &progress_cb, cancel_flag)?; - progress_cb(1.0); - - // SHA256 verification - status_cb("Verifying download..."); - match fetch_expected_sha256() { - Ok(expected) => { - if let Err(e) = verify_sha256(&archive_path, &expected) { - let _ = fs::remove_file(&archive_path); - return Err(format!("Checksum verification failed: {}", e).into()); - } - log_info("SHA256 verification passed"); - } - Err(e) => { - log_warning(&format!( - "Could not fetch SHA256SUMS ({}), skipping verification", - e - )); - } - } - - // Extract - status_cb("Extracting Steam Linux Runtime..."); - let extracted = install_dir.join(EXTRACTED_DIR); - // Remove old copy if present before extracting - if extracted.exists() { - fs::remove_dir_all(&extracted)?; - } - - let status = Command::new("tar") - .args(["xJf", archive_path.to_str().unwrap_or("")]) - .current_dir(&install_dir) - .status()?; - - let _ = fs::remove_file(&archive_path); - - if !status.success() { - return Err(format!("tar extraction failed with status: {}", status).into()); - } - - // Sanity check — run script must now exist - if !slr_run_script().exists() { - return Err(format!( - "Extraction succeeded but run script not found at {:?}", - slr_run_script() - ) - .into()); - } - - // Save BUILD_ID for future update checks - fs::write(local_build_id_path(), &remote_build_id)?; - - log_info("Steam Linux Runtime installed successfully"); - status_cb("Steam Linux Runtime ready"); - Ok(()) -} +//! Steam Linux Runtime (SLR) download and management for Fluorine Manager. +//! +//! Downloads SteamLinuxRuntime_sniper from Valve's official repo and stores it +//! at `~/.local/share/fluorine/steamrt/`. The `run` script is then used to +//! wrap game launches inside the pressure-vessel container, providing +//! GStreamer, 32-bit libs, and an FHS-compliant environment for non-FHS +//! distros (NixOS, etc.). + +use std::error::Error; +use std::fs; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Command; +use std::sync::atomic::{AtomicI32, Ordering}; + +use crate::logging::log_info; + +const BASE_URL: &str = + "https://repo.steampowered.com/steamrt3/images/latest-public-beta"; +const ARCHIVE_NAME: &str = "SteamLinuxRuntime_sniper.tar.xz"; +const EXTRACTED_DIR: &str = "SteamLinuxRuntime_sniper"; + +/// Directory where SLR is installed: `~/.local/share/fluorine/steamrt/` +pub fn slr_install_dir() -> PathBuf { + crate::paths::data_dir().join("steamrt") +} + +/// Path to the `run` script inside the extracted SLR. +pub fn slr_run_script() -> PathBuf { + slr_install_dir().join(EXTRACTED_DIR).join("run") +} + +/// Path where we store the remote BUILD_ID for update checks. +fn local_build_id_path() -> PathBuf { + slr_install_dir().join("BUILD_ID.txt") +} + +/// Returns true if the SLR `run` script is present and executable. +pub fn is_slr_installed() -> bool { + let script = slr_run_script(); + if !script.exists() { + return false; + } + // Verify it's actually executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Ok(meta) = fs::metadata(&script) { + return meta.permissions().mode() & 0o111 != 0; + } + return false; + } + #[cfg(not(unix))] + true +} + +/// Returns the path to the `run` script, or None if SLR is not installed. +pub fn get_slr_run_script() -> Option { + if is_slr_installed() { + Some(slr_run_script()) + } else { + None + } +} + +/// Fetch the remote BUILD_ID as a string. +fn fetch_remote_build_id() -> Result> { + let url = format!("{}/BUILD_ID.txt", BASE_URL); + let resp = ureq::get(&url).call()?; + let mut body = String::new(); + resp.into_reader().read_to_string(&mut body)?; + Ok(body.trim().to_string()) +} + +/// Read the locally cached BUILD_ID, if any. +fn read_local_build_id() -> Option { + fs::read_to_string(local_build_id_path()) + .ok() + .map(|s| s.trim().to_string()) +} + +/// Verify downloaded file size matches the Content-Length from the HTTP response. +/// This is more reliable than SHA256SUMS which can be stale on Valve's CDN. +fn verify_download_size(file: &std::path::Path, expected: u64) -> Result<(), Box> { + let actual = fs::metadata(file)?.len(); + if actual != expected { + return Err(format!( + "Download incomplete: expected {} bytes, got {}", + expected, actual + ) + .into()); + } + Ok(()) +} + +/// Download the archive with streaming progress. +/// +/// `progress_cb` receives values in 0.0..=1.0. +/// `cancel_flag` is polled each chunk — set to non-zero to abort. +/// Returns the Content-Length on success for verification. +fn download_archive( + dest: &std::path::Path, + progress_cb: &impl Fn(f32), + cancel_flag: &AtomicI32, +) -> Result, Box> { + let url = format!("{}/{}", BASE_URL, ARCHIVE_NAME); + let resp = ureq::get(&url).call()?; + + let total_bytes: Option = resp + .header("Content-Length") + .and_then(|v| v.parse().ok()); + + let mut reader = resp.into_reader(); + let mut file = fs::File::create(dest)?; + let mut buf = vec![0u8; 64 * 1024]; // 64 KiB chunks + let mut downloaded: u64 = 0; + + loop { + if cancel_flag.load(Ordering::Relaxed) != 0 { + drop(file); + let _ = fs::remove_file(dest); + return Err("Download cancelled".into()); + } + + let n = reader.read(&mut buf)?; + if n == 0 { + break; + } + file.write_all(&buf[..n])?; + downloaded += n as u64; + + if let Some(total) = total_bytes { + if total > 0 { + progress_cb((downloaded as f32) / (total as f32)); + } + } + } + + file.flush()?; + Ok(total_bytes) +} + +/// Download and install the Steam Linux Runtime (sniper). +/// +/// - Skips download if already at the latest BUILD_ID. +/// - Calls `status_cb` with human-readable status strings. +/// - Calls `progress_cb` with 0.0..=1.0 during the download phase. +/// - Polls `cancel_flag`; returns an error if it becomes non-zero. +pub fn download_slr( + progress_cb: impl Fn(f32), + status_cb: impl Fn(&str), + cancel_flag: &AtomicI32, +) -> Result<(), Box> { + // Check for updates + status_cb("Checking Steam Linux Runtime version..."); + let remote_build_id = fetch_remote_build_id()?; + let local_build_id = read_local_build_id(); + + if local_build_id.as_deref() == Some(remote_build_id.as_str()) && is_slr_installed() { + log_info("Steam Linux Runtime is already up to date"); + status_cb("Steam Linux Runtime is already up to date"); + progress_cb(1.0); + return Ok(()); + } + + log_info(&format!( + "Downloading Steam Linux Runtime (BUILD_ID: {})", + remote_build_id + )); + + let install_dir = slr_install_dir(); + fs::create_dir_all(&install_dir)?; + + // Temp file in the install dir + let archive_path = install_dir.join(ARCHIVE_NAME); + + // Download + status_cb("Downloading Steam Linux Runtime (sniper, ~180 MB)..."); + let content_length = download_archive(&archive_path, &progress_cb, cancel_flag)?; + progress_cb(1.0); + + // Verify download integrity via Content-Length. + // We don't use Valve's SHA256SUMS file because their CDN frequently + // serves a stale copy that doesn't match the current archive. + if let Some(expected_size) = content_length { + status_cb("Verifying download..."); + if let Err(e) = verify_download_size(&archive_path, expected_size) { + let _ = fs::remove_file(&archive_path); + return Err(format!("Download verification failed: {}", e).into()); + } + log_info(&format!( + "Download verified ({} bytes)", + expected_size + )); + } + + // Extract + status_cb("Extracting Steam Linux Runtime..."); + let extracted = install_dir.join(EXTRACTED_DIR); + // Remove old copy if present before extracting + if extracted.exists() { + fs::remove_dir_all(&extracted)?; + } + + let status = Command::new("tar") + .args(["xJf", archive_path.to_str().unwrap_or("")]) + .current_dir(&install_dir) + .status()?; + + let _ = fs::remove_file(&archive_path); + + if !status.success() { + return Err(format!("tar extraction failed with status: {}", status).into()); + } + + // Sanity check — run script must now exist + if !slr_run_script().exists() { + return Err(format!( + "Extraction succeeded but run script not found at {:?}", + slr_run_script() + ) + .into()); + } + + // Save BUILD_ID for future update checks + fs::write(local_build_id_path(), &remote_build_id)?; + + log_info("Steam Linux Runtime installed successfully"); + status_cb("Steam Linux Runtime ready"); + Ok(()) +} diff --git a/libs/nak_ffi/CMakeLists.txt b/libs/nak_ffi/CMakeLists.txt index bdfac9a..72d939f 100644 --- a/libs/nak_ffi/CMakeLists.txt +++ b/libs/nak_ffi/CMakeLists.txt @@ -12,14 +12,17 @@ endif() set(NAK_FFI_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(NAK_FFI_LIB ${NAK_FFI_DIR}/target/${CARGO_BUILD_TYPE}/libnak_ffi.so) +# Glob all Rust sources so any change triggers a cargo rebuild +file(GLOB_RECURSE NAK_FFI_SOURCES ${NAK_FFI_DIR}/src/*.rs) +file(GLOB_RECURSE NAK_SOURCES ${NAK_FFI_DIR}/../nak/src/*.rs) + add_custom_command( OUTPUT ${NAK_FFI_LIB} COMMAND cargo build ${CARGO_BUILD_FLAGS} WORKING_DIRECTORY ${NAK_FFI_DIR} COMMENT "Building NaK FFI library (Rust)" - DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/src/lib.rs - ${NAK_FFI_DIR}/../nak/Cargo.toml ${NAK_FFI_DIR}/../nak/src/lib.rs - ${NAK_FFI_DIR}/../nak/src/icons.rs + DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/../nak/Cargo.toml + ${NAK_FFI_SOURCES} ${NAK_SOURCES} ) add_custom_target(nak_ffi_build DEPENDS ${NAK_FFI_LIB}) -- cgit v1.3.1