Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 94 additions & 27 deletions src-tauri/src/autostart/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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<PathBuf>) -> Result<String, String> {
/// - **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<tauri::utils::config::BundleType>,
appimage: Option<&std::ffi::OsStr>,
exe: Option<PathBuf>,
) -> Result<String, String> {
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()),
Expand All @@ -195,6 +204,13 @@ fn program(exe: Option<PathBuf>) -> Result<String, String> {
})
}

/// `<AppDir>/AppRun` for a binary at `<AppDir>/usr/bin/<name>`, when it is there.
fn app_run(exe: &Path) -> Option<PathBuf> {
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
Expand Down Expand Up @@ -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<BundleType> = 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)]
Expand Down
17 changes: 6 additions & 11 deletions src-tauri/src/desktop_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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.
Expand Down
64 changes: 54 additions & 10 deletions src-tauri/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,41 @@ 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<tauri::utils::config::BundleType>,
appimage: Option<&std::ffi::OsStr>,
) -> Option<std::path::PathBuf> {
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")]
pub fn linux_kind(
bundle: Option<tauri::utils::config::BundleType>,
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,
}
}

Expand Down Expand Up @@ -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]
Expand Down
Loading