Linux app id: ai.storyteller.solidcraft, with an end-of-life rebase from the old id (#619)

The Flatpak app id, desktop/metainfo/MIME file names, icon name and the window's Wayland app_id / X11 class are now ai.storyteller.solidcraft, the id the ArtCraft launcher expects.

Existing Flatpak installs move over: the Pages repo marks app/ai.storyteller.solvecraft end-of-life with a rebase to the new id, so flatpak update offers the switch and Flatpak carries ~/.var/app along. A new install next to the old one copies settings and autosaves on first launch through a read-only grant on the old data folder. The metainfo declares <replaces>/<provides>, the desktop entry X-Flatpak-RenamedFrom, and AppImages drop the menu entry an older AppImage installed. deb/rpm upgrades remove the old-named files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 05:20:58 -04:00
1 parent bcbfa50eac
commit e67cc0c747
21 files changed
+194 -68

No files matched your search

+7 -4
View File
@@ -35,10 +35,13 @@ pub fn default_dir() -> Option<PathBuf> {
env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?
};
let (dir, legacy) = dirs_under(&root, cfg!(windows) || cfg!(target_os = "macos"));
// Compatibility shim (docs/compatibility.md): autosaves made before the rename are copied
// to the new folder on first launch; the old folder is left as it was.
if let Err(e) = solidcraft_io::adopt_legacy_dir(&dir, &legacy) {
log::warn!("recovery: could not copy {} to {}: {e}", legacy.display(), dir.display());
// Compatibility shim (docs/compatibility.md): autosaves made before the rename (or, in the
// Flatpak sandbox, under the old app id) are copied to the new folder on first launch; the
// old folder is left as it was.
for old in solidcraft_io::legacy_flatpak_path(&dir).into_iter().chain([legacy]) {
if let Err(e) = solidcraft_io::adopt_legacy_dir(&dir, &old) {
log::warn!("recovery: could not copy {} to {}: {e}", old.display(), dir.display());
}
}
Some(dir)
}
+36 -1
View File
@@ -3,7 +3,12 @@
//! the legacy folder is copied over. The legacy folder is never changed or deleted, so an older
//! release installed side by side keeps working.
use std::path::Path;
use std::path::{Path, PathBuf};
/// The Linux app id (Flatpak, desktop entry, AppStream, Wayland `app_id`).
pub const APP_ID: &str = "ai.storyteller.solidcraft";
/// The app id of releases before 0.8 (compatibility shim, docs/compatibility.md).
pub const LEGACY_APP_ID: &str = "ai.storyteller.solvecraft";
/// Most files and nesting copied from a legacy folder (it holds preferences, autosaves and
/// small caches; anything bigger is not ours).
@@ -22,6 +27,25 @@ pub fn adopt_legacy_dir(current: &Path, legacy: &Path) -> std::io::Result<bool>
Ok(true)
}
/// In the Flatpak sandbox, the same `path` under the pre-rename app's data folder: Flatpak keeps
/// each app's XDG folders under `~/.var/app/<app id>`, so after the app id changed, settings and
/// autosaves of earlier Flatpak releases sit under the old id. `None` outside the sandbox or for
/// paths outside our own `~/.var/app` folder. Flatpak moves that folder itself when an install
/// follows the end-of-life rebase; this covers installs made next to the old app, which reach it
/// read-only through the manifest's `--filesystem` grant.
pub fn legacy_flatpak_path(path: &Path) -> Option<PathBuf> {
flatpak_twin(path, &std::env::var_os("FLATPAK_ID")?, Path::new(&std::env::var_os("HOME")?))
}
fn flatpak_twin(path: &Path, flatpak_id: &std::ffi::OsStr, home: &Path) -> Option<PathBuf> {
if flatpak_id != APP_ID {
return None;
}
let apps = home.join(".var").join("app");
let rel = path.strip_prefix(apps.join(APP_ID)).ok()?;
Some(apps.join(LEGACY_APP_ID).join(rel))
}
fn copy_tree(from: &Path, to: &Path, depth: usize, budget: &mut usize) -> std::io::Result<()> {
std::fs::create_dir_all(to)?;
if depth >= MAX_DEPTH {
@@ -75,6 +99,17 @@ mod tests {
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn flatpak_twin_maps_our_sandbox_data_to_the_old_app_id() {
let home = Path::new("/home/u");
let ours = home.join(".var/app/ai.storyteller.solidcraft/data/solidcraft");
let id = std::ffi::OsStr::new(APP_ID);
assert_eq!(flatpak_twin(&ours, id, home), Some(home.join(".var/app/ai.storyteller.solvecraft/data/solidcraft")));
// Outside our sandbox folder, or in another app's sandbox, there is nothing to map.
assert_eq!(flatpak_twin(&home.join(".local/share/solidcraft"), id, home), None);
assert_eq!(flatpak_twin(&ours, std::ffi::OsStr::new("org.example.Other"), home), None);
}
#[test]
fn nothing_to_adopt() {
let d = scratch("none");
+1 -1
View File
@@ -23,7 +23,7 @@ mod threemf;
pub mod vfs;
pub use dxf::{read_dxf, write_dxf};
pub use legacy_dir::adopt_legacy_dir;
pub use legacy_dir::{APP_ID, LEGACY_APP_ID, adopt_legacy_dir, legacy_flatpak_path};
pub use safe_write::{backup_path, write_atomic};
pub use sketch2d::{Geom2, MAX_DRAWING_BYTES};
pub use svg::read_svg;