From a8e64baf21597a8f68c3cca91127b65df0cdcb43 Mon Sep 17 00:00:00 2001 From: kuddev Date: Tue, 15 Sep 2026 23:38:21 +0800 Subject: [PATCH] chore(release): prepare 1.8.1 metadata and release contracts Synchronize package versions and bilingual notes with verified issue references. Allow releases without PR contributors to omit that section, with positive and negative format coverage as requested by the maintainer. Keep native update process operations and default-shell resolution inside the platform boundary. Preserve transaction authority in the updater and tighten the platform condition budget to the measured 484. --- CHANGELOG.md | 60 ++++++ Cargo.lock | 14 +- docs/architecture-decisions.md | 17 ++ docs/release-notes/v1.8.1.md | 59 ++++++ nebula-completions/Cargo.toml | 2 +- nebula_app/Cargo.toml | 12 +- nebula_app/src/gpui_shell/mod.rs | 26 +-- .../src/gpui_shell/terminal/view/startup.rs | 7 +- nebula_app/src/platform/mod.rs | 1 + nebula_app/src/platform/shell.rs | 13 ++ .../src/platform/update_installation.rs | 72 +++++++ nebula_app/src/update_download/handoff.rs | 198 +++++++----------- nebula_app/windows/nebula.rc | 8 +- nebula_config/Cargo.toml | 4 +- nebula_config_derive/Cargo.toml | 4 +- nebula_settings/Cargo.toml | 2 +- nebula_split/Cargo.toml | 2 +- nebula_terminal/Cargo.toml | 2 +- scripts/installer.iss | 4 +- scripts/platform_cfg_budget.txt | 2 +- scripts/stable_release.py | 11 +- scripts/tests/test_stable_release.py | 21 ++ tools/i18n-contract/Cargo.lock | 2 +- 23 files changed, 366 insertions(+), 177 deletions(-) create mode 100644 docs/release-notes/v1.8.1.md create mode 100644 nebula_app/src/platform/update_installation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 17201b4..9d1cccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,66 @@ Every release entry is provided in English and Simplified Chinese. 每个版本条目均同时提供英文和简体中文说明。 +## 1.8.1 - 2026-09-15 + +### English + +#### Added + +- Added a custom theme library with saved snapshots, a preview editor, and theme import/export. Saved themes can retain terminal colors, typography and window appearance alongside the built-in themes. +- Added optional background update downloads and an option to install a verified download on the next application launch on Windows. + +#### Fixed + +- Restored the default terminal font to the exact Maple Mono Normal NF CN font bundled with 1.7.0 after the default changed in 1.8.0. Explicitly selected custom fonts remain available. Addresses [#148](https://github.com/Kuddev/nebula/issues/148). +- Fixed update checks and installer downloads ignoring the application's proxy configuration, including proxy exclusions and SOCKS connections. +- Fixed scheduled updates merging saved windows into one window. Update recovery retains each window's tabs and active tab; this does not change desktop position or window-size restoration. +- Fixed the Windows installer failing to reuse its registered installation directory when updating an existing installation. +- Restored the 1.7 tab-status alignment and prevented status glyphs from being clipped. + +#### Improved + +- Improved Pi conversation recovery by retaining the native session ID and session file together with each pane's shell and working directory. Failed restoration keeps the original target available for retry instead of treating command submission as successful recovery. Addresses [#146](https://github.com/Kuddev/nebula/issues/146). +- Improved Windows update handoff: save workspace state before exit, wait for the old process to stop before installing, verify the installer, and retain failure details when installation or restart does not complete. +- Improved cleanup of terminals on Windows when panes close, including console resources and process-monitoring handles; retained font ownership while text rendering still needs it. + +Windows packages include a Pebrel installer and portable ZIP. Linux x64 packages and macOS Apple Silicon/Intel DMGs retain their Preview designation. macOS requires version 14 or later. Molecular structure rendering remains disabled. + +### 中文 + +#### 新增 + +- 新增:自定义主题库,支持保存主题快照、预览编辑和主题导入导出;可在内置主题之外保存终端配色、字体排版及窗口外观。 +- 新增:Windows 可选的后台更新下载,以及在下次启动应用时安装已校验下载包的选项。 + +#### 修复 + +- 修复:将 1.8.0 更换后的默认终端字体恢复为与 1.7.0 完全相同的内置 Maple Mono Normal NF CN;用户明确选择的自定义字体仍可使用。对应 [#148](https://github.com/Kuddev/nebula/issues/148)。 +- 修复:检查更新和下载安装器时忽略应用代理配置的问题,包括代理排除规则与 SOCKS 连接。 +- 修复:计划更新将已保存的多个窗口合并成一个窗口的问题;更新恢复时保留各窗口的标签及活动标签,不改变桌面位置或窗口尺寸的恢复行为。 +- 修复:Windows 安装器更新已有安装时未复用已注册安装目录的问题。 +- 修复:恢复 1.7 的标签状态对齐方式,避免状态符号被裁切。 + +#### 改进 + +- 改进:Pi 会话恢复同时保留原生会话 ID、会话文件,以及各窗格的 Shell 和工作目录。恢复失败时保留原目标以便重试,不再仅凭恢复命令已提交就视为恢复成功。对应 [#146](https://github.com/Kuddev/nebula/issues/146)。 +- 改进:Windows 更新交接在退出前保存工作区,等待旧进程退出后再安装,校验安装包,并在安装或重启未完成时保留失败详情。 +- 改进:Windows 关闭窗格时释放控制台资源及进程监测句柄,并在文字渲染仍需使用字体时保持其有效。 + +Windows 提供 Pebrel 安装器和 ZIP 便携包;Linux x64 包以及 macOS Apple Silicon/Intel DMG 继续标记为 Preview。macOS 最低运行版本为 14。分子结构渲染仍处于禁用状态。 + +--- + +**SHA256** + +- `Pebrel-v1.8.1-windows-x64.zip`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-windows-x64-setup.exe`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.AppImage`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.deb`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.tar.gz`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-macos-arm64-preview.dmg`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-macos-x64-preview.dmg`: `PENDING FINAL BUILD` + ## 1.8.0 - 2026-09-14 ### English diff --git a/Cargo.lock b/Cargo.lock index bc0c95e..6a54cf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5087,7 +5087,7 @@ dependencies = [ [[package]] name = "nebula" -version = "1.8.0" +version = "1.8.1" dependencies = [ "ab_glyph_rasterizer", "aes-gcm", @@ -5174,7 +5174,7 @@ dependencies = [ [[package]] name = "nebula-completions" -version = "1.8.0" +version = "1.8.1" dependencies = [ "lscolors", "nu-ansi-term", @@ -5195,15 +5195,15 @@ dependencies = [ [[package]] name = "nebula-settings" -version = "1.8.0" +version = "1.8.1" [[package]] name = "nebula-split" -version = "1.8.0" +version = "1.8.1" [[package]] name = "nebula_config" -version = "1.8.0" +version = "1.8.1" dependencies = [ "log", "nebula_config_derive", @@ -5213,7 +5213,7 @@ dependencies = [ [[package]] name = "nebula_config_derive" -version = "1.8.0" +version = "1.8.1" dependencies = [ "log", "nebula_config", @@ -5230,7 +5230,7 @@ version = "0.6.0" [[package]] name = "nebula_terminal" -version = "1.8.0" +version = "1.8.1" dependencies = [ "base64", "bitflags 2.13.0", diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 5403b52..ecb2426 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -614,6 +614,9 @@ settings files. block competing installation/startup and cannot remain owned after a crash. Upgrade restore state and installation failures survive process exit. Inno in-place installation is not claimed to provide arbitrary mid-install rollback. + Native path normalization, process creation time and hidden helper spawning + belong to `platform::update_installation`; transaction state and commit authority + remain in the updater. UI startup consults the existing installation capability. - **Validation boundary:** Provider bridge execution, serialization/failure cases, actual GPUI lifecycle tests and native simulated upgrades are separate evidence. Metadata compilation alone does not validate restoration or installer behavior. @@ -621,3 +624,17 @@ settings files. it permits an exact-version reinstall, never a downgrade. Such a reinstall must replace the executable bytes; an unchanged executable after setup exits zero is a failure. This does not substitute for a full packaged upgrade test. + +## ADR-0017 — Optional release contributor section + +- **Status:** Approved by the maintainer on 2026-09-15. +- **Context:** A release without new PR contributors previously failed validation + unless it listed a contributor. This encouraged crediting maintainers or issue + reporters in the PR contributor area, contrary to the maintainer's policy. +- **Decision:** Omit Contributors when there are no PR contributors to credit. + When present, the section still requires GitHub links, occurs once after both + languages, and precedes SHA256. PR eligibility is checked against GitHub during + release review; the local Markdown checker cannot establish it from a link. +- **Validation:** A bilingual release without Contributors passes. Empty, + duplicated, unlinked or misplaced contributor sections fail; language and + asset checksum contracts remain required. diff --git a/docs/release-notes/v1.8.1.md b/docs/release-notes/v1.8.1.md new file mode 100644 index 0000000..9a0d1d6 --- /dev/null +++ b/docs/release-notes/v1.8.1.md @@ -0,0 +1,59 @@ +# Pebrel 1.8.1 + +## English + +### Added + +- Added a custom theme library with saved snapshots, a preview editor, and theme import/export. Saved themes can retain terminal colors, typography and window appearance alongside the built-in themes. +- Added optional background update downloads and an option to install a verified download on the next application launch on Windows. + +### Fixed + +- Restored the default terminal font to the exact Maple Mono Normal NF CN font bundled with 1.7.0 after the default changed in 1.8.0. Explicitly selected custom fonts remain available. Addresses [#148](https://github.com/Kuddev/nebula/issues/148). +- Fixed update checks and installer downloads ignoring the application's proxy configuration, including proxy exclusions and SOCKS connections. +- Fixed scheduled updates merging saved windows into one window. Update recovery retains each window's tabs and active tab; this does not change desktop position or window-size restoration. +- Fixed the Windows installer failing to reuse its registered installation directory when updating an existing installation. +- Restored the 1.7 tab-status alignment and prevented status glyphs from being clipped. + +### Improved + +- Improved Pi conversation recovery by retaining the native session ID and session file together with each pane's shell and working directory. Failed restoration keeps the original target available for retry instead of treating command submission as successful recovery. Addresses [#146](https://github.com/Kuddev/nebula/issues/146). +- Improved Windows update handoff: save workspace state before exit, wait for the old process to stop before installing, verify the installer, and retain failure details when installation or restart does not complete. +- Improved cleanup of terminals on Windows when panes close, including console resources and process-monitoring handles; retained font ownership while text rendering still needs it. + +Windows packages include a Pebrel installer and portable ZIP. Linux x64 packages and macOS Apple Silicon/Intel DMGs retain their Preview designation. macOS requires version 14 or later. Molecular structure rendering remains disabled. + +## 中文 + +### 新增 + +- 新增:自定义主题库,支持保存主题快照、预览编辑和主题导入导出;可在内置主题之外保存终端配色、字体排版及窗口外观。 +- 新增:Windows 可选的后台更新下载,以及在下次启动应用时安装已校验下载包的选项。 + +### 修复 + +- 修复:将 1.8.0 更换后的默认终端字体恢复为与 1.7.0 完全相同的内置 Maple Mono Normal NF CN;用户明确选择的自定义字体仍可使用。对应 [#148](https://github.com/Kuddev/nebula/issues/148)。 +- 修复:检查更新和下载安装器时忽略应用代理配置的问题,包括代理排除规则与 SOCKS 连接。 +- 修复:计划更新将已保存的多个窗口合并成一个窗口的问题;更新恢复时保留各窗口的标签及活动标签,不改变桌面位置或窗口尺寸的恢复行为。 +- 修复:Windows 安装器更新已有安装时未复用已注册安装目录的问题。 +- 修复:恢复 1.7 的标签状态对齐方式,避免状态符号被裁切。 + +### 改进 + +- 改进:Pi 会话恢复同时保留原生会话 ID、会话文件,以及各窗格的 Shell 和工作目录。恢复失败时保留原目标以便重试,不再仅凭恢复命令已提交就视为恢复成功。对应 [#146](https://github.com/Kuddev/nebula/issues/146)。 +- 改进:Windows 更新交接在退出前保存工作区,等待旧进程退出后再安装,校验安装包,并在安装或重启未完成时保留失败详情。 +- 改进:Windows 关闭窗格时释放控制台资源及进程监测句柄,并在文字渲染仍需使用字体时保持其有效。 + +Windows 提供 Pebrel 安装器和 ZIP 便携包;Linux x64 包以及 macOS Apple Silicon/Intel DMG 继续标记为 Preview。macOS 最低运行版本为 14。分子结构渲染仍处于禁用状态。 + +--- + +**SHA256** + +- `Pebrel-v1.8.1-windows-x64.zip`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-windows-x64-setup.exe`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.AppImage`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.deb`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-linux-x64-preview.tar.gz`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-macos-arm64-preview.dmg`: `PENDING FINAL BUILD` +- `Pebrel-v1.8.1-macos-x64-preview.dmg`: `PENDING FINAL BUILD` diff --git a/nebula-completions/Cargo.toml b/nebula-completions/Cargo.toml index d550f62..68c74d5 100644 --- a/nebula-completions/Cargo.toml +++ b/nebula-completions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula-completions" -version = "1.8.0" +version = "1.8.1" edition.workspace = true [features] diff --git a/nebula_app/Cargo.toml b/nebula_app/Cargo.toml index c03b6fd..8ad440e 100644 --- a/nebula_app/Cargo.toml +++ b/nebula_app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Pebrel — a GPU-accelerated terminal emulator" readme = "README.md" @@ -15,15 +15,15 @@ path = "src/main.rs" [dependencies.nebula_terminal] path = "../nebula_terminal" -version = "1.8.0" +version = "1.8.1" [dependencies.nebula_config_derive] path = "../nebula_config_derive" -version = "1.8.0" +version = "1.8.1" [dependencies.nebula_config] path = "../nebula_config" -version = "1.8.0" +version = "1.8.1" # 可选:GPUI UI 层(feature = "gpui-shell",模块 src/gpui_shell/)。 # GPUI/component 的版本和 Git 来源由根 Cargo.toml 统一精确固定。 @@ -67,7 +67,7 @@ optional = true [dependencies.nebula-completions] path = "../nebula-completions" -version = "1.8.0" +version = "1.8.1" [dependencies] ahash = { version = "0.8.6", features = ["no-rng"] } @@ -145,7 +145,7 @@ rusqlite = { version = "0.40.2", features = ["bundled"] } sha1 = "0.11.0" [build-dependencies] -nebula-settings = { path = "../nebula_settings", version = "1.8.0" } +nebula-settings = { path = "../nebula_settings", version = "1.8.1" } gl_generator = { version = "0.14.0", optional = true } serde = "1" serde_json = "1" diff --git a/nebula_app/src/gpui_shell/mod.rs b/nebula_app/src/gpui_shell/mod.rs index e89ef82..880fa1d 100644 --- a/nebula_app/src/gpui_shell/mod.rs +++ b/nebula_app/src/gpui_shell/mod.rs @@ -79,19 +79,21 @@ pub fn run_shell( initial_cwd: Option, initial_command: Option, ) { - #[cfg(windows)] - match crate::update_download::handoff::installation_in_progress() { - Ok(true) => { - log::info!("Installation is being updated; resident startup deferred"); - return; - }, - Err(error) => { - log::warn!("Could not inspect installation ownership: {error}"); - }, - Ok(false) => {}, + if crate::platform::CAPABILITIES.self_update_install { + match crate::update_download::handoff::installation_in_progress() { + Ok(true) => { + log::info!("Installation is being updated; resident startup deferred"); + return; + }, + Err(error) => { + log::warn!("Could not inspect installation ownership: {error}"); + }, + Ok(false) => {}, + } } - #[cfg(windows)] - if crate::update_download::handoff::apply_scheduled() { + if crate::platform::CAPABILITIES.self_update_install + && crate::update_download::handoff::apply_scheduled() + { return; } let (shell_tx, shell_rx) = std::sync::mpsc::channel(); diff --git a/nebula_app/src/gpui_shell/terminal/view/startup.rs b/nebula_app/src/gpui_shell/terminal/view/startup.rs index 0f691be..3f1d4be 100644 --- a/nebula_app/src/gpui_shell/terminal/view/startup.rs +++ b/nebula_app/src/gpui_shell/terminal/view/startup.rs @@ -106,12 +106,7 @@ impl TerminalView { .map_or(crate::display::SuggestEnv::Local, |distro| { crate::display::SuggestEnv::Wsl { distro: distro.to_owned() } }); - #[cfg(windows)] - let snapshot_shell = effective - .clone() - .or_else(|| Some(nebula_terminal::tty::resolved_default_shell())); - #[cfg(not(windows))] - let snapshot_shell = effective.clone(); + let snapshot_shell = crate::platform::shell::snapshot_shell(effective.clone()); let session_launch = snapshot_shell.as_ref().map_or( crate::session::LaunchSession::Default, |shell| crate::session::LaunchSession::Shell { diff --git a/nebula_app/src/platform/mod.rs b/nebula_app/src/platform/mod.rs index e262393..06074ef 100644 --- a/nebula_app/src/platform/mod.rs +++ b/nebula_app/src/platform/mod.rs @@ -30,6 +30,7 @@ pub(crate) mod process_snapshot; pub mod shell; pub mod shell_integration; pub mod startup; +pub(crate) mod update_installation; #[cfg(feature = "gpui-shell")] pub(crate) mod window_chrome; diff --git a/nebula_app/src/platform/shell.rs b/nebula_app/src/platform/shell.rs index 565fa57..c5d2a24 100644 --- a/nebula_app/src/platform/shell.rs +++ b/nebula_app/src/platform/shell.rs @@ -49,6 +49,19 @@ pub fn interactive_args(id: &str) -> Vec { } } +/// Preserve the actual Windows PTY default in a pane's durable launch snapshot. +/// Unix keeps an unspecified shell unspecified so the login-shell policy applies. +pub(crate) fn snapshot_shell( + configured: Option, +) -> Option { + #[cfg(windows)] + { + configured.or_else(|| Some(nebula_terminal::tty::resolved_default_shell())) + } + #[cfg(not(windows))] + configured +} + #[cfg(target_os = "macos")] const fn default_unix_shell_id() -> &'static str { "zsh" diff --git a/nebula_app/src/platform/update_installation.rs b/nebula_app/src/platform/update_installation.rs new file mode 100644 index 0000000..465e8fd --- /dev/null +++ b/nebula_app/src/platform/update_installation.rs @@ -0,0 +1,72 @@ +//! Native paths and process identity for the Windows installer handoff. +//! Transaction persistence and commit authority remain in `update_download`. +use std::io; +use std::path::{Path, PathBuf}; +use std::process::Child; + +pub(crate) fn canonical(path: &Path) -> io::Result { + let path = std::fs::canonicalize(path)?; + #[cfg(windows)] + { + let text = path.to_string_lossy(); + if let Some(unc) = text.strip_prefix(r"\\?\UNC\") { + return Ok(PathBuf::from(format!(r"\\{unc}"))); + } + if let Some(local) = text.strip_prefix(r"\\?\") { + return Ok(PathBuf::from(local)); + } + } + Ok(path) +} + +/// FILETIME identity is compared with the exact process handle by the helper. +pub(crate) fn current_process_created() -> io::Result { + #[cfg(windows)] + { + use windows_sys::Win32::Foundation::FILETIME; + use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes}; + let mut created: FILETIME = unsafe { std::mem::zeroed() }; + let mut exited = created; + let mut kernel = created; + let mut user = created; + // SAFETY: the pseudo-handle is valid and each output is a live FILETIME. + if unsafe { + GetProcessTimes(GetCurrentProcess(), &mut created, &mut exited, &mut kernel, &mut user) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok((u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime)) + } + #[cfg(not(windows))] + Err(io::Error::new(io::ErrorKind::Unsupported, "Windows process identity is unavailable")) +} + +pub(crate) fn spawn_helper(helper: &Path, plan: &Path) -> io::Result { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + use std::process::{Command, Stdio}; + let powershell = std::env::var_os("SystemRoot") + .map(PathBuf::from) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Windows directory is unavailable") + })? + .join("System32/WindowsPowerShell/v1.0/powershell.exe"); + Command::new(powershell) + .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"]) + .arg(helper) + .arg("-PlanPath") + .arg(plan) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(0x0800_0000) + .spawn() + } + #[cfg(not(windows))] + { + let _ = (helper, plan); + Err(io::Error::new(io::ErrorKind::Unsupported, "Windows installation is unavailable")) + } +} diff --git a/nebula_app/src/update_download/handoff.rs b/nebula_app/src/update_download/handoff.rs index 876ed28..ef703d0 100644 --- a/nebula_app/src/update_download/handoff.rs +++ b/nebula_app/src/update_download/handoff.rs @@ -2,11 +2,12 @@ //! Files under one transaction are durable evidence; only commit authorizes setup. use std::io::{self, Read as _}; use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::process::Child; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; +use crate::platform::update_installation::{canonical, current_process_created, spawn_helper}; use crate::session::Session; use crate::update_check::UpdateAsset; @@ -75,21 +76,6 @@ impl Drop for PreparedUpdate { } } -fn canonical(path: &Path) -> io::Result { - let path = std::fs::canonicalize(path)?; - #[cfg(windows)] - { - let text = path.to_string_lossy(); - if let Some(unc) = text.strip_prefix(r"\\?\UNC\") { - return Ok(PathBuf::from(format!(r"\\{unc}"))); - } - if let Some(local) = text.strip_prefix(r"\\?\") { - return Ok(PathBuf::from(local)); - } - } - Ok(path) -} - fn guard_base(executable: &Path) -> PathBuf { // All configurations of the same installed binary share this lock. A // settings-directory override must not bypass an installation in progress. @@ -112,124 +98,84 @@ pub(crate) fn prepare(asset: &UpdateAsset) -> Result { .into(), ); } - #[cfg(not(windows))] - { - let _ = asset; + if !crate::platform::CAPABILITIES.self_update_install { return Err("In-app installation is unavailable on this platform".into()); } - #[cfg(windows)] - { - use std::os::windows::process::CommandExt as _; - use windows_sys::Win32::Foundation::FILETIME; - use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes}; - - let installer = super::ready_path(asset)?; - let executable = canonical(&std::env::current_exe().map_err(|error| error.to_string())?) - .map_err(|error| error.to_string())?; - let installation = - executable.parent().ok_or("Application directory is missing")?.to_owned(); - if !installation.join("unins000.exe").is_file() { + let installer = super::ready_path(asset)?; + let executable = canonical(&std::env::current_exe().map_err(|error| error.to_string())?) + .map_err(|error| error.to_string())?; + let installation = executable.parent().ok_or("Application directory is missing")?.to_owned(); + if !installation.join("unins000.exe").is_file() { + return Err("This copy is portable. Use the download page to replace its package.".into()); + } + let transaction = format!( + "{}-{}", + std::process::id(), + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() + ); + let directory = nebula_settings::settings_dir().join("updates/handoffs").join(&transaction); + std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?; + // Detect an unwritable target while all windows are still available. + let probe = installation.join(format!(".pebrel-update-{transaction}")); + let probe_file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe) + .map_err(|error| format!("Installation directory is not writable: {error}"))?; + drop(probe_file); + std::fs::remove_file(probe).map_err(|error| error.to_string())?; + let created = current_process_created().map_err(|error| error.to_string())?; + let base = guard_base(&executable); + let plan = Plan { + schema: 1, + asset: asset.clone(), + transaction: transaction.clone(), + installation, + executable, + config_directory: canonical(&nebula_settings::settings_dir()) + .map_err(|error| error.to_string())?, + installer: canonical(&installer).map_err(|error| error.to_string())?, + sha256: asset.sha256.clone().ok_or("Missing package digest")?, + bytes: std::fs::metadata(installer).map_err(|error| error.to_string())?.len(), + version: asset.version.clone(), + original_version: env!("CARGO_PKG_VERSION").into(), + guard_path: base.with_extension("nebula-lock"), + participants: vec![Participant { pid: std::process::id(), created: created.to_string() }], + }; + let plan_path = directory.join("plan.json"); + crate::atomic_file::write( + &plan_path, + &serde_json::to_vec(&plan).map_err(|error| error.to_string())?, + ) + .map_err(|error| error.to_string())?; + crate::atomic_file::write( + &nebula_settings::settings_dir().join("updates/last-handoff.json"), + &serde_json::to_vec(&plan_path).map_err(|error| error.to_string())?, + ) + .map_err(|error| error.to_string())?; + let helper = directory.join("handoff.ps1"); + crate::atomic_file::write(&helper, include_bytes!("handoff.ps1")) + .map_err(|error| error.to_string())?; + let child = spawn_helper(&helper, &plan_path).map_err(|error| error.to_string())?; + let mut prepared = PreparedUpdate { directory, transaction, child, committed: false }; + let deadline = Instant::now() + Duration::from_secs(30); + loop { + if prepared.child.try_wait().map_err(|error| error.to_string())?.is_some() { + let result = read_json::(&prepared.directory.join("result.json")) + .and_then(|result| result["error"].as_str().map(str::to_owned)); return Err( - "This copy is portable. Use the download page to replace its package.".into() + result.unwrap_or_else(|| "The update helper could not prepare installation".into()) ); } - let transaction = format!( - "{}-{}", - std::process::id(), - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() - ); - let directory = nebula_settings::settings_dir().join("updates/handoffs").join(&transaction); - std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?; - // Detect an unwritable target while all windows are still available. - let probe = installation.join(format!(".pebrel-update-{transaction}")); - let probe_file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&probe) - .map_err(|error| format!("Installation directory is not writable: {error}"))?; - drop(probe_file); - std::fs::remove_file(probe).map_err(|error| error.to_string())?; - let mut created: FILETIME = unsafe { std::mem::zeroed() }; - let mut exited = created; - let mut kernel = created; - let mut user = created; - if unsafe { - GetProcessTimes(GetCurrentProcess(), &mut created, &mut exited, &mut kernel, &mut user) - } == 0 + if let Some(ready) = read_json::(&prepared.directory.join("ready.json")) + && ready["transaction"] == prepared.transaction { - return Err(io::Error::last_os_error().to_string()); + return Ok(prepared); } - let created = (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime); - let base = guard_base(&executable); - let plan = Plan { - schema: 1, - asset: asset.clone(), - transaction: transaction.clone(), - installation, - executable, - config_directory: canonical(&nebula_settings::settings_dir()) - .map_err(|error| error.to_string())?, - installer: canonical(&installer).map_err(|error| error.to_string())?, - sha256: asset.sha256.clone().ok_or("Missing package digest")?, - bytes: std::fs::metadata(installer).map_err(|error| error.to_string())?.len(), - version: asset.version.clone(), - original_version: env!("CARGO_PKG_VERSION").into(), - guard_path: base.with_extension("nebula-lock"), - participants: vec![Participant { - pid: std::process::id(), - created: created.to_string(), - }], - }; - let plan_path = directory.join("plan.json"); - crate::atomic_file::write( - &plan_path, - &serde_json::to_vec(&plan).map_err(|error| error.to_string())?, - ) - .map_err(|error| error.to_string())?; - crate::atomic_file::write( - &nebula_settings::settings_dir().join("updates/last-handoff.json"), - &serde_json::to_vec(&plan_path).map_err(|error| error.to_string())?, - ) - .map_err(|error| error.to_string())?; - let helper = directory.join("handoff.ps1"); - crate::atomic_file::write(&helper, include_bytes!("handoff.ps1")) - .map_err(|error| error.to_string())?; - let powershell = std::env::var_os("SystemRoot") - .map(PathBuf::from) - .ok_or("Windows directory is unavailable")? - .join("System32/WindowsPowerShell/v1.0/powershell.exe"); - let child = Command::new(powershell) - .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"]) - .arg(helper) - .arg("-PlanPath") - .arg(plan_path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .creation_flags(0x0800_0000) - .spawn() - .map_err(|error| error.to_string())?; - let mut prepared = PreparedUpdate { directory, transaction, child, committed: false }; - let deadline = Instant::now() + Duration::from_secs(30); - loop { - if prepared.child.try_wait().map_err(|error| error.to_string())?.is_some() { - let result = - read_json::(&prepared.directory.join("result.json")) - .and_then(|result| result["error"].as_str().map(str::to_owned)); - return Err(result - .unwrap_or_else(|| "The update helper could not prepare installation".into())); - } - if let Some(ready) = - read_json::(&prepared.directory.join("ready.json")) - && ready["transaction"] == prepared.transaction - { - return Ok(prepared); - } - if Instant::now() >= deadline { - return Err("The update helper did not become ready".into()); - } - std::thread::sleep(Duration::from_millis(50)); + if Instant::now() >= deadline { + return Err("The update helper did not become ready".into()); } + std::thread::sleep(Duration::from_millis(50)); } } diff --git a/nebula_app/windows/nebula.rc b/nebula_app/windows/nebula.rc index 5ed706b..0fc8ff1 100644 --- a/nebula_app/windows/nebula.rc +++ b/nebula_app/windows/nebula.rc @@ -10,8 +10,8 @@ APP_MANIFEST RT_MANIFEST nebula.manifest #endif 1 VERSIONINFO -FILEVERSION 1,8,0,0 -PRODUCTVERSION 1,8,0,0 +FILEVERSION 1,8,1,0 +PRODUCTVERSION 1,8,1,0 FILEFLAGSMASK 0x3fL FILEFLAGS 0x0L FILEOS 0x40004L @@ -24,11 +24,11 @@ BEGIN BEGIN VALUE "CompanyName", "Kuddev" VALUE "FileDescription", "Pebrel" - VALUE "FileVersion", "1.8.0" + VALUE "FileVersion", "1.8.1" VALUE "InternalName", "nebula" VALUE "OriginalFilename", "pebrel.exe" VALUE "ProductName", "Pebrel" - VALUE "ProductVersion", "1.8.0" + VALUE "ProductVersion", "1.8.1" END END BLOCK "VarFileInfo" diff --git a/nebula_config/Cargo.toml b/nebula_config/Cargo.toml index dc15094..d9d451f 100644 --- a/nebula_config/Cargo.toml +++ b/nebula_config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula_config" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Nebula configuration abstractions" homepage = "https://github.com/Kuddev/pebrel" @@ -14,5 +14,5 @@ serde = "1.0.163" toml.workspace = true [dev-dependencies] -nebula_config_derive = { version = "1.8.0", path = "../nebula_config_derive" } +nebula_config_derive = { version = "1.8.1", path = "../nebula_config_derive" } serde = { version = "1.0.163", features = ["derive"] } diff --git a/nebula_config_derive/Cargo.toml b/nebula_config_derive/Cargo.toml index 2828977..a4d3d6b 100644 --- a/nebula_config_derive/Cargo.toml +++ b/nebula_config_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula_config_derive" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Failure resistant deserialization derive" homepage = "https://github.com/Kuddev/pebrel" @@ -18,7 +18,7 @@ syn = { version = "2.0.16", features = ["derive", "parsing", "proc-macro", "prin [dev-dependencies.nebula_config] path = "../nebula_config" -version = "1.8.0" +version = "1.8.1" [dev-dependencies] log = "0.4.11" diff --git a/nebula_settings/Cargo.toml b/nebula_settings/Cargo.toml index c7c29f3..d8c2206 100644 --- a/nebula_settings/Cargo.toml +++ b/nebula_settings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula-settings" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Shared reader for Nebula runtime user settings (nebula_settings.txt) and theme terminal-color tables" publish = false diff --git a/nebula_split/Cargo.toml b/nebula_split/Cargo.toml index f57f9fe..87ebcad 100644 --- a/nebula_split/Cargo.toml +++ b/nebula_split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula-split" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Shared split-pane rules: layout tree, geometry, divider drag and directional focus" publish = false diff --git a/nebula_terminal/Cargo.toml b/nebula_terminal/Cargo.toml index b712354..050fafe 100644 --- a/nebula_terminal/Cargo.toml +++ b/nebula_terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nebula_terminal" -version = "1.8.0" +version = "1.8.1" license = "GPL-3.0-or-later" description = "Nebula terminal emulation core" readme = "../README.md" diff --git a/scripts/installer.iss b/scripts/installer.iss index bc5c574..f3bc60e 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -1,9 +1,9 @@ #ifndef AppVersion - #define AppVersion "1.8.0" + #define AppVersion "1.8.1" #endif #ifndef NumericVersion - #define NumericVersion "1.8.0.0" + #define NumericVersion "1.8.1.0" #endif #ifndef Configuration diff --git a/scripts/platform_cfg_budget.txt b/scripts/platform_cfg_budget.txt index 629f34c..f52aaac 100644 --- a/scripts/platform_cfg_budget.txt +++ b/scripts/platform_cfg_budget.txt @@ -1 +1 @@ -485 +484 diff --git a/scripts/stable_release.py b/scripts/stable_release.py index 7956364..4ef9927 100644 --- a/scripts/stable_release.py +++ b/scripts/stable_release.py @@ -121,7 +121,7 @@ def validate_notes(source: Path, version: str) -> str: notes = source.read_text(encoding="utf-8").strip() if not notes.startswith(f"# Pebrel {version}\n"): raise StableReleaseError("stable release notes have the wrong title") - for heading in ("## English", "## 中文", "## Contributors"): + for heading in ("## English", "## 中文"): if notes.splitlines().count(heading) != 1: raise StableReleaseError(f"stable release notes require one {heading} section") checksum_headings = ("\n## SHA256\n", "\n**SHA256**\n") @@ -130,9 +130,12 @@ def validate_notes(source: Path, version: str) -> str: raise StableReleaseError("stable release notes require one SHA256 section") english_start = notes.index("\n## English\n") chinese_start = notes.index("\n## 中文\n") - contributors_start = notes.index("\n## Contributors\n") checksum_start = notes.index(present_checksums[0]) - if not english_start < chinese_start < contributors_start < checksum_start: + contributor_count = notes.splitlines().count("## Contributors") + if contributor_count > 1: + raise StableReleaseError("stable release notes allow at most one Contributors section") + contributors_start = notes.index("\n## Contributors\n") if contributor_count else checksum_start + if not english_start < chinese_start < contributors_start <= checksum_start: raise StableReleaseError("stable release notes sections are out of order") pairs = (("Added", "新增"), ("Fixed", "修复"), ("Improved", "改进")) english = notes[english_start:chinese_start] @@ -142,7 +145,7 @@ def validate_notes(source: Path, version: str) -> str: ): raise StableReleaseError("stable release notes need matching bilingual change categories") contributors = notes[contributors_start:checksum_start] - if not re.search(r"github\.com/[^)]+", contributors, flags=re.IGNORECASE): + if contributor_count and not re.search(r"github\.com/[^)]+", contributors, flags=re.IGNORECASE): raise StableReleaseError("stable release notes must identify contributors with GitHub links") checksum_body = notes[checksum_start:].strip() if STABLE_SHA256_PLACEHOLDER not in checksum_body: diff --git a/scripts/tests/test_stable_release.py b/scripts/tests/test_stable_release.py index 0163fcf..55b3814 100644 --- a/scripts/tests/test_stable_release.py +++ b/scripts/tests/test_stable_release.py @@ -183,6 +183,27 @@ class StableReleaseTests(unittest.TestCase): with self.assertRaisesRegex(StableReleaseError, "GitHub links"): validate_notes(source, VERSION) + def test_notes_allow_no_pr_contributors_but_keep_section_contracts(self) -> None: + before, rest = notes().split("## Contributors\n", 1) + contributor_body, after = rest.split("## SHA256\n", 1) + without_contributors = before + "## SHA256\n" + after + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "notes.md" + source.write_text(without_contributors, encoding="utf-8") + self.assertEqual(validate_notes(source, VERSION), without_contributors) + invalid = ( + (before + "## Contributors\n\n## SHA256\n" + after, "GitHub links"), + (notes().replace("## Contributors", "## Contributors\n\n## Contributors"), "at most one"), + (without_contributors + "\n## Contributors\n" + contributor_body, "out of order"), + (without_contributors.replace("### 新增", "### 修复"), "matching bilingual"), + (without_contributors.replace("## 中文", "## Chinese"), "require one"), + ) + for body, error in invalid: + with self.subTest(error=error): + source.write_text(body, encoding="utf-8") + with self.assertRaisesRegex(StableReleaseError, error): + validate_notes(source, VERSION) + def test_changelog_must_list_the_same_stable_asset_names(self) -> None: with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "CHANGELOG.md" diff --git a/tools/i18n-contract/Cargo.lock b/tools/i18n-contract/Cargo.lock index f055b79..da0db86 100644 --- a/tools/i18n-contract/Cargo.lock +++ b/tools/i18n-contract/Cargo.lock @@ -16,7 +16,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "nebula-settings" -version = "1.8.0" +version = "1.8.1" [[package]] name = "pebrel-i18n-contract"