diff --git a/src-tauri/src/autostart/linux.rs b/src-tauri/src/autostart/linux.rs index 1f4fb684..b87e89d6 100644 --- a/src-tauri/src/autostart/linux.rs +++ b/src-tauri/src/autostart/linux.rs @@ -14,7 +14,8 @@ //! `~/.config`), with `Exec` encoded per the Desktop Entry Specification //! (section "The Exec key", plus the string escapes of "Possible value types"); //! - points it at `$APPIMAGE` for an AppImage (the running binary lives under a -//! per-launch `/tmp/.mount_*` directory) and at the installed binary otherwise; +//! per-launch `/tmp/.mount_*` directory), at the AppDir's `AppRun` for an +//! unpacked one, and at the installed binary otherwise; //! - treats `Hidden=true` and `X-GNOME-Autostart-enabled=false` as off, which is //! how GNOME and KDE record "disabled" without deleting the file; //! - rewrites the entry on every launch when it points somewhere else (the @@ -48,7 +49,11 @@ impl Autostart { // there; a development build gets the desktop's generic icon. Self { file, - program: program(std::env::current_exe().ok()), + program: program( + tauri::utils::platform::bundle_type(), + std::env::var_os("APPIMAGE").as_deref(), + std::env::current_exe().ok(), + ), name: app.package_info().name.clone(), icon: app.config().identifier.clone(), } @@ -158,32 +163,36 @@ impl Autostart { /// What the entry should start. /// -/// **Asks the bundle type, not the environment alone.** `$APPIMAGE` is inherited -/// by everything an AppImage starts, so a development build launched from, say, -/// an AppImage terminal emulator would see someone else's `$APPIMAGE`. The bundle -/// type is patched into the binary by the bundler, so it cannot be inherited. -fn program(exe: Option) -> Result { +/// - **An AppImage the runtime mounted**: the `.AppImage` file. Which one that is +/// comes from the bundle marker and `$APPIMAGE` together +/// (`update::mounted_appimage`). +/// - **An unpacked AppImage** (the AUR `thinkwatch-lite-bin`, +/// `--appimage-extract`): the AppDir's `AppRun`, never the binary itself. +/// It keeps the bundle marker but has no `$APPIMAGE`. `AppRun` changes into +/// `usr/` and puts the bundled libraries first, and the bundled WebKit needs +/// both: the bundler rewrites `/usr` to `././` in it, so it looks for its +/// helper processes relative to the working directory. That is also what the +/// AUR package's own launcher starts. +/// - **Anything else**: the running binary. +fn program( + bundle: Option, + appimage: Option<&std::ffi::OsStr>, + exe: Option, +) -> Result { use tauri::utils::config::BundleType; - let path = if tauri::utils::platform::bundle_type() == Some(BundleType::AppImage) { - std::env::var_os("APPIMAGE") - .map(PathBuf::from) - .filter(|p| p.is_absolute()) - .ok_or_else(|| { - tr!( - "未能确定 AppImage 文件的位置(APPIMAGE 未设置)", - "The location of the AppImage file could not be determined (APPIMAGE is not set)" - ) - .to_string() - })? - } else { - exe.ok_or_else(|| { - tr!( - "未能确定应用程序的位置", - "The location of the application could not be determined" - ) - .to_string() - })? - }; + let unpacked = bundle == Some(BundleType::AppImage); + let path = match crate::update::mounted_appimage(bundle, appimage) { + Some(file) => Some(file), + None if unpacked => exe.map(|exe| app_run(&exe).unwrap_or(exe)), + None => exe, + } + .ok_or_else(|| { + tr!( + "未能确定应用程序的位置", + "The location of the application could not be determined" + ) + .to_string() + })?; path.into_os_string().into_string().map_err(|p| { tr!( format!("应用程序路径不是有效的 UTF-8:{}", p.to_string_lossy()), @@ -195,6 +204,13 @@ fn program(exe: Option) -> Result { }) } +/// `/AppRun` for a binary at `/usr/bin/`, when it is there. +fn app_run(exe: &Path) -> Option { + let bin = exe.parent().filter(|d| d.ends_with("usr/bin"))?; + let app_run = bin.parent()?.parent()?.join("AppRun"); + app_run.is_file().then_some(app_run) +} + /// `$XDG_CONFIG_HOME/autostart`, or `$HOME/.config/autostart`. /// /// Per the XDG Base Directory Specification a relative `$XDG_CONFIG_HOME` is @@ -633,6 +649,57 @@ mod tests { assert_eq!(autostart_dir(None, None), None); } + /// The mounted AppImage file; an unpacked AppImage's `AppRun`, not its bare + /// binary; the running binary for anything else. Unix paths only, as above. + #[cfg(unix)] + #[test] + fn the_entry_starts_the_appimage_or_its_app_run() { + use std::ffi::OsStr; + use tauri::utils::config::BundleType; + const APPIMAGE: Option = Some(BundleType::AppImage); + let (_tmp, dir) = tempdir(); + let exe = dir.join("usr/bin/thinkwatch-lite"); + let app_run = dir.join("AppRun"); + let image = OsStr::new("/home/a/Apps/ThinkWatch-Lite.AppImage"); + let s = |p: &Path| p.to_str().unwrap().to_owned(); + + // Mounted by the runtime: the AppImage file + assert_eq!( + program(APPIMAGE, Some(image), Some(exe.clone())).unwrap(), + "/home/a/Apps/ThinkWatch-Lite.AppImage" + ); + // Unpacked, before there is an `AppRun` to find: the binary + assert_eq!(program(APPIMAGE, None, Some(exe.clone())).unwrap(), s(&exe)); + // Unpacked (no, empty or relative `$APPIMAGE`): `AppRun` + std::fs::write(&app_run, "").unwrap(); + for v in [ + None, + Some(OsStr::new("")), + Some(OsStr::new("rel/x.AppImage")), + ] { + assert_eq!( + program(APPIMAGE, v, Some(exe.clone())).unwrap(), + s(&app_run), + "{v:?}" + ); + } + // No marker: the binary, even next to an `AppRun`, and someone else's + // `$APPIMAGE` is never used + assert_eq!( + program(None, Some(image), Some(exe.clone())).unwrap(), + s(&exe) + ); + // A binary outside `usr/bin` has no AppDir around it + let elsewhere = dir.join("thinkwatch-lite"); + assert_eq!( + program(APPIMAGE, None, Some(elsewhere.clone())).unwrap(), + s(&elsewhere) + ); + // Neither a location nor a running binary + assert!(program(APPIMAGE, None, None).is_err()); + assert!(program(None, None, None).is_err()); + } + /// Writes, reads back, is switched off by "GNOME", re-enabled in place, and /// repaired after a "move" — against a temporary directory, on any unix. #[cfg(unix)] diff --git a/src-tauri/src/desktop_entry.rs b/src-tauri/src/desktop_entry.rs index 2116802a..4eb40931 100644 --- a/src-tauri/src/desktop_entry.rs +++ b/src-tauri/src/desktop_entry.rs @@ -80,18 +80,13 @@ fn data_home( } /// The running AppImage file. `None` for anything else (a development build, -/// or an AppImage unpacked with `--appimage-extract`). -/// -/// The bundle type comes first for the same reason as in `update::linux_kind`: -/// `$APPIMAGE` is inherited by everything an AppImage starts. +/// or an AppImage unpacked with `--appimage-extract`); see +/// `update::mounted_appimage`. pub(crate) fn appimage() -> Option { - use tauri::utils::config::BundleType; - if tauri::utils::platform::bundle_type() != Some(BundleType::AppImage) { - return None; - } - std::env::var_os("APPIMAGE") - .map(PathBuf::from) - .filter(|p| p.is_absolute()) + crate::update::mounted_appimage( + tauri::utils::platform::bundle_type(), + std::env::var_os("APPIMAGE").as_deref(), + ) } /// The whole file. diff --git a/src-tauri/src/update.rs b/src-tauri/src/update.rs index 674041c1..c2d4a8f8 100644 --- a/src-tauri/src/update.rs +++ b/src-tauri/src/update.rs @@ -178,12 +178,31 @@ pub fn official_build() -> bool { option_env!("TW_OFFICIAL_BUILD") == Some("1") } +/// 运行时挂载起来跑的那个 AppImage 文件。 +/// +/// **打包标记和 `APPIMAGE` 两样都要。**`APPIMAGE` 会被 AppImage 拉起来的一切 +/// 继承,只看它会拿到别人的文件;标记打在二进制里,继承不来。可只有标记也 +/// 不够:解开来跑的(AUR 的 `thinkwatch-lite-bin`、`--appimage-extract`)标记 +/// 照样是 AppImage,但运行时挂载时才设 `APPIMAGE`,这时根本没有那个文件。 +/// +/// 更新([`linux_kind`])、菜单条目(`desktop_entry`)、自启项 +/// (`autostart::linux`)都按这一条判断。 +pub fn mounted_appimage( + bundle: Option, + appimage: Option<&std::ffi::OsStr>, +) -> Option { + if bundle != Some(tauri::utils::config::BundleType::AppImage) { + return None; + } + appimage + .map(std::path::PathBuf::from) + .filter(|p| p.is_absolute()) +} + /// Linux 上这一份是怎么装上来的。 /// -/// **AppImage 还要看 `APPIMAGE` 在不在。**同一个打过补丁的二进制,用户用 -/// `--appimage-extract` 解开之后直接跑,标记照样是 AppImage,而这时根本没有 -/// 一个可以原地替换的 AppImage 文件 —— 插件会去替换正在跑的那个可执行文件。 -/// 运行时(AppImage 的 runtime)挂载时才设这个变量。 +/// **只有挂载起来跑的 AppImage 自己更新**([`mounted_appimage`])。解开来跑的 +/// 没有一个可以原地替换的 AppImage 文件 —— 插件会去替换正在跑的那个可执行文件。 /// /// deb、rpm 都不发,自己打出来的也不自己更新。 #[cfg(target_os = "linux")] @@ -191,12 +210,9 @@ pub fn linux_kind( bundle: Option, appimage: Option<&std::ffi::OsStr>, ) -> Install { - use tauri::utils::config::BundleType; - match bundle { - Some(BundleType::AppImage) if appimage.is_some_and(|p| !p.is_empty()) => { - Install::Standalone - } - _ => Install::Dev, + match mounted_appimage(bundle, appimage) { + Some(_) => Install::Standalone, + None => Install::Dev, } } @@ -482,6 +498,34 @@ mod tests { ); } + /// 标记和一个绝对路径的 `APPIMAGE` 缺一不可(Windows 上 `/home/…` 不算绝对路径)。 + #[cfg(unix)] + #[test] + fn a_mounted_appimage_needs_the_marker_and_the_variable() { + use std::ffi::OsStr; + use tauri::utils::config::BundleType; + let image = OsStr::new("/home/u/Apps/ThinkWatch-Lite.AppImage"); + assert_eq!( + mounted_appimage(Some(BundleType::AppImage), Some(image)), + Some(std::path::PathBuf::from(image)) + ); + // 解开来跑的:有标记、没有变量 + for v in [ + None, + Some(OsStr::new("")), + Some(OsStr::new("rel/x.AppImage")), + ] { + assert_eq!( + mounted_appimage(Some(BundleType::AppImage), v), + None, + "{v:?}" + ); + } + // 没有标记,环境里那个 `APPIMAGE` 是别人的 + assert_eq!(mounted_appimage(None, Some(image)), None); + assert_eq!(mounted_appimage(Some(BundleType::Deb), Some(image)), None); + } + /// Linux 上信打包时写进去的标记,不信路径和环境变量。 #[cfg(target_os = "linux")] #[test]