mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-14 14:26:48 -04:00
Linux: prefer the launcher's own format (Flatpak, AppImage) and run host apps from the Flatpak sandbox (#89)
* linux: prefer Flatpak if available and support Flatpak sandbox execution * linux: match default package format to launcher format (Flatpak for Flatpak, AppImage for AppImage) * ui: default package preference selection to launcher format (Flatpak for Flatpak, AppImage for AppImage)
This commit is contained in:
1 parent
9eb2c60f18
commit
28ef3ad924
6 files changed
+186
-19
No files matched your search
@@ -217,11 +217,11 @@ pub mod linux {
|
||||
if let Some(list) = capture("pacman", &["-Q"], t) {
|
||||
add(InstallSource::Pacman, packages::parse_pacman(&list));
|
||||
}
|
||||
if let Some(list) = capture("flatpak", &["list", "--app", "--columns=application,version"], t) {
|
||||
if let Some(list) = crate::process::capture_flatpak(&["list", "--app", "--columns=application,version"], t) {
|
||||
let mut flatpaks = packages::parse_flatpak_list(&list);
|
||||
// Bundles built from a release tarball often list no version: ask Flatpak directly.
|
||||
for (id, version) in flatpaks.iter_mut().filter(|(id, v)| v.is_empty() && id.starts_with("ai.storyteller.")) {
|
||||
if let Some(v) = capture("flatpak", &["info", id], t).as_deref().and_then(packages::parse_flatpak_info) {
|
||||
if let Some(v) = crate::process::capture_flatpak(&["info", id], t).as_deref().and_then(packages::parse_flatpak_info) {
|
||||
*version = v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ pub fn launch(app: &CraftApp, installed: &Installed) -> Result<(), String> {
|
||||
let path = Path::new(&installed.location);
|
||||
match installed.source {
|
||||
InstallSource::Flatpak => {
|
||||
let mut cmd = process::command("flatpak");
|
||||
let mut cmd = process::flatpak_command();
|
||||
cmd.args(["run", &app.bundle_id()]);
|
||||
process::spawn_detached(cmd)
|
||||
}
|
||||
@@ -413,21 +413,50 @@ pub(crate) mod linux {
|
||||
}
|
||||
}
|
||||
|
||||
/// What this system can install: its own package format first (when polkit can ask for
|
||||
/// permission), then Flatpak, then AppImage, which works everywhere.
|
||||
/// 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 an AppImage: prefers AppImage.
|
||||
/// - Otherwise (system install): prefers native distro packages (`.deb` / `.rpm`),
|
||||
/// then Flatpak if available, then AppImage.
|
||||
pub fn available_kinds() -> Vec<PackageKind> {
|
||||
let mut kinds = Vec::new();
|
||||
let flatpak = process::is_flatpak_available();
|
||||
let is_flatpak = process::in_sandbox();
|
||||
let is_appimage = std::env::var_os("APPIMAGE").is_some();
|
||||
let root = which("pkexec").is_some();
|
||||
if root && which("apt-get").is_some() && which("dpkg").is_some() {
|
||||
let can_deb = root && which("apt-get").is_some() && which("dpkg").is_some();
|
||||
let can_rpm = root && which("rpm").is_some() && which("dpkg").is_none();
|
||||
order_available_kinds(is_flatpak, is_appimage, flatpak, can_deb, can_rpm)
|
||||
}
|
||||
|
||||
pub(crate) fn order_available_kinds(
|
||||
is_flatpak: bool,
|
||||
is_appimage: bool,
|
||||
flatpak_available: bool,
|
||||
can_deb: bool,
|
||||
can_rpm: bool,
|
||||
) -> Vec<PackageKind> {
|
||||
let mut kinds = Vec::new();
|
||||
if is_flatpak && flatpak_available {
|
||||
kinds.push(PackageKind::Flatpak);
|
||||
} else if is_appimage {
|
||||
kinds.push(PackageKind::AppImage);
|
||||
}
|
||||
|
||||
if can_deb {
|
||||
kinds.push(PackageKind::Deb);
|
||||
}
|
||||
if root && which("rpm").is_some() && which("dpkg").is_none() {
|
||||
if can_rpm {
|
||||
kinds.push(PackageKind::Rpm);
|
||||
}
|
||||
if which("flatpak").is_some() {
|
||||
|
||||
if flatpak_available && !kinds.contains(&PackageKind::Flatpak) {
|
||||
kinds.push(PackageKind::Flatpak);
|
||||
}
|
||||
kinds.push(PackageKind::AppImage);
|
||||
if !kinds.contains(&PackageKind::AppImage) {
|
||||
kinds.push(PackageKind::AppImage);
|
||||
}
|
||||
kinds
|
||||
}
|
||||
|
||||
@@ -443,7 +472,7 @@ pub(crate) mod linux {
|
||||
}
|
||||
match kind {
|
||||
PackageKind::Flatpak => {
|
||||
let status = process::command("flatpak")
|
||||
let status = process::flatpak_command()
|
||||
.args(["install", "--user", "-y", "--noninteractive", "--bundle"])
|
||||
.arg(f)
|
||||
.status()
|
||||
@@ -473,10 +502,17 @@ pub(crate) mod linux {
|
||||
InstallSource::Pacman => remove(&["pacman", "-R", "--noconfirm"], installed.package.as_deref().unwrap_or(app.id)),
|
||||
InstallSource::Flatpak => {
|
||||
let id = app.bundle_id();
|
||||
let user = process::command("flatpak").args(["uninstall", "--user", "-y", "--noninteractive", &id]).status();
|
||||
let user = process::flatpak_command().args(["uninstall", "--user", "-y", "--noninteractive", &id]).status();
|
||||
if user.is_ok_and(|s| s.success()) {
|
||||
return Ok(());
|
||||
}
|
||||
if process::in_sandbox() {
|
||||
let system = process::flatpak_command().args(["uninstall", "--system", "-y", "--noninteractive", &id]).status();
|
||||
if system.is_ok_and(|s| s.success()) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!("couldn't uninstall {id}"));
|
||||
}
|
||||
remove(&["flatpak", "uninstall", "--system", "-y", "--noninteractive"], &id)
|
||||
}
|
||||
other => Err(format!("{} isn't a system package", other.label())),
|
||||
@@ -994,4 +1030,34 @@ mod tests {
|
||||
assert_eq!(choose_kinds(Some(&inst(InstallSource::AppImage)), None, &[]), [PackageKind::AppImage]);
|
||||
let _ = inst;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn package_kinds_order_matches_launcher_format() {
|
||||
use super::linux::order_available_kinds;
|
||||
|
||||
// Flatpak launcher defaults to Flatpak if available
|
||||
let kinds = order_available_kinds(true, false, true, true, false);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::Flatpak));
|
||||
assert!(kinds.contains(&PackageKind::Deb));
|
||||
assert!(kinds.contains(&PackageKind::AppImage));
|
||||
|
||||
// AppImage launcher defaults to AppImage
|
||||
let kinds = order_available_kinds(false, true, true, true, false);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::AppImage));
|
||||
assert!(kinds.contains(&PackageKind::Deb));
|
||||
assert!(kinds.contains(&PackageKind::Flatpak));
|
||||
|
||||
// Base Linux launcher defaults to native package format (deb or rpm) if available
|
||||
let kinds = order_available_kinds(false, false, true, true, false);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::Deb));
|
||||
|
||||
let kinds = order_available_kinds(false, false, true, false, true);
|
||||
assert_eq!(kinds.first(), Some(&PackageKind::Rpm));
|
||||
|
||||
// Base Linux launcher without root/distro packaging falls back to Flatpak then AppImage
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -77,9 +77,56 @@ pub fn spawn_detached(mut cmd: Command) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether this process is running inside a Flatpak container sandbox.
|
||||
pub fn in_sandbox() -> bool {
|
||||
std::env::var_os("FLATPAK_ID").is_some() || Path::new("/.flatpak-info").exists()
|
||||
}
|
||||
|
||||
/// A command to run `flatpak`, delegating to the host via `flatpak-spawn --host` if inside a sandbox.
|
||||
pub fn flatpak_command() -> Command {
|
||||
if in_sandbox() {
|
||||
let mut cmd = command("flatpak-spawn");
|
||||
cmd.args(["--host", "flatpak"]);
|
||||
cmd
|
||||
} else {
|
||||
command("flatpak")
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a flatpak command and captures its standard output.
|
||||
pub fn capture_flatpak(args: &[&str], timeout: Duration) -> Option<String> {
|
||||
if in_sandbox() {
|
||||
let mut spawn_args = vec!["--host", "flatpak"];
|
||||
spawn_args.extend_from_slice(args);
|
||||
capture("flatpak-spawn", &spawn_args, timeout)
|
||||
} else {
|
||||
capture("flatpak", args, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Flatpak is available on the machine (or on the host when sandboxed).
|
||||
pub fn is_flatpak_available() -> bool {
|
||||
if in_sandbox() {
|
||||
capture("flatpak-spawn", &["--host", "flatpak", "--version"], Duration::from_secs(3)).is_some()
|
||||
} else {
|
||||
which_on_path("flatpak").is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn which_on_path(name: &str) -> Option<std::path::PathBuf> {
|
||||
let path = std::env::var_os("PATH")?;
|
||||
std::env::split_paths(&path).map(|d| d.join(name)).find(|p| p.is_file())
|
||||
}
|
||||
|
||||
/// Starts an app binary from its own folder.
|
||||
pub fn launch_binary(exe: &Path, args: &[&str]) -> Result<(), String> {
|
||||
let mut cmd = command(exe);
|
||||
let mut cmd = if in_sandbox() {
|
||||
let mut c = command("flatpak-spawn");
|
||||
c.arg("--host").arg(exe);
|
||||
c
|
||||
} else {
|
||||
command(exe)
|
||||
};
|
||||
cmd.args(args);
|
||||
if let Some(dir) = exe.parent() {
|
||||
cmd.current_dir(dir);
|
||||
@@ -95,6 +142,10 @@ pub fn open(target: &std::ffi::OsStr) -> Result<(), String> {
|
||||
let mut c = command("explorer");
|
||||
c.arg(target);
|
||||
return spawn_detached(c);
|
||||
} else if in_sandbox() {
|
||||
let mut c = command("flatpak-spawn");
|
||||
c.arg("--host").arg("xdg-open");
|
||||
c
|
||||
} else {
|
||||
command("xdg-open")
|
||||
};
|
||||
@@ -130,7 +181,11 @@ pub fn is_running(path: &Path) -> bool {
|
||||
let Some(p) = path.to_str() else { return false };
|
||||
let pattern = pgrep_pattern(p);
|
||||
// pgrep exits 0 when something matches; it never matches itself.
|
||||
capture("pgrep", &["-f", "--", &pattern], Duration::from_secs(5)).is_some()
|
||||
if in_sandbox() {
|
||||
capture("flatpak-spawn", &["--host", "pgrep", "-f", "--", &pattern], Duration::from_secs(5)).is_some()
|
||||
} else {
|
||||
capture("pgrep", &["-f", "--", &pattern], Duration::from_secs(5)).is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ fn msi_install_dir() -> Option<PathBuf> {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect(exe: &Path) -> Option<Install> {
|
||||
if std::env::var_os("FLATPAK_ID").is_some() {
|
||||
if process::in_sandbox() {
|
||||
return None;
|
||||
}
|
||||
if let Some(appimage) = std::env::var_os("APPIMAGE") {
|
||||
|
||||
@@ -227,11 +227,26 @@ impl LauncherUi {
|
||||
|
||||
if self.caps.manage_apps && self.platform.is_some_and(|p| p.os == Os::Linux) {
|
||||
section(ui, tl!("Installing on Linux"));
|
||||
let mut choice = self.settings.preferred_kinds.first().copied();
|
||||
let is_flatpak = crate::settings::is_flatpak_sandbox();
|
||||
let is_appimage = crate::settings::is_appimage();
|
||||
let default_kind = if is_flatpak {
|
||||
Some(PackageKind::Flatpak)
|
||||
} else if is_appimage {
|
||||
Some(PackageKind::AppImage)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut choice = self.settings.preferred_kinds.first().copied().or(default_kind);
|
||||
row(ui, tl!("Install new apps as"), &mut |ui| {
|
||||
let label = choice.map_or(tl!("System package (recommended)"), |k| crate::i18n::t(k.label()));
|
||||
let default_label = match default_kind {
|
||||
Some(k) => crate::i18n::t(k.label()),
|
||||
None => tl!("System package (recommended)"),
|
||||
};
|
||||
let label = choice.map_or(default_label, |k| crate::i18n::t(k.label()));
|
||||
egui::ComboBox::from_id_salt("kind").selected_text(label).width(210.0).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut choice, None, tl!("System package (recommended)"));
|
||||
if !is_flatpak && !is_appimage {
|
||||
ui.selectable_value(&mut choice, None, tl!("System package (recommended)"));
|
||||
}
|
||||
for k in [PackageKind::Deb, PackageKind::Rpm, PackageKind::Flatpak, PackageKind::AppImage] {
|
||||
ui.selectable_value(&mut choice, Some(k), crate::i18n::t(k.label()));
|
||||
}
|
||||
|
||||
@@ -132,6 +132,37 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this process is running inside a Flatpak container sandbox.
|
||||
pub fn is_flatpak_sandbox() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
std::env::var_os("FLATPAK_ID").is_some() || std::path::Path::new("/.flatpak-info").exists()
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this process was launched from an AppImage.
|
||||
pub fn is_appimage() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
std::env::var_os("APPIMAGE").is_some()
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
false
|
||||
}
|
||||
|
||||
/// Default package kinds preferred on Linux based on how the launcher itself was installed.
|
||||
pub fn default_preferred_kinds() -> Vec<PackageKind> {
|
||||
if is_flatpak_sandbox() {
|
||||
vec![PackageKind::Flatpak]
|
||||
} else if is_appimage() {
|
||||
vec![PackageKind::AppImage]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -147,7 +178,7 @@ impl Default for Settings {
|
||||
auto_update: false,
|
||||
notify: true,
|
||||
prereleases: false,
|
||||
preferred_kinds: Vec::new(),
|
||||
preferred_kinds: default_preferred_kinds(),
|
||||
dismissed_notices: std::collections::BTreeSet::new(),
|
||||
notified: BTreeMap::new(),
|
||||
last_check: None,
|
||||
|
||||
Reference in new issue
Block a user