- 新增 update 模块:从 dl.shellrs.com 取已签名的更新清单(minisign,签名绑定通道和版本), 按大小和 SHA-256 校验安装包;下载可续传、可换镜像,走系统代理 - 按安装方式安装:macOS 整包交换并核对开发者签名,Windows 静默运行安装程序,Linux 覆盖 AppImage; 开发构建和无法自更新的安装只提示前往下载页 - 设置新增「关于」:版本信息、检查更新、下载进度和「自动检查并下载更新」开关; 新版本就绪后标题栏右上角提示,更新对话框写明重启会中断什么;退出时自动安装,新版本启动后提示已更新 - 数据库拒绝打开更新版本建的库,迁移已有的库之前先备份成 shellrs.db.v<旧版本>.bak - 新增 release workflow 和 packaging 脚本:universal macOS 包签名公证、Inno Setup 安装程序、 AppImage、上传 R2、生成并签名更新清单;新增 CHANGELOG.md - 应用图标放在 assets/logo;开发模式下 macOS Dock 显示 logo,Windows exe 嵌入图标 - 修正 AppImage 里 shellrs 命令链接到临时挂载点;启动时刷新已安装但过期的 Agent skill - 新增清单、验签、下载、各平台安装器和界面的单元测试与 UI 测试
128 lines
3.9 KiB
Rust
128 lines
3.9 KiB
Rust
//! Linux: an AppImage is one file, renamed over.
|
|
//!
|
|
//! The new AppImage is copied next to the running one first, so putting it
|
|
//! in place is one rename on one volume. The running copy keeps its mount
|
|
//! of the old file until it exits; the restart starts `$APPIMAGE`, not the
|
|
//! mounted executable, which disappears with the old process.
|
|
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt as _;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use super::error::UpdateError;
|
|
use super::install::{InstallKind, Installer, Relaunch, Staged, sibling};
|
|
use super::manifest::Release;
|
|
|
|
pub struct AppImageInstaller {
|
|
kind: InstallKind,
|
|
file: PathBuf,
|
|
}
|
|
|
|
impl AppImageInstaller {
|
|
pub fn new(file: PathBuf) -> Self {
|
|
Self {
|
|
kind: InstallKind::AppImage { file: file.clone() },
|
|
file,
|
|
}
|
|
}
|
|
|
|
fn prefix(&self) -> String {
|
|
format!(
|
|
".{}.update-",
|
|
self.file.file_name().unwrap_or_default().to_string_lossy()
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Installer for AppImageInstaller {
|
|
fn kind(&self) -> &InstallKind {
|
|
&self.kind
|
|
}
|
|
|
|
fn stage(&self, package: &Path, release: &Release) -> Result<Staged, UpdateError> {
|
|
let staged = sibling(&self.file, &format!("{}{}", self.prefix(), release.version));
|
|
fs::copy(package, &staged)?;
|
|
fs::set_permissions(&staged, fs::Permissions::from_mode(0o755))?;
|
|
Ok(Staged {
|
|
version: release.version.clone(),
|
|
path: staged,
|
|
})
|
|
}
|
|
|
|
fn apply(&self, staged: &Staged, relaunch: bool) -> Result<Relaunch, UpdateError> {
|
|
fs::rename(&staged.path, &self.file)
|
|
.map_err(|error| UpdateError::Install(error.to_string()))?;
|
|
Ok(if relaunch {
|
|
Relaunch::Restart(self.file.clone())
|
|
} else {
|
|
Relaunch::Nothing
|
|
})
|
|
}
|
|
|
|
fn clean_up(&self) {
|
|
let Some(folder) = self.file.parent() else {
|
|
return;
|
|
};
|
|
let prefix = self.prefix();
|
|
for entry in fs::read_dir(folder).into_iter().flatten().flatten() {
|
|
if entry.file_name().to_string_lossy().starts_with(&prefix) {
|
|
let _ = fs::remove_file(entry.path());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use semver::Version;
|
|
|
|
use super::*;
|
|
use crate::update::manifest::Asset;
|
|
|
|
#[test]
|
|
fn the_appimage_is_replaced_in_one_rename_and_stays_executable() {
|
|
let folder = tempfile::tempdir().unwrap();
|
|
let file = folder.path().join("ShellRS-x86_64.AppImage");
|
|
fs::write(&file, "old").unwrap();
|
|
let package = folder.path().join("downloaded");
|
|
fs::write(&package, "new").unwrap();
|
|
let release = Release {
|
|
version: Version::new(0, 2, 0),
|
|
published_at: None,
|
|
notes: String::new(),
|
|
notes_url: None,
|
|
asset: Asset {
|
|
urls: Vec::new(),
|
|
size: 3,
|
|
sha256: String::new(),
|
|
},
|
|
installer: None,
|
|
};
|
|
let installer = AppImageInstaller::new(file.clone());
|
|
let staged = installer.stage(&package, &release).unwrap();
|
|
assert_eq!(fs::read_to_string(&file).unwrap(), "old");
|
|
|
|
assert_eq!(
|
|
installer.apply(&staged, true),
|
|
Ok(Relaunch::Restart(file.clone()))
|
|
);
|
|
assert_eq!(fs::read_to_string(&file).unwrap(), "new");
|
|
let mode = fs::metadata(&file).unwrap().permissions().mode();
|
|
assert_eq!(mode & 0o777, 0o755);
|
|
|
|
fs::write(
|
|
sibling(&file, ".ShellRS-x86_64.AppImage.update-0.1.9"),
|
|
"stale",
|
|
)
|
|
.unwrap();
|
|
installer.clean_up();
|
|
let left: Vec<_> = fs::read_dir(folder.path())
|
|
.unwrap()
|
|
.flatten()
|
|
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
|
.collect();
|
|
assert_eq!(left.len(), 2, "{left:?}");
|
|
assert!(left.contains(&"ShellRS-x86_64.AppImage".to_string()));
|
|
}
|
|
}
|