diff --git a/crates/update/src/folders.rs b/crates/update/src/folders.rs index a144a3ad..0fc6da8c 100644 --- a/crates/update/src/folders.rs +++ b/crates/update/src/folders.rs @@ -40,6 +40,20 @@ pub fn data_local_dir() -> Option { } } +/// The host's per-user data folder, also from inside a Flatpak, where `XDG_DATA_HOME` points into +/// the sandbox (`~/.var/app//data`). Files other programs on the host must find go here. +pub fn host_data_local_dir() -> Option { + pick_host_data_dir(std::path::Path::new("/.flatpak-info").exists(), absolute_env("HOST_XDG_DATA_HOME"), home_dir(), data_local_dir()) +} + +/// [`host_data_local_dir`] with its inputs passed in, so every CI OS can test the Flatpak case. +fn pick_host_data_dir(in_flatpak: bool, host_xdg: Option, home: Option, local: Option) -> Option { + if in_flatpak { + return host_xdg.or_else(|| home.map(|h| h.join(".local").join("share"))); + } + local +} + /// Where per-user caches live on this machine. pub fn cache_dir() -> Option { if cfg!(target_os = "windows") { @@ -62,4 +76,15 @@ mod tests { assert!(cache_dir().is_some_and(|p| p.is_absolute())); } } + + #[test] + fn inside_a_flatpak_the_hosts_data_folder_is_used() { + let home = Some(PathBuf::from("/home/u")); + let sandboxed = Some(PathBuf::from("/home/u/.var/app/ai.storyteller.artcraft-launcher/data")); + let host = Some(PathBuf::from("/srv/u/data")); + assert_eq!(pick_host_data_dir(true, host.clone(), home.clone(), sandboxed.clone()), host); + assert_eq!(pick_host_data_dir(true, None, home.clone(), sandboxed.clone()), Some(PathBuf::from("/home/u/.local/share"))); + assert_eq!(pick_host_data_dir(false, host, home, sandboxed.clone()), sandboxed); + assert_eq!(pick_host_data_dir(true, None, None, None), None); + } } diff --git a/crates/update/src/install.rs b/crates/update/src/install.rs index 5f1f7ff3..2a4ca248 100644 --- a/crates/update/src/install.rs +++ b/crates/update/src/install.rs @@ -1113,13 +1113,7 @@ pub(crate) mod linux { /// Where menu entries and icons go: `$XDG_DATA_HOME`, else `~/.local/share`. Inside the /// launcher's own Flatpak that variable points into the sandbox, so the host's is used. pub fn host_data_dir() -> Option { - if Path::new("/.flatpak-info").exists() { - if let Some(dir) = std::env::var_os("HOST_XDG_DATA_HOME").map(PathBuf::from).filter(|p| p.is_absolute()) { - return Some(dir); - } - return crate::folders::home_dir().map(|h| h.join(".local").join("share")); - } - crate::folders::data_local_dir() + crate::folders::host_data_local_dir() } /// The app's 128 px icon, as bundled in `assets/app-icons`. diff --git a/crates/update/src/pins.rs b/crates/update/src/pins.rs index 03d9a0d2..c0921d0f 100644 --- a/crates/update/src/pins.rs +++ b/crates/update/src/pins.rs @@ -4,6 +4,13 @@ //! own update check reads it, so an app kept on an older release, or on nightlies, is not offered //! the latest stable release every time it starts. Only the launcher writes the file; apps read. //! +//! The file sits in a folder of its own, `ArtCraft/versions` in the host's per-user data folder +//! (`%LOCALAPPDATA%`, `~/Library/Application Support`, `$XDG_DATA_HOME` or `~/.local/share`), +//! never in the launcher's own folder: a Flatpak launcher's own folder is inside its sandbox, and a +//! Flatpak app can be given read access to this one folder alone +//! (`--filesystem=xdg-data/ArtCraft/versions:ro`) without seeing the launcher's settings. +//! Inside a Flatpak, the host's folder is used (see [`crate::folders::host_data_local_dir`]). +//! //! Invariants: //! - A missing, unreadable or damaged file means every app follows stable releases, as before. //! - An entry this version cannot read (written by a newer launcher) counts as managed by the @@ -19,6 +26,10 @@ use serde::{Deserialize, Serialize}; /// app whose home it redirects (tarball installs on Linux), so the app still finds the real file. pub const PATH_ENV: &str = "CRAFT_VERSIONS_FILE"; +/// The line a Flatpak app's manifest adds to its `finish-args` so it can read the file. It names +/// the folder [`Pins::default_path`] uses; the two change together or Flatpak apps lose the file. +pub const FLATPAK_GRANT: &str = "--filesystem=xdg-data/ArtCraft/versions:ro"; + /// Which published build an app follows. A pinned version never changes on its own. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum VersionTrack { @@ -53,14 +64,12 @@ pub enum Choice { } impl Pins { - /// The file in the launcher's per-user data folder, or the one [`PATH_ENV`] names. + /// The shared file (see the module notes), or the one [`PATH_ENV`] names. pub fn default_path() -> Option { if let Some(path) = std::env::var_os(PATH_ENV).map(PathBuf::from).filter(|p| p.is_absolute()) { return Some(path); } - let launcher = &crate::suite::LAUNCHER; - let dir = if cfg!(any(target_os = "macos", target_os = "windows")) { launcher.name } else { launcher.id }; - crate::folders::data_local_dir().map(|d| d.join(dir).join("versions.json")) + crate::folders::host_data_local_dir().map(|d| d.join("ArtCraft").join("versions").join("versions.json")) } /// Reads `path`. A missing or damaged file reads as empty: every app follows stable releases. @@ -211,7 +220,7 @@ mod tests { } #[test] - fn the_default_file_sits_in_the_launchers_data_folder() { + fn the_default_file_sits_in_its_own_folder_outside_the_launchers() { // The environment override is read by the same function; it is not set in tests. if std::env::var_os(PATH_ENV).is_some() { return; @@ -219,8 +228,10 @@ mod tests { if let Some(path) = Pins::default_path() { assert!(path.is_absolute()); assert_eq!(path.file_name().and_then(|n| n.to_str()), Some("versions.json")); - let folder = path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()).unwrap_or_default().to_string(); - assert!(folder == "ArtCraft Launcher" || folder == "artcraft-launcher", "{folder}"); + // The folder is the one the Flatpak grant names, under the host's data folder. + let granted = FLATPAK_GRANT.strip_prefix("--filesystem=xdg-data/").and_then(|g| g.strip_suffix(":ro")).unwrap_or_default(); + let expected = granted.split('/').fold(crate::folders::host_data_local_dir().unwrap_or_default(), |p, part| p.join(part)); + assert_eq!(path.parent(), Some(expected.as_path()), "{FLATPAK_GRANT}"); } } }