mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-13 23:08:16 +00:00
Fallback apps to install from release tarballs when no usable Flatpak spawn is available (#87)
This commit is contained in:
1 parent
98c31aa066
commit
41ded15851
8 files changed
+255
-22
No files matched your search
@@ -142,6 +142,16 @@ Packaging facts the code relies on:
|
||||
normal Dock app that can also sit in the menu bar: `LSUIElement` must never be set.
|
||||
- Linux: the tray uses StatusNotifierItem over D-Bus and notifications use
|
||||
`org.freedesktop.Notifications`, both in pure Rust (no GTK, appindicator or libdbus).
|
||||
- Inside the Flatpak launcher (`process::in_sandbox()`), apps normally install as host Flatpaks
|
||||
and run through `flatpak-spawn --host`. When the host has no usable Flatpak — none installed,
|
||||
or `flatpak-spawn` not allowed — `linux::order_available_kinds` offers the release tarball
|
||||
instead: it is unpacked into `<data>/apps/<app>` (`bin/`, `share/`) and run with `HOME` =
|
||||
`<data>/apps/<app>/home`, so a Craft App's settings stay in the sandbox. An update swaps
|
||||
`bin/` and `share/` and keeps `home/`; removing the app takes those two folders only. Outside
|
||||
a sandbox the tarball is not on offer at all.
|
||||
- `--socket=pulseaudio` in the launcher manifests is for an app running *inside* the sandbox
|
||||
(SoundCraft's ALSA reaches the runtime's PulseAudio plugin through it); it also covers the
|
||||
microphone, which SoundCraft records through.
|
||||
- `build.rs` reads `ARTCRAFT_LAUNCHER_BUILD_SHA` / `ARTCRAFT_LAUNCHER_BUILD_DATE` and, on Windows,
|
||||
embeds `assets/app-icon/artcraft-launcher.ico`, failing the build when
|
||||
`ARTCRAFT_LAUNCHER_REQUIRE_WINRES=1` and it can't.
|
||||
|
||||
@@ -111,6 +111,7 @@ installed by hand (and the launcher finds apps you installed yourself):
|
||||
| macOS | `/Applications/<App>.app` | Copied from the signed, notarized release DMG |
|
||||
| Windows | `C:\Program Files\<App>` | The release MSI, through Windows Installer (with Start menu entry and Apps & features entry) |
|
||||
| Linux | `/usr/bin`, `/usr/share/applications` | The release `.deb` (apt) or `.rpm` (dnf, zypper), with your system's password prompt. Without those, a Flatpak, or an AppImage in `~/Applications` |
|
||||
| Linux, inside the launcher's Flatpak, with no Flatpak on the host | `~/.var/app/ai.storyteller.artcraft-launcher/data/artcraft-launcher/apps/<app>` | The release tarball, unpacked into the launcher's own folder and run inside the same sandbox, with its settings in a `home/` folder beside it. Nothing is installed on the host, and each app's configuration stays in the sandbox |
|
||||
| FreeBSD | `/usr/local` | The release tree, with a password prompt (polkit); `~/.local` when polkit isn't installed |
|
||||
|
||||
ArtCraft Launcher keeps itself current too: when a new version is out, **Update and restart**
|
||||
@@ -152,7 +153,7 @@ Download the build for your computer from the [latest release](https://github.co
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.AppImage` | Runs on almost any distro without installing; `chmod +x` and run. The `.AppImage.zsync` beside it lets AppImageUpdate fetch only what changed |
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.deb` | Debian, Ubuntu, Mint, Pop!_OS: `sudo apt install ./artcraft-launcher-…deb` |
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.rpm` | Fedora, openSUSE, RHEL: `sudo dnf install ./artcraft-launcher-…rpm` |
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.flatpak` | Flatpak bundle: `flatpak install --user artcraft-launcher-…flatpak`. Inside the sandbox the launcher can download apps but has limited power to install them on the host; prefer the AppImage, .deb or .rpm |
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.flatpak` | Flatpak bundle: `flatpak install --user artcraft-launcher-…flatpak`. Inside the sandbox the launcher installs apps as Flatpaks on the host through `flatpak-spawn --host`; where the host has no Flatpak it falls back to each app's release tarball, unpacked and run inside the sandbox with the app's settings kept in it. Either way it can only reach the host as far as that permission allows; the AppImage, .deb and .rpm builds are the recommended way to run the launcher on Linux |
|
||||
| `artcraft-launcher-<ver>-linux-{x86_64,aarch64}.tar.gz` | Plain tree (`bin/`, `share/`) for everyone else and for scripts |
|
||||
|
||||
RISC-V (riscv64): `artcraft-launcher-<ver>-linux-riscv64.{deb,rpm,tar.gz}`, built against Ubuntu 24.04 (needs glibc 2.39+).
|
||||
|
||||
@@ -339,7 +339,7 @@ impl NativeBackend {
|
||||
shared.emit(Event::Progress { app: app.id.to_string(), phase: JobPhase::Removing });
|
||||
let mut phase = |phase| shared.emit(Event::Progress { app: app.id.to_string(), phase });
|
||||
match &existing {
|
||||
Some(inst) => install::uninstall(app, inst, record.as_ref(), &stop, &mut phase).map(|()| {
|
||||
Some(inst) => install::uninstall(app, inst, record.as_ref(), &paths, &stop, &mut phase).map(|()| {
|
||||
let mut m = shared.manifest.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if m.apps.remove(app.id).is_some() {
|
||||
let _ = m.save(&paths.manifest());
|
||||
@@ -889,7 +889,7 @@ impl Backend for NativeBackend {
|
||||
Command::Launch { app } => {
|
||||
let Some(def) = CraftApp::by_id(&app) else { return };
|
||||
let result = match self.installed(&app) {
|
||||
Some(inst) => install::launch(def, &inst),
|
||||
Some(inst) => install::launch(def, &inst, &self.paths),
|
||||
None => Err(format!("{} isn't installed", def.name)),
|
||||
};
|
||||
if let Err(e) = result {
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
//! `~/.local/share` so it shows up like any other app. A Flatpak or AppImage launcher prefers
|
||||
//! its own format.
|
||||
//! - FreeBSD: the release's `/usr/local` tree, with a polkit prompt (or `~/.local` without one).
|
||||
//! - Linux, inside the Flatpak launcher with no usable host Flatpak (none installed, or
|
||||
//! `flatpak-spawn` not allowed): the release tarball, unpacked into the launcher's own folder
|
||||
//! (`<data>/apps/<app>`) and run inside the same sandbox with `HOME` pointed at a `home/`
|
||||
//! folder beside it, so the app's settings stay in the sandbox too.
|
||||
//!
|
||||
//! Every download is checked against its published SHA-256 before anything is unpacked, a
|
||||
//! running app is never replaced, and a new copy is fully in place before the old one goes.
|
||||
@@ -74,6 +78,8 @@ pub fn install(
|
||||
}),
|
||||
#[cfg(target_os = "freebsd")]
|
||||
PackageKind::TarGz => freebsd::install_tree(app, &file, job.paths).map(|(bin, files, root)| Some(record(&version, kind, bin, files, root))),
|
||||
#[cfg(target_os = "linux")]
|
||||
PackageKind::TarGz => linux::install_tree(app, &file, job.paths).map(|path| Some(record(&version, kind, path, Vec::new(), false))),
|
||||
other => Err(format!("{} isn't supported on this system", other.label())),
|
||||
};
|
||||
match &result {
|
||||
@@ -205,8 +211,15 @@ pub fn managed_elsewhere(app: &CraftApp, installed: &Installed, uninstall: bool)
|
||||
|
||||
/// Removes an installed copy. `phase` hears when the removal has to wait for another install;
|
||||
/// `stop` cancels it while it waits.
|
||||
pub fn uninstall(app: &CraftApp, installed: &Installed, record: Option<&Record>, stop: &Stop, phase: &mut dyn FnMut(JobPhase)) -> Result<(), String> {
|
||||
let _ = (record, stop, &phase);
|
||||
pub fn uninstall(
|
||||
app: &CraftApp,
|
||||
installed: &Installed,
|
||||
record: Option<&Record>,
|
||||
paths: &Paths,
|
||||
stop: &Stop,
|
||||
phase: &mut dyn FnMut(JobPhase),
|
||||
) -> Result<(), String> {
|
||||
let _ = (record, stop, &phase, paths);
|
||||
let path = PathBuf::from(&installed.location);
|
||||
if let Some(msg) = managed_elsewhere(app, installed, true) {
|
||||
return Err(msg);
|
||||
@@ -231,7 +244,13 @@ pub fn uninstall(app: &CraftApp, installed: &Installed, record: Option<&Record>,
|
||||
None => Err("the launcher has no record of these files".into()),
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
InstallSource::AppImage | InstallSource::Launcher => remove_appimage(app, &path, record),
|
||||
InstallSource::AppImage => remove_appimage(app, &path, record),
|
||||
// A tarball the launcher unpacked into its own folder: take the tree out and leave the
|
||||
// app's settings (`home/`) alone, so a reinstall finds its projects where they were.
|
||||
#[cfg(target_os = "linux")]
|
||||
InstallSource::Launcher if record.is_some_and(|r| r.kind == PackageKind::TarGz) => linux::remove_tree(app, paths),
|
||||
#[cfg(target_os = "linux")]
|
||||
InstallSource::Launcher => remove_appimage(app, &path, record),
|
||||
other => Err(format!("{} can't be removed from here", other.label())),
|
||||
}
|
||||
}
|
||||
@@ -256,7 +275,8 @@ fn remove_appimage(app: &CraftApp, path: &Path, record: Option<&Record>) -> Resu
|
||||
}
|
||||
|
||||
/// Starts an installed app.
|
||||
pub fn launch(app: &CraftApp, installed: &Installed) -> Result<(), String> {
|
||||
pub fn launch(app: &CraftApp, installed: &Installed, paths: &Paths) -> Result<(), String> {
|
||||
let _ = paths;
|
||||
let path = Path::new(&installed.location);
|
||||
match installed.source {
|
||||
InstallSource::Flatpak => {
|
||||
@@ -265,6 +285,23 @@ pub fn launch(app: &CraftApp, installed: &Installed) -> Result<(), String> {
|
||||
process::spawn_detached(cmd)
|
||||
}
|
||||
InstallSource::MacApp => process::open(path.as_os_str()),
|
||||
// A tarball the launcher unpacked into its own folder: run it here, against its own
|
||||
// home, rather than on the host through `flatpak-spawn` (which is what `launch_binary`
|
||||
// would do inside the sandbox). Its settings then land beside it instead of in the
|
||||
// launcher's or the host's home folder.
|
||||
#[cfg(target_os = "linux")]
|
||||
InstallSource::Launcher if path.starts_with(paths.apps()) => {
|
||||
let home = paths.app_home(app.id);
|
||||
prepare_home(&home);
|
||||
let mut cmd = process::command(path);
|
||||
for (name, value) in home_env(&home) {
|
||||
cmd.env(name, value);
|
||||
}
|
||||
if let Some(dir) = path.parent() {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
process::spawn_detached(cmd)
|
||||
}
|
||||
_ if cfg!(target_os = "macos") && path.extension().is_some_and(|e| e == "app") => process::open(path.as_os_str()),
|
||||
_ => {
|
||||
if !path.exists() {
|
||||
@@ -276,6 +313,44 @@ pub fn launch(app: &CraftApp, installed: &Installed) -> Result<(), String> {
|
||||
.map_err(|e| format!("Couldn't open {}: {e}", app.name))
|
||||
}
|
||||
|
||||
/// The environment a Craft App unpacked by the launcher runs with: its own `HOME` and the XDG
|
||||
/// base directories inside it. Apps read their settings from these, so everything they keep
|
||||
/// (settings, projects, caches) stays in that one folder.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn home_env(home: &Path) -> [(&'static str, PathBuf); 5] {
|
||||
[
|
||||
("HOME", home.to_path_buf()),
|
||||
("XDG_CONFIG_HOME", home.join(".config")),
|
||||
("XDG_DATA_HOME", home.join(".local/share")),
|
||||
("XDG_CACHE_HOME", home.join(".cache")),
|
||||
("XDG_STATE_HOME", home.join(".local/state")),
|
||||
]
|
||||
}
|
||||
|
||||
/// Creates the folders an app unpacked by the launcher expects to find, and links the host's
|
||||
/// `~/.vst3` and `~/.clap` (SoundCraft's plugin folders) in when they exist, so an app running
|
||||
/// inside the sandbox still sees the plugins installed on the host. Every failure here is
|
||||
/// ignored: the app is worth starting even if its home is not quite ready.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn prepare_home(home: &Path) {
|
||||
for (_, dir) in home_env(home).iter().skip(1) {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let Some(real) = dirs::home_dir() else { return };
|
||||
if real == home {
|
||||
return;
|
||||
}
|
||||
for name in [".vst3", ".clap"] {
|
||||
let link = home.join(name);
|
||||
if link.symlink_metadata().is_ok() {
|
||||
continue;
|
||||
}
|
||||
if real.join(name).is_dir() {
|
||||
let _ = std::os::unix::fs::symlink(real.join(name), link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves `staged` into place at `dest`, keeping the old copy until the new one is there.
|
||||
#[allow(dead_code)]
|
||||
fn swap_into_place(staged: &Path, dest: &Path) -> Result<(), String> {
|
||||
@@ -432,7 +507,10 @@ pub(crate) mod linux {
|
||||
/// What this system can install, ordered with the best default first.
|
||||
///
|
||||
/// The default package format aligns with how the user installed the launcher itself:
|
||||
/// - Running as a Flatpak: prefers Flatpak (per-user, desktop integration).
|
||||
/// - Running as a Flatpak: prefers Flatpak (per-user, desktop integration). When the host
|
||||
/// has no usable Flatpak (none installed, or `flatpak-spawn` refused) it prefers the
|
||||
/// release tarball instead, which the launcher unpacks and runs inside the sandbox; an
|
||||
/// AppImage cannot mount there and a distro package needs root, so both are dead ends.
|
||||
/// - Running as an AppImage: prefers AppImage.
|
||||
/// - Otherwise (system install): prefers the distro's own package format (`.deb` on the
|
||||
/// Debian family, `.rpm` on the Fedora, Red Hat and SUSE families, read from `os-release`
|
||||
@@ -459,6 +537,8 @@ pub(crate) mod linux {
|
||||
let mut kinds = Vec::new();
|
||||
if is_flatpak && flatpak_available {
|
||||
kinds.push(PackageKind::Flatpak);
|
||||
} else if is_flatpak {
|
||||
kinds.push(PackageKind::TarGz);
|
||||
} else if is_appimage {
|
||||
kinds.push(PackageKind::AppImage);
|
||||
}
|
||||
@@ -473,6 +553,10 @@ pub(crate) mod linux {
|
||||
if flatpak_available && !kinds.contains(&PackageKind::Flatpak) {
|
||||
kinds.push(PackageKind::Flatpak);
|
||||
}
|
||||
// The tarball runs inside the sandbox, so it is only ever offered there.
|
||||
if is_flatpak && !kinds.contains(&PackageKind::TarGz) {
|
||||
kinds.push(PackageKind::TarGz);
|
||||
}
|
||||
if !kinds.contains(&PackageKind::AppImage) {
|
||||
kinds.push(PackageKind::AppImage);
|
||||
}
|
||||
@@ -584,6 +668,49 @@ pub(crate) mod linux {
|
||||
Ok((dest, removed))
|
||||
}
|
||||
|
||||
/// Unpacks a release tarball (`bin/<app>`, `share/…`) into `<data>/apps/<app>`, a folder
|
||||
/// only the launcher owns, and returns the app's binary. This is the fallback for the
|
||||
/// Flatpak launcher when the host has no usable Flatpak: the app then runs inside the
|
||||
/// sandbox instead of on the host. `bin` and `share` are swapped in one at a time, so an
|
||||
/// update leaves the app's own `home/` beside them untouched.
|
||||
pub fn install_tree(app: &CraftApp, file: &Path, paths: &Paths) -> Result<PathBuf, String> {
|
||||
let staged = paths.cache.join(format!("{}.new", app.id));
|
||||
let result = unpack_tree(app, file, &staged, paths);
|
||||
let _ = std::fs::remove_dir_all(&staged);
|
||||
result
|
||||
}
|
||||
|
||||
fn unpack_tree(app: &CraftApp, file: &Path, staged: &Path, paths: &Paths) -> Result<PathBuf, String> {
|
||||
let tree = tree::unpack(app, file, staged)?;
|
||||
let root = paths.apps().join(app.id);
|
||||
std::fs::create_dir_all(&root).map_err(|e| format!("couldn't create {} ({e})", root.display()))?;
|
||||
for dir in ["bin", "share"] {
|
||||
if tree.join(dir).is_dir() {
|
||||
swap_into_place(&tree.join(dir), &root.join(dir))?;
|
||||
}
|
||||
}
|
||||
let bin = root.join("bin").join(app.id);
|
||||
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755))
|
||||
.map_err(|e| format!("couldn't make {} runnable ({e})", bin.display()))?;
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
/// Removes a tree the launcher unpacked in [`install_tree`], keeping the app's `home/`: the
|
||||
/// settings and projects in it are the user's, not the installer's. Only the two folders the
|
||||
/// launcher put there are removed, and only inside the launcher's own apps folder.
|
||||
pub fn remove_tree(app: &CraftApp, paths: &Paths) -> Result<(), String> {
|
||||
let root = paths.apps().join(app.id);
|
||||
let mut removed = false;
|
||||
for dir in ["bin", "share"] {
|
||||
let path = root.join(dir);
|
||||
if path.starts_with(paths.apps()) && path.is_dir() {
|
||||
std::fs::remove_dir_all(&path).map_err(|e| format!("couldn't remove {} ({e})", path.display()))?;
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
if removed { Ok(()) } else { Err(format!("{} isn't installed in this folder", app.name)) }
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -1193,6 +1320,57 @@ mod tests {
|
||||
assert!(tree::unpack(app, &bad, &dir.path().join("staging")).is_err());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn a_tarball_install_lives_in_the_launchers_folder_and_keeps_the_apps_home() {
|
||||
let app = CraftApp::by_id("wordcraft").unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let paths = Paths::under(dir.path());
|
||||
let file = dir.path().join("wordcraft-0.3.0-linux-x86_64.tar.gz");
|
||||
tarball(
|
||||
&file,
|
||||
&[
|
||||
"wordcraft-0.3.0-linux-x86_64/bin/wordcraft",
|
||||
"wordcraft-0.3.0-linux-x86_64/bin/wordcraft-cli",
|
||||
"wordcraft-0.3.0-linux-x86_64/share/applications/ai.storyteller.wordcraft.desktop",
|
||||
],
|
||||
);
|
||||
|
||||
let bin = linux::install_tree(app, &file, &paths).unwrap();
|
||||
let root = paths.apps().join("wordcraft");
|
||||
assert_eq!(bin, root.join("bin").join("wordcraft"));
|
||||
assert!(bin.is_file(), "the app's binary wasn't unpacked");
|
||||
assert!(root.join("share/applications/ai.storyteller.wordcraft.desktop").is_file(), "share/ wasn't unpacked");
|
||||
assert!(!paths.cache.join("wordcraft.new").exists(), "the staging folder was left behind");
|
||||
|
||||
// The app's own home sits beside the tree, and an update keeps it.
|
||||
let home = paths.app_home(app.id);
|
||||
std::fs::create_dir_all(home.join(".config/wordcraft")).unwrap();
|
||||
std::fs::write(home.join(".config/wordcraft/settings.toml"), "keep me").unwrap();
|
||||
linux::install_tree(app, &file, &paths).unwrap();
|
||||
assert_eq!(std::fs::read_to_string(home.join(".config/wordcraft/settings.toml")).unwrap(), "keep me");
|
||||
|
||||
// Removing the install takes the tree and nothing else.
|
||||
linux::remove_tree(app, &paths).unwrap();
|
||||
assert!(!bin.exists() && !root.join("share").exists());
|
||||
assert!(home.join(".config/wordcraft/settings.toml").is_file(), "the app's settings were removed with it");
|
||||
assert!(linux::remove_tree(app, &paths).is_err(), "a second removal says there is nothing there");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn an_app_sees_its_own_home_and_the_xdg_folders_inside_it() {
|
||||
let home = PathBuf::from("/data/apps/photocraft/home");
|
||||
let env = home_env(&home);
|
||||
assert_eq!(env[0], ("HOME", home.clone()));
|
||||
for (name, dir) in &env[1..] {
|
||||
assert!(dir.starts_with(&home), "{name}: {}", dir.display());
|
||||
}
|
||||
for wanted in ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME"] {
|
||||
assert!(env.iter().any(|(name, _)| *name == wanted), "{wanted} isn't set");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn several_debs_fit_one_apt_call_and_share_its_result() {
|
||||
@@ -1398,6 +1576,18 @@ mod tests {
|
||||
let kinds = order_available_kinds(false, false, true, false, false);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::Flatpak));
|
||||
assert_eq!(kinds.get(1), Some(&PackageKind::AppImage));
|
||||
|
||||
// The tarball runs inside the sandbox, so it is only offered there: with a usable host
|
||||
// Flatpak it stays an alternative, without one it is the default (an AppImage cannot
|
||||
// mount in a sandbox, and a distro package needs root).
|
||||
let kinds = order_available_kinds(true, false, true, false, false);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::Flatpak));
|
||||
assert_eq!(kinds.get(1), Some(&PackageKind::TarGz));
|
||||
let kinds = order_available_kinds(true, false, false, false, false);
|
||||
assert_eq!(kinds, [PackageKind::TarGz, PackageKind::AppImage]);
|
||||
for (appimage, flatpak) in [(true, true), (false, false), (false, true)] {
|
||||
assert!(!order_available_kinds(false, appimage, flatpak, true, true).contains(&PackageKind::TarGz));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -59,6 +59,15 @@ impl Paths {
|
||||
pub fn apps(&self) -> PathBuf {
|
||||
self.data.join("apps")
|
||||
}
|
||||
|
||||
/// The folder an app unpacked in [`Self::apps`] sees as its home: `<data>/apps/<app>/home`.
|
||||
/// A Craft App keeps its settings in the XDG folders (`~/.config`, `~/.local/share`,
|
||||
/// `~/.cache`), so pointing `HOME` and those variables here keeps every app's configuration
|
||||
/// beside it, inside the folder the launcher owns.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn app_home(&self, id: &str) -> PathBuf {
|
||||
self.apps().join(id).join("home")
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `bytes` to `path` via a temporary file and a rename, so a crash never leaves half a file.
|
||||
|
||||
@@ -258,6 +258,11 @@ impl LauncherUi {
|
||||
for k in [PackageKind::Deb, PackageKind::Rpm, PackageKind::Flatpak, PackageKind::AppImage] {
|
||||
ui.selectable_value(&mut choice, Some(k), crate::i18n::t(k.label()));
|
||||
}
|
||||
// The tarball is unpacked and run inside the launcher's own sandbox, so it
|
||||
// is only ever on offer there (as the fallback when the host has no Flatpak).
|
||||
if is_flatpak {
|
||||
ui.selectable_value(&mut choice, Some(PackageKind::TarGz), crate::i18n::t(PackageKind::TarGz.label()));
|
||||
}
|
||||
});
|
||||
});
|
||||
self.settings.preferred_kinds = choice.into_iter().collect();
|
||||
|
||||
@@ -24,18 +24,27 @@ finish-args:
|
||||
- --share=ipc
|
||||
# GPU access for egui's wgpu renderer (Vulkan, or GL as a fallback).
|
||||
- --device=dri
|
||||
# SoundCraft's audio, for the case where it runs inside this sandbox rather than on the host
|
||||
# (the launcher falls back to its release tarball when the host has no usable Flatpak): its
|
||||
# ALSA goes through the runtime's PulseAudio plugin to this socket, which PipeWire's pulse
|
||||
# server provides on modern desktops. Note this socket also covers the microphone (SoundCraft
|
||||
# records, so it needs it); it grants recording, not raw hardware access.
|
||||
- --socket=pulseaudio
|
||||
# The launcher reads the Craft apps' GitHub releases and downloads them.
|
||||
- --share=network
|
||||
# Tray icon (StatusNotifierItem) and desktop notifications, both over the session bus.
|
||||
- --talk-name=org.kde.StatusNotifierWatcher
|
||||
- --talk-name=org.freedesktop.Notifications
|
||||
# LIMITATION: a launcher inside a Flatpak sandbox cannot install software on the host by
|
||||
# itself. It may run `flatpak-spawn --host` (org.freedesktop.Flatpak) to start installed apps
|
||||
# and host installers, and it gets the home directory so downloaded AppImages and tarballs land
|
||||
# where the host can run them (~/Applications, ~/.local). Distro packages (.deb/.rpm) and
|
||||
# anything needing root still have to be installed by the user. org.freedesktop.Flatpak is a
|
||||
# sandbox escape that Flathub reviews case by case; the AppImage, .deb and .rpm builds are the
|
||||
# recommended way to run the launcher on Linux.
|
||||
# Apps normally install as Flatpaks and run on the host: `flatpak-spawn --host` (below) starts
|
||||
# them, and the home directory is granted so files opened by path work. When the host has no
|
||||
# usable Flatpak the launcher falls back to the release tarball, which it unpacks into its own
|
||||
# folder (~/.var/app/ai.storyteller.artcraft-launcher/data/artcraft-launcher/apps/<app>) and
|
||||
# runs in the same sandbox with HOME pointed at a home/ folder beside it, so each app's
|
||||
# settings stay in the sandbox too.
|
||||
# LIMITATION: installing software on the host still needs `flatpak-spawn --host`. Distro
|
||||
# packages (.deb/.rpm) or anything needing root still have to be installed by the user.
|
||||
# org.freedesktop.Flatpak is a sandbox escape that Flathub reviews case by case; the AppImage,
|
||||
# .deb and .rpm builds are the recommended way to run the launcher on Linux.
|
||||
- --talk-name=org.freedesktop.Flatpak
|
||||
- --filesystem=home
|
||||
modules:
|
||||
|
||||
@@ -26,18 +26,27 @@ finish-args:
|
||||
- --share=ipc
|
||||
# GPU access for egui's wgpu renderer (Vulkan, or GL as a fallback).
|
||||
- --device=dri
|
||||
# SoundCraft's audio, for the case where it runs inside this sandbox rather than on the host
|
||||
# (the launcher falls back to its release tarball when the host has no usable Flatpak): its
|
||||
# ALSA goes through the runtime's PulseAudio plugin to this socket, which PipeWire's pulse
|
||||
# server provides on modern desktops. Note this socket also covers the microphone (SoundCraft
|
||||
# records, so it needs it); it grants recording, not raw hardware access.
|
||||
- --socket=pulseaudio
|
||||
# The launcher reads the Craft apps' GitHub releases and downloads them.
|
||||
- --share=network
|
||||
# Tray icon (StatusNotifierItem) and desktop notifications, both over the session bus.
|
||||
- --talk-name=org.kde.StatusNotifierWatcher
|
||||
- --talk-name=org.freedesktop.Notifications
|
||||
# LIMITATION: a launcher inside a Flatpak sandbox cannot install software on the host by
|
||||
# itself. It may run `flatpak-spawn --host` (org.freedesktop.Flatpak) to start installed apps
|
||||
# and host installers, and it gets the home directory so downloaded AppImages and tarballs land
|
||||
# where the host can run them (~/Applications, ~/.local). Distro packages (.deb/.rpm) and
|
||||
# anything needing root still have to be installed by the user. org.freedesktop.Flatpak is a
|
||||
# sandbox escape that Flathub reviews case by case; the AppImage, .deb and .rpm builds are the
|
||||
# recommended way to run the launcher on Linux.
|
||||
# Apps normally install as Flatpaks and run on the host: `flatpak-spawn --host` (below) starts
|
||||
# them, and the home directory is granted so files opened by path work. When the host has no
|
||||
# usable Flatpak the launcher falls back to the release tarball, which it unpacks into its own
|
||||
# folder (~/.var/app/ai.storyteller.artcraft-launcher/data/artcraft-launcher/apps/<app>) and
|
||||
# runs in the same sandbox with HOME pointed at a home/ folder beside it, so each app's
|
||||
# settings stay in the sandbox too.
|
||||
# LIMITATION: installing software on the host still needs `flatpak-spawn --host`. Distro
|
||||
# packages (.deb/.rpm) or anything needing root still have to be installed by the user.
|
||||
# org.freedesktop.Flatpak is a sandbox escape that Flathub reviews case by case; the AppImage,
|
||||
# .deb and .rpm builds are the recommended way to run the launcher on Linux.
|
||||
- --talk-name=org.freedesktop.Flatpak
|
||||
- --filesystem=home
|
||||
build-options:
|
||||
|
||||
Reference in new issue
Block a user