From af61c93bd8387b9326f75d49753ba2941dc28094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sat, 24 Oct 2026 03:13:45 +0200 Subject: [PATCH 1/2] fix: open links from the AppImage --- resources/AppRun | 8 +++----- scripts/build-appimage.sh | 15 ++++++++++++++ scripts/test-release.py | 10 ++++++++++ src/browser.rs | 42 ++++++--------------------------------- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/resources/AppRun b/resources/AppRun index f4a84b09..e838d1e4 100755 --- a/resources/AppRun +++ b/resources/AppRun @@ -85,12 +85,10 @@ fi export MALLOC_ARENA_MAX="${MALLOC_ARENA_MAX:-1}" export MALLOC_TRIM_THRESHOLD_="${MALLOC_TRIM_THRESHOLD_:-65536}" -# Preserve the host search path for external programs such as the browser. -export FLECTAR_MAIL_HOST_LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}" -export FLECTAR_MAIL_BUNDLED=1 +# The executable finds the bundled libraries in usr/lib through its RUNPATH. +# Do not export LD_LIBRARY_PATH: the browser and every other program the app +# launches would inherit it and load those libraries instead of the host's. -# linuxdeploy places any non-system ELF dependencies here. -export LD_LIBRARY_PATH="$app_dir/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # The Linux binary supports Slint's Wayland and X11 winit integrations. Slint # paints every application control while the window system owns window chrome. export SLINT_BACKEND="${SLINT_BACKEND:-winit-software}" diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 3fbeafb9..cd8ae357 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -108,6 +108,21 @@ for library in libxkbcommon-x11.so.0 libxkbcommon.so.0; do fi done +# AppRun leaves LD_LIBRARY_PATH to the host, so the executable and bundled +# libraries must find usr/lib through the RUNPATH linuxdeploy sets. +require_runpath() { + local dynamic + dynamic="$(readelf -d "$1")" + if [[ "$dynamic" != *"Library runpath: [$2]"* ]]; then + printf '%s does not have RUNPATH %s.\n' "$1" "$2" >&2 + exit 1 + fi +} +require_runpath "$app_dir/usr/bin/flectar-mail" '$ORIGIN/../lib' +for library in "$app_dir"/usr/lib/*.so*; do + require_runpath "$library" '$ORIGIN' +done + # Preserve the allocator tuning and stable process name from our launcher. cp "$project_dir/resources/AppRun" "$app_dir/AppRun" chmod +x "$app_dir/AppRun" diff --git a/scripts/test-release.py b/scripts/test-release.py index f6b062f0..54c7d471 100644 --- a/scripts/test-release.py +++ b/scripts/test-release.py @@ -406,6 +406,16 @@ class ReleaseTests(unittest.TestCase): f"print({str(libraries)!r})\n" ) pkg_config.chmod(0o755) + # The fixture ELFs carry no RUNPATH; report the one linuxdeploy sets. + readelf = root / "bin/readelf" + readelf.write_text( + "#!/usr/bin/env python3\n" + "import sys\n" + "assert sys.argv[1] == '-d'\n" + "runpath = '$ORIGIN/../lib' if sys.argv[2].endswith('/usr/bin/flectar-mail') else '$ORIGIN'\n" + "print(f' 0x000000000000001d (RUNPATH) Library runpath: [{runpath}]')\n" + ) + readelf.chmod(0o755) deploy = root / "bin/linuxdeploy" deploy.write_text('''#!/usr/bin/env python3 import pathlib, shutil, sys diff --git a/src/browser.rs b/src/browser.rs index 177bcea4..81dd5f20 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -6,9 +6,9 @@ type OpenUrl = dyn Fn(&str) -> Result<(), String> + Send + Sync; /// The URL service carried by [`crate::PlatformContext`]. /// -/// The default uses the system browser, preserving Linux's portal and AppImage -/// library-path handling for authorization. Hosts without desktop launchers -/// supply a callback with [`Self::new`]. Clones share that callback's state. +/// The default uses the system browser, preferring Linux's desktop portal for +/// authorization. Hosts without desktop launchers supply a callback with +/// [`Self::new`]. Clones share that callback's state. #[derive(Clone, Default)] pub struct ExternalUrlOpener { handler: Option>, @@ -51,13 +51,8 @@ impl ExternalUrlOpener { use std::process::{Command, Stdio}; #[cfg(linux_desktop)] -fn host_command(program: &str) -> Command { +fn quiet_command(program: &str) -> Command { let mut command = Command::new(program); - restore_library_path( - &mut command, - std::env::var_os("FLECTAR_MAIL_BUNDLED").is_some(), - std::env::var_os("FLECTAR_MAIL_HOST_LD_LIBRARY_PATH"), - ); command .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -65,21 +60,6 @@ fn host_command(program: &str) -> Command { command } -#[cfg(linux_desktop)] -fn restore_library_path(command: &mut Command, bundled: bool, path: Option) { - if !bundled { - return; - } - match path.filter(|p| !p.is_empty()) { - Some(path) => { - command.env("LD_LIBRARY_PATH", path); - } - None => { - command.env_remove("LD_LIBRARY_PATH"); - } - } -} - #[cfg(linux_desktop)] fn launch(mut command: Command) -> Result<(), String> { let mut child = command.spawn().map_err(|e| e.to_string())?; @@ -132,12 +112,12 @@ fn open_system_authorization(address: &url::Url) -> Result<(), String> { if open_with_desktop_portal(address).is_ok() { return Ok(()); } - let mut xdg = host_command("xdg-open"); + let mut xdg = quiet_command("xdg-open"); xdg.arg(url); if launch(xdg).is_ok() { return Ok(()); } - let mut gio = host_command("gio"); + let mut gio = quiet_command("gio"); gio.args(["open", url]); launch(gio).map_err(|_| "Could not open the default browser".into()) } @@ -266,16 +246,6 @@ mod tests { assert!(launch(command).is_ok()); } #[test] - fn restores_host_libraries_only_for_bundled_builds() { - let mut command = Command::new("xdg-open"); - restore_library_path(&mut command, false, None); - assert_eq!(command.get_envs().count(), 0); - restore_library_path(&mut command, true, Some("/host/lib".into())); - assert_eq!(command.get_envs().next().unwrap().1.unwrap(), "/host/lib"); - restore_library_path(&mut command, true, Some("".into())); - assert_eq!(command.get_envs().next().unwrap().1, None); - } - #[test] fn foreground_browser_does_not_hold_up_authorization() { let mut command = Command::new("sh"); command.args(["-c", "sleep 2"]); From 39dbfee542b67cb291a362949d55178f1999951c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sat, 24 Oct 2026 03:14:54 +0200 Subject: [PATCH 2/2] fix: tune the allocator in-process --- resources/AppRun | 6 ------ scripts/build-appimage.sh | 2 +- src/desktop.rs | 24 ++++++++++++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/resources/AppRun b/resources/AppRun index e838d1e4..873a76c6 100755 --- a/resources/AppRun +++ b/resources/AppRun @@ -79,12 +79,6 @@ if [[ "${1:-}" == "--integrate" ]]; then exit 0 fi -# The mail shell has a small number of worker threads. One glibc allocation -# arena is enough and prevents per-thread arenas from retaining render/sync -# scratch memory after a burst of work. Respect an explicit user override. -export MALLOC_ARENA_MAX="${MALLOC_ARENA_MAX:-1}" -export MALLOC_TRIM_THRESHOLD_="${MALLOC_TRIM_THRESHOLD_:-65536}" - # The executable finds the bundled libraries in usr/lib through its RUNPATH. # Do not export LD_LIBRARY_PATH: the browser and every other program the app # launches would inherit it and load those libraries instead of the host's. diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index cd8ae357..2ff4e886 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -123,7 +123,7 @@ for library in "$app_dir"/usr/lib/*.so*; do require_runpath "$library" '$ORIGIN' done -# Preserve the allocator tuning and stable process name from our launcher. +# Preserve the renderer default and stable process name from our launcher. cp "$project_dir/resources/AppRun" "$app_dir/AppRun" chmod +x "$app_dir/AppRun" diff --git a/src/desktop.rs b/src/desktop.rs index b0db946c..1cbb81ca 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,6 +1,8 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() -> Result<(), Box> { + #[cfg(glibc)] + tune_allocator(); // Slint 1.18 keeps the native window and its rendering surface when // hidden on X11 and Windows. Use its window recreation path for tray mode, // as it already does on Wayland, so a hidden window holds no surface. @@ -18,3 +20,25 @@ fn main() -> Result<(), Box> { } flectar_mail::run_desktop(flectar_mail::platform::launch_context()?) } + +/// The mail shell has a small number of worker threads. One glibc allocation +/// arena is enough and prevents per-thread arenas from retaining render/sync +/// scratch memory after a burst of work. Set in-process rather than through +/// the environment, which the browser and other launched programs would +/// inherit. Explicit user tuning wins. +#[cfg(glibc)] +fn tune_allocator() { + let tunables = std::env::var("GLIBC_TUNABLES").unwrap_or_default(); + let user_set = |variable: &str, tunable: &str| { + std::env::var_os(variable).is_some() || tunables.contains(tunable) + }; + // SAFETY: mallopt only adjusts allocator parameters; no pointers change. + unsafe { + if !user_set("MALLOC_ARENA_MAX", "glibc.malloc.arena_max") { + libc::mallopt(libc::M_ARENA_MAX, 1); + } + if !user_set("MALLOC_TRIM_THRESHOLD_", "glibc.malloc.trim_threshold") { + libc::mallopt(libc::M_TRIM_THRESHOLD, 64 * 1024); + } + } +}