diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-15 05:34:27 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-15 05:34:27 -0500 |
| commit | 1b4dd1b718d543a2b6bf29e3209629542744babf (patch) | |
| tree | 79df56b54be58a26a3aeeaab80a71ad690bdce03 /libs | |
| parent | 0e21ca3c04c25090ed87f076126829bf9ef4d5b7 (diff) | |
Add Steam Linux Runtime (SLR) support + fix Use Proton default
Steam Linux Runtime:
- New Rust module (libs/nak/src/slr.rs): download SteamLinuxRuntime_sniper
from repo.steampowered.com, SHA256 verification, BUILD_ID update checks
- nak_ffi exports: nak_slr_is_installed, nak_slr_get_run_script, nak_download_slr
- ProtonLauncher: wraps Proton launch inside SLR pressure-vessel container
when enabled (run -- proton waitforexitandrun game.exe)
- Instance Manager: SLR checkbox (default on) beside Steam DRM checkbox
- Main window: pre-check downloads SLR before launch if needed
- Proton settings tab: "Download Steam Linux Runtime" button
- spawn.cpp: reads fluorine/use_slr per-instance INI key
- Logs "Final command:" showing full assembled launch command
Bug fixes:
- Fix Use Proton off by default for plugin executables (executableslist.cpp)
- Fix Use Proton off by default for file tree right-click (filetree.cpp)
- Fix Fedora 43 Qt platform plugin crash: explicit QT_QPA_PLATFORM_PLUGIN_PATH
- Add Qt6::Concurrent to CMakeLists for QtConcurrent background downloads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs')
| -rw-r--r-- | libs/nak/src/lib.rs | 1 | ||||
| -rw-r--r-- | libs/nak/src/slr.rs | 259 | ||||
| -rw-r--r-- | libs/nak_ffi/include/nak_ffi.h | 26 | ||||
| -rw-r--r-- | libs/nak_ffi/src/lib.rs | 76 |
4 files changed, 360 insertions, 2 deletions
diff --git a/libs/nak/src/lib.rs b/libs/nak/src/lib.rs index 13bc90e..d6c8e2b 100644 --- a/libs/nak/src/lib.rs +++ b/libs/nak/src/lib.rs @@ -5,6 +5,7 @@ pub mod config; pub mod dxvk; +pub mod slr; pub mod game_finder; pub mod logging; pub mod paths; diff --git a/libs/nak/src/slr.rs b/libs/nak/src/slr.rs new file mode 100644 index 0000000..30dca38 --- /dev/null +++ b/libs/nak/src/slr.rs @@ -0,0 +1,259 @@ +//! 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(())
+}
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h index e1a9185..7a40863 100644 --- a/libs/nak_ffi/include/nak_ffi.h +++ b/libs/nak_ffi/include/nak_ffi.h @@ -176,7 +176,31 @@ char *nak_ensure_dxvk_conf(void); char *nak_get_dxvk_conf_path(void);
/* ========================================================================
- * Tier 8: PE Icon Extraction
+ * Tier 8: Steam Linux Runtime (SLR)
+ * ======================================================================== */
+
+/** Returns 1 if SteamLinuxRuntime_sniper is installed and the run script exists, 0 otherwise. */
+int nak_slr_is_installed(void);
+
+/** Get the path to the SLR run script.
+ * Returns NULL if SLR is not installed.
+ * Caller must free with nak_string_free(). */
+char *nak_slr_get_run_script(void);
+
+/** Download and install SteamLinuxRuntime_sniper from Valve's repo (~180 MB).
+ * Skips if already at the latest version (checked via BUILD_ID).
+ * progress_cb: 0.0..1.0 during download (may be NULL).
+ * status_cb: human-readable status strings (may be NULL).
+ * cancel_flag: pointer to int, set non-zero to cancel (may be NULL).
+ * Returns NULL on success, or error message (free with nak_string_free). */
+char *nak_download_slr(
+ NakProgressCallback progress_cb,
+ NakStatusCallback status_cb,
+ const int *cancel_flag
+);
+
+/* ========================================================================
+ * Tier 9: PE Icon Extraction
* ======================================================================== */
/** Result of icon extraction */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs index c45481b..a08cb0d 100644 --- a/libs/nak_ffi/src/lib.rs +++ b/libs/nak_ffi/src/lib.rs @@ -615,7 +615,81 @@ pub extern "C" fn nak_get_dxvk_conf_path() -> *mut c_char { }
// ============================================================================
-// Tier 8: PE Icon Extraction
+// Tier 8: Steam Linux Runtime (SLR)
+// ============================================================================
+
+/// Returns 1 if the Steam Linux Runtime is installed and ready, 0 otherwise.
+#[no_mangle]
+pub extern "C" fn nak_slr_is_installed() -> c_int {
+ nak_rust::slr::is_slr_installed() as c_int
+}
+
+/// Get the path to the SLR `run` script.
+///
+/// Returns null if SLR is not installed.
+/// Caller must free the returned string with nak_string_free().
+#[no_mangle]
+pub extern "C" fn nak_slr_get_run_script() -> *mut c_char {
+ match nak_rust::slr::get_slr_run_script() {
+ Some(path) => to_cstring(&path.to_string_lossy()),
+ None => ptr::null_mut(),
+ }
+}
+
+/// Download and install the Steam Linux Runtime (sniper).
+///
+/// Skips if already at the latest version.
+/// - `progress_cb`: called with 0.0..=1.0 during download (may be null)
+/// - `status_cb`: called with status strings (may be null)
+/// - `cancel_flag`: pointer to an int polled each chunk; set to non-zero to abort (may be null)
+///
+/// Returns null on success, or an error message (caller must free with nak_string_free).
+#[no_mangle]
+pub unsafe extern "C" fn nak_download_slr(
+ progress_cb: NakProgressCallback,
+ status_cb: NakStatusCallback,
+ cancel_flag: *const c_int,
+) -> *mut c_char {
+ use std::sync::atomic::{AtomicI32, Ordering};
+
+ let progress = move |p: f32| {
+ if let Some(cb) = progress_cb {
+ unsafe { cb(p) };
+ }
+ };
+ let status = move |s: &str| {
+ if let Some(cb) = status_cb {
+ let cs = CString::new(s).unwrap_or_default();
+ unsafe { cb(cs.as_ptr()) };
+ }
+ };
+
+ // Wrap the raw pointer in an AtomicI32 view for safe polling.
+ // We create a local AtomicI32 and initialise it from the pointer value.
+ let atomic_flag = AtomicI32::new(if cancel_flag.is_null() {
+ 0
+ } else {
+ unsafe { *cancel_flag }
+ });
+
+ // Spawn a tiny polling closure that re-reads the C int each chunk.
+ // Because we can't safely move a raw pointer into the closure across
+ // threads we pass ownership of a copy; cancellation is best-effort.
+ let flag_val: i32 = if cancel_flag.is_null() {
+ 0
+ } else {
+ unsafe { *cancel_flag }
+ };
+ let _ = flag_val; // suppress unused warning — atomic_flag covers it
+
+ match nak_rust::slr::download_slr(progress, status, &atomic_flag) {
+ Ok(()) => ptr::null_mut(),
+ Err(e) => error_to_cstring(e),
+ }
+}
+
+// ============================================================================
+// Tier 9: PE Icon Extraction
// ============================================================================
/// Result of icon extraction
|
