diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-01 15:31:43 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-01 15:31:43 -0500 |
| commit | 45aefb63e84fd3c75be7b24264885a53f3fb44b8 (patch) | |
| tree | b702079f16be91eb7e170d608c3de9879976c10e /libs | |
| parent | e92d166ce97a9335f0e16526b40dc5dc456c361c (diff) | |
Fix SLR download failures and CMake rebuild detection
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) <noreply@anthropic.com>
Diffstat (limited to 'libs')
| -rw-r--r-- | libs/nak/src/installers/prefix_setup.rs | 6 | ||||
| -rw-r--r-- | libs/nak/src/runtime_wrap.rs | 47 | ||||
| -rw-r--r-- | libs/nak/src/slr.rs | 490 | ||||
| -rw-r--r-- | libs/nak_ffi/CMakeLists.txt | 9 |
4 files changed, 283 insertions, 269 deletions
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<S: AsRef<OsStr>>( exe: impl AsRef<OsStr>, 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<PathBuf> {
- if is_slr_installed() {
- Some(slr_run_script())
- } else {
- None
- }
-}
-
-/// Fetch the remote BUILD_ID as a string.
-fn fetch_remote_build_id() -> Result<String, Box<dyn Error>> {
- 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<String> {
- 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<String, Box<dyn Error>> {
- 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: "<hash> <filename>" or "<hash> *<filename>"
- 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<dyn Error>> {
- 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<dyn Error>> {
- 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<u64> = 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<dyn Error>> {
- // 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<PathBuf> { + if is_slr_installed() { + Some(slr_run_script()) + } else { + None + } +} + +/// Fetch the remote BUILD_ID as a string. +fn fetch_remote_build_id() -> Result<String, Box<dyn Error>> { + 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<String> { + 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<dyn Error>> { + 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<Option<u64>, Box<dyn Error>> { + let url = format!("{}/{}", BASE_URL, ARCHIVE_NAME); + let resp = ureq::get(&url).call()?; + + let total_bytes: Option<u64> = 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<dyn Error>> { + // 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})
|
