mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
craft-update: the version-choice file sits in a host folder Flatpaks can read
A Flatpak launcher wrote versions.json inside its own sandbox, and no Flatpak app could read the launcher's folder, so pins never reached Flatpak apps, nor host apps started from their menu entry. The file now sits in its own folder, ArtCraft/versions under the host's per-user data folder, found from inside a Flatpak too (HOST_XDG_DATA_HOME, else ~/.local/share). A Flatpak app reads it with one read-only grant for that folder alone (pins::FLATPAK_GRANT). folders::host_data_local_dir replaces install's private copy of the same rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ada678488
commit
9817c81fd2
3 files changed
+44
-14
No files matched your search
@@ -40,6 +40,20 @@ pub fn data_local_dir() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The host's per-user data folder, also from inside a Flatpak, where `XDG_DATA_HOME` points into
|
||||
/// the sandbox (`~/.var/app/<id>/data`). Files other programs on the host must find go here.
|
||||
pub fn host_data_local_dir() -> Option<PathBuf> {
|
||||
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<PathBuf>, home: Option<PathBuf>, local: Option<PathBuf>) -> Option<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<PathBuf> {
|
||||
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`.
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user