diff --git a/agents.md b/agents.md index 4913b68..c780cb3 100644 --- a/agents.md +++ b/agents.md @@ -170,6 +170,7 @@ tests/scripts/pdf-recovery.sh 真窗口 ElyPDF:加锁文件的封存恢复点 tests/scripts/autosave.sh 真窗口 ElyDoc:自动保存开关、停手即写、外部改动不覆盖(需辅助功能权限) tests/scripts/print.sh 真窗口 ElyDoc:打印用文档自己的纸张,取消不动文档(需辅助功能权限) tests/scripts/show-fullscreen.sh 真窗口 ElyShow:F5 放映的窗口盖住整块屏幕(菜单栏与程序坞之上,需辅助功能权限) +tests/scripts/windows-installer.sh 假应用的安装程序:所选目录原有的文件经两次安装与卸载都在,旧版按注册表清理,文件被占用或程序在运行时什么也不改,写不进时放回旧版(缺 makensis 时 SKIP) tests/scripts/types.sh packaging/types.tsv 的文件类型在 Linux 桌面项、Windows 安装程序与 macOS 文档类型里一致,从不设为默认 tests/scripts/layout.sh 真窗口:六个应用在 640、800、1100 宽下无元素越出窗口左右,留截图(需辅助功能与屏幕录制权限) tests/scripts/ooxml.py <用例> <文件> 不经 LibreOffice 读 DOCX、XLSX、PPTX 并按自算的预期核对(往返用例调用) @@ -225,7 +226,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1)。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。 - 2026-10-07:ElyTree 储藏、远程、工作树、子模块、LFS(C13.5,TREE-10 至 12,§13.3)。左栏第四页“仓库”:上方取回(`fetch --progress --all`)、拉取(`pull --progress`,照用户的 pull 设置)、推送、强制推送…;储藏(`stash list -z`,新的在前;“储藏改动…”问说明与是否包括未跟踪的文件,`stash push`),远程(名称与地址,`remote` 加 `config --get-regexp` 读出取回与推送地址),工作树(`worktree list --porcelain -z`,行名为文件夹名,有失效的可“清理”即 `worktree prune`),子模块(索引里的 gitlink 与文件夹自身 `.git` 的 HEAD:未检出、在记录的提交上、在别的提交上、冲突),LFS 文件(`git lfs ls-files --long --json`:工作树里是内容、对象在本机而工作树是指针、只有指针而对象不在本机);没有子模块或 LFS 文件时不列。各列表分别读出,一个读不出只在该处显示 Git 原话;切到此页时读,此后每次状态重读时重读,只取最新一次。选中一项,中间显示:储藏为提交详情的同一套(`hist::Picked`,历史与储藏共用读取与面板):改动的文件(对第一个父提交)与储藏的未跟踪文件(第三个父提交,各自对空树),选文件看差异,上方应用、弹出、删除…;远程为地址与推送地址,取回、修改…(改名 `remote rename`、改地址 `remote set-url`)、移除…(先问);工作树为路径与分支或提交、锁定与失效原因,打开(作为标签)、移除…(先问,主工作树与当前的不给);子模块为记录的与检出的提交,“检出记录的提交”(`submodule update --init`,经远程运行)与打开。用户输入的名称、地址、路径前一律加 `--`(推送的远程名同样)。表单与选择对话框(新建分支、标签、改名、身份、储藏、远程、工作树、选远程与上游)记下打开时的仓库,提交时写进那个仓库,不随当前标签变;储藏列表按每条三个字段读(说明可为空)。储藏的应用、弹出、删除按 `stash@{n}` 执行,执行前在写队列里核对它仍是所见的那个储藏(另有储藏插入即拒绝,什么也不做);弹出遇冲突时 Git 保留储藏,对话框显示 Git 原话。删除储藏、移除远程与工作树每次都问。分支页的本地分支多出“设置上游…”(列出远程分支,`branch --set-upstream-to=refs/remotes/名`)与“取消上游”。推送:有上游即 `push --progress`;没有上游时后台读出远程,问推到哪个远程(`push --set-upstream -- 远程 refs/heads/分支:refs/heads/分支`),没有远程或 HEAD 游离即说明。强制推送(§13.3):后台读出当前分支、上游的远程与远端分支、上次取回时远端的提交(租约)、此刻 HEAD 的提交(源)与远端有而源没有的提交,对话框写明远程、分支与将被覆盖的提交(或写明没有),确认后推送的就是所示的源提交(`push --force-with-lease=远端分支:租约提交 -- 远程 源提交:远端分支`),确认前本地分支再动也不改变推送内容;远端自上次取回后变了即被拒绝;没有上游或游离即说明不能。远程命令(取回、拉取、推送、子模块更新)是写队列里的一种(`Write::Remote`),经 `running::Running` 运行:Git 在自己的会话里(`setsid`,SSH 找不到终端,不会挂在终端提示上;凭据照用 credential helper 与 ssh-agent),进度逐行显示在中间上方的提示条,可停止(向整个进程组发 SIGTERM,Git 自行清理;已结束的不再发);用户停止时提示“已停止”、清掉其后的排队写,不当作失败;失败时对话框显示 Git 的 stdout 与 stderr(以回车结尾的进度行被下一行改写,只留最后一次;以换行结尾的行全部保留,钩子的每一行都在);拉取遇冲突照合并类命令判定为停下。关窗与退出时停下所有远程命令与克隆。克隆改用同一运行器。真窗口(macOS):仓库页五类列表、储藏详情与文件、工作树详情、分支行。未验证:Windows 与 Linux(停止远程命令在非 Unix 上报未构建);真实网络与 HTTPS、SSH 凭据;LFS 对象的下载(只显示状态)。 - 2026-10-07(用户定):跟随组件库最新的 main。组件库 K01 把 gpui 从 zed 仓库的提交换成 crates.io 的 `gpui-pre` =0.3.8(`gpui-pre-platform` 同版本),K02 让 Tab 绑在 FocusScope 的 ElyFocus 上下文下、更深的上下文先得;本项目工作区的 `gpui`、`gpui_platform` 随之改为这两个包(以 `package =` 保留原名,源码不变),组件库 pin 到 dabc1eb(其上加本项目一个提交:树与树行的无障碍角色)。升级后全工作区编译无改动。 -- 2026-10-08:首版打包(R1.1)。图标(用户定:logo-design 技能出方案,选 C“实心几何”;macOS 标准 squircle,Linux 与 Windows 无底形,八个同一套设计):母版在 `assets/icons`(我按圆角矩形、圆与 30°/45° 斜线在 256 网格上画;每个颜色对白与 `#1E1E1E` 都不低于 3:1,ElyMark 原定的石板灰在深色上 1.75:1,改为琥珀 `#B87A00`;ElyTable 在 48 像素以下用简化的一版),`packaging/icons/build.sh` 以 AppKit 渲染出 icns(macOS)、512 像素 png(Linux 的 AppImage 与桌面文件)、16 至 256 像素 ico(Windows 安装程序、开始菜单快捷方式与卸载项);生成物入库,Linux 与 Windows 构建不需要 macOS。系统交给应用的文件:`ely_ui::run` 收下启动参数与 `on_open_urls`(访达的打开请求,启动前到达的也在内),应用先取 `launch_files`(有则不开空白文档;ElyMark 有则不恢复上次的标签),再以 `on_open` 处理运行中的请求;六个应用都走这一条(ElyDoc、ElyTable、ElyShow 原先不收启动参数)。macOS 包:LOK 应用只带内核,按 LibreOffice 自己的布局并入本应用:内核的库进 `Contents/Frameworks`(合并库构建留下的“invalid - merged lib”占位文件不拷,其余非代码文件即报错),内核的资料进 `Contents/Resources`,另有 `MacOS/ely-lok-worker`;`install_dir` 先找包内(`Frameworks/libmergedlo.dylib`)。不带 `LibreOffice.app` 本身:原先整份嵌入时,其快速查看与聚焦插件、`soffice` 等启动程序与文档类型被系统登记,系统设置里出现“LibreOffice 扩展”(用户发现;已注销本机七处登记);测试也不再运行 LibreOffice 应用(样本改为 `tests/fixtures/office` 的固定文件,运行开发构建的 `soffice` 会让系统重新登记它)。ElyPDF 带 `MacOS/ely-pdf-worker` 与 `Resources/tessdata`(三种语言),OCR 数据先找应用旁再找包内;ElyTree 带 `Resources/git`,其中每个 Mach-O 先签(Git 以 `INSTALL_SYMLINKS` 构建,内建命令是指向同一程序的相对链接;原先是硬链接,拷进包后变成约 150 份副本、689 MB;以 `MACOSX_DEPLOYMENT_TARGET=14.0` 构建,原先随本机 SDK 要求 macOS 27);ElyMark 用同级 ElyTree 包里的 Git(2026-10-15 起 Windows 同此:`git::own` 先找本应用旁的,再按 `apps` 的同级查找与 macOS 的登记找 ElyTree 旁的,都没有即 `git::NoGit`,ElyMark 的 Git 项写明要装 ElyTree;Windows 的 ElyMark 原随带一份,用户问起后去掉。Linux 的 ElyMark 照带:AppImage 自成一体,找不到别的映像里的 Git,deb、rpm、Arch 包与它同一 AppDir,R6.6)。Windows 安装程序装前删去上一版的文件(与卸载同一清单 `ElyRemoveFiles`,只在目录里已有本应用的程序时),升级后不留旧的 `git` 与 CEF 文件(R6.11)。文档类型(`packaging/macos/documents.sh`)一律 `Alternate`,不抢系统默认:Doc 为 docx、odt、doc、rtf;Table 为 xlsx、ods、xls、csv、tsv;Show 为 pptx、odp、ppt;PDF;Markdown;Tree 为文件夹(查看者)。worker、内核、OCR 数据的所在路径都写日志,`tests/scripts/release-macos.sh` 据此核对都在包内。许可(§21.2、§21.3):每个包的 `Resources/Licenses` 有 Ely 的 AGPL 全文(仓库根 `LICENSE`)、`rust-crates.txt`(`packaging/crates.py`:本机目标下链进各二进制的全部 crate 与其许可文件,无许可文件的取 `packaging/licenses` 里该表达式的标准文本并写明作者,都没有即失败)与所带引擎的许可:LibreOffice 的 LICENSE、NOTICE、CREDITS;MuPDF 的 COPYING 与其编进去的每个第三方及每组内嵌字体(按构建实际嵌入的)的许可、OpenSSL、tessdata(Apache-2.0,按提交与散列取)、Great Vibes 的 OFL;Git 的 COPYING 与 LGPL-2.1、git-lfs 的 LICENSE.md(发布包里没有,按标签与散列取);ElyMark 带 RaTeX 嵌入的 KaTeX 字体的 OFL 与说明;缺任何一份即打包失败(`packaging/licenses.sh`)。`release.sh` 另出 `ElyOffice-<版本>-source.tar.gz`:自有源码、锁文件、上游补丁与构建脚本(上游各自钉在提交,取得方式即其中的 fetch 脚本)。ElyTable 同时收到多个文本文件(访达一次交来多份 CSV)时,读完的导入排队,没有对话框开着才逐个询问(原先后到的一份替换前一份,前一份静默丢失)。`packaging/macos/release.sh` 按 `apps.tsv` 的批次出各应用的 dmg;本机只有 Apple Development 证书,无 Developer ID,未公证。未验证:Intel 包(只建了 arm64)、公证、Linux 与 Windows 的包(本机不能建)。 +- 2026-10-08:首版打包(R1.1)。图标(用户定:logo-design 技能出方案,选 C“实心几何”;macOS 标准 squircle,Linux 与 Windows 无底形,八个同一套设计):母版在 `assets/icons`(我按圆角矩形、圆与 30°/45° 斜线在 256 网格上画;每个颜色对白与 `#1E1E1E` 都不低于 3:1,ElyMark 原定的石板灰在深色上 1.75:1,改为琥珀 `#B87A00`;ElyTable 在 48 像素以下用简化的一版),`packaging/icons/build.sh` 以 AppKit 渲染出 icns(macOS)、512 像素 png(Linux 的 AppImage 与桌面文件)、16 至 256 像素 ico(Windows 安装程序、开始菜单快捷方式与卸载项);生成物入库,Linux 与 Windows 构建不需要 macOS。系统交给应用的文件:`ely_ui::run` 收下启动参数与 `on_open_urls`(访达的打开请求,启动前到达的也在内),应用先取 `launch_files`(有则不开空白文档;ElyMark 有则不恢复上次的标签),再以 `on_open` 处理运行中的请求;六个应用都走这一条(ElyDoc、ElyTable、ElyShow 原先不收启动参数)。macOS 包:LOK 应用只带内核,按 LibreOffice 自己的布局并入本应用:内核的库进 `Contents/Frameworks`(合并库构建留下的“invalid - merged lib”占位文件不拷,其余非代码文件即报错),内核的资料进 `Contents/Resources`,另有 `MacOS/ely-lok-worker`;`install_dir` 先找包内(`Frameworks/libmergedlo.dylib`)。不带 `LibreOffice.app` 本身:原先整份嵌入时,其快速查看与聚焦插件、`soffice` 等启动程序与文档类型被系统登记,系统设置里出现“LibreOffice 扩展”(用户发现;已注销本机七处登记);测试也不再运行 LibreOffice 应用(样本改为 `tests/fixtures/office` 的固定文件,运行开发构建的 `soffice` 会让系统重新登记它)。ElyPDF 带 `MacOS/ely-pdf-worker` 与 `Resources/tessdata`(三种语言),OCR 数据先找应用旁再找包内;ElyTree 带 `Resources/git`,其中每个 Mach-O 先签(Git 以 `INSTALL_SYMLINKS` 构建,内建命令是指向同一程序的相对链接;原先是硬链接,拷进包后变成约 150 份副本、689 MB;以 `MACOSX_DEPLOYMENT_TARGET=14.0` 构建,原先随本机 SDK 要求 macOS 27);ElyMark 用同级 ElyTree 包里的 Git(2026-10-15 起 Windows 同此:`git::own` 先找本应用旁的,再按 `apps` 的同级查找与 macOS 的登记找 ElyTree 旁的,都没有即 `git::NoGit`,ElyMark 的 Git 项写明要装 ElyTree;Windows 的 ElyMark 原随带一份,用户问起后去掉。Linux 的 ElyMark 照带:AppImage 自成一体,找不到别的映像里的 Git,deb、rpm、Arch 包与它同一 AppDir,R6.6)。Windows 安装程序只动自己装下的:装好后写下所装的顶层名(`.ely-files`,由 `packaging/windows/files.sh` 按分装目录生成),升级与卸载只移去清单里的;清单之前的版本只在注册表记下的安装目录里按旧的通配清单移去;先查清单里的文件能否写(运行中或被占用即不能),再整个挪进 `.ely-old`,任一项挪不动即放回、什么也不改、退出码 2,新文件写不进即删去本次新建的、放回旧版;升级后不留旧的 `git` 与 CEF 文件(R6.11,`tests/scripts/windows-installer.sh`)。文档类型(`packaging/macos/documents.sh`)一律 `Alternate`,不抢系统默认:Doc 为 docx、odt、doc、rtf;Table 为 xlsx、ods、xls、csv、tsv;Show 为 pptx、odp、ppt;PDF;Markdown;Tree 为文件夹(查看者)。worker、内核、OCR 数据的所在路径都写日志,`tests/scripts/release-macos.sh` 据此核对都在包内。许可(§21.2、§21.3):每个包的 `Resources/Licenses` 有 Ely 的 AGPL 全文(仓库根 `LICENSE`)、`rust-crates.txt`(`packaging/crates.py`:本机目标下链进各二进制的全部 crate 与其许可文件,无许可文件的取 `packaging/licenses` 里该表达式的标准文本并写明作者,都没有即失败)与所带引擎的许可:LibreOffice 的 LICENSE、NOTICE、CREDITS;MuPDF 的 COPYING 与其编进去的每个第三方及每组内嵌字体(按构建实际嵌入的)的许可、OpenSSL、tessdata(Apache-2.0,按提交与散列取)、Great Vibes 的 OFL;Git 的 COPYING 与 LGPL-2.1、git-lfs 的 LICENSE.md(发布包里没有,按标签与散列取);ElyMark 带 RaTeX 嵌入的 KaTeX 字体的 OFL 与说明;缺任何一份即打包失败(`packaging/licenses.sh`)。`release.sh` 另出 `ElyOffice-<版本>-source.tar.gz`:自有源码、锁文件、上游补丁与构建脚本(上游各自钉在提交,取得方式即其中的 fetch 脚本)。ElyTable 同时收到多个文本文件(访达一次交来多份 CSV)时,读完的导入排队,没有对话框开着才逐个询问(原先后到的一份替换前一份,前一份静默丢失)。`packaging/macos/release.sh` 按 `apps.tsv` 的批次出各应用的 dmg;本机只有 Apple Development 证书,无 Developer ID,未公证。未验证:Intel 包(只建了 arm64)、公证、Linux 与 Windows 的包(本机不能建)。 - 2026-10-08(用户定):深色主题不用纯黑或近黑,底色取柔和的灰 `#252525`。组件库 fd0efc7:深色色板 bg `#252525`、surface `#2c2c2c`、sunken `#202020`、overlay `#323232`、hover `#383838`、active 与 border `#3f3f3f`、border_strong `#555555`;为让说明文字在较亮的 hover 与 active 上仍有 4.5:1,muted 提到 `#c4c2c0`、subtle 与代码注释提到 `#acaaa8`;色盲友好图表的紫与橙改为 `#9d8dec`、`#ffd93d`(3:1 且各色两两分得开)。`ely.xcd` 的深色方案随之:页面 `#2c2c2c`、页外 `#252525`、边界与网格 `#555555`、域底纹 `#3f3f3f`。跨应用同步外观、设置里的浅色/深色/跟随系统、Omarchy 下随 Omarchy 主题,本来已有(共用 `settings.json` 加文件监视;`settings.sh` 核对 ElyPDF 选深色后 ElyTree 即刻变深);Omarchy 的读法与 OmaPhoto 相同(`~/.local/state/omarchy/current/theme/colors.toml`、`mode` 与具名颜色)。 - 2026-10-08:图像的显存与内存(C20.2 长时测试查出)。GPUI 把每张画过的 `RenderImage` 留在窗口的图集里,直到调 `window.drop_image`;LOK 的图块、PDF 的图块与缩略图、演示的缩略图、动画预览与放映图层原先都不释放,ElyDoc 编辑保存 60 轮后应用进程涨到约 2 GB(内核不涨)。组件库 `primitives::Retired`:不再画的图先交给它,下一次绘制时释放(`release` 只放别处已不持有的,`release_all` 给自知不再画的属主,`append` 接过关闭者的);LokView、PdfView 在替换、挤出、清空图块时交出旧图,绘制前释放;ElyShow 每个标签一份,关闭的标签与结束的预览交给 Deck;放映按窗口各一份(观众屏、演讲者视图),离场中的幻灯片还在画时其图层等它结束。修后同样 100 轮在 340 至 440 MB 之间起落。 - 2026-10-08:性能基准(C20.1,§20.1)。`tests/perf/bench.py` 在发行构建上测 PERF-01 至 08(每项 30 次,P50、P95,含全部子进程的内存),样本由 `tests/perf/samples.py` 按固定种子生成到 `engines/bench`,记录写到 `docs/perf/<日期>-<机型>.md`(格式见其 README);`ELY_STALLS=1` 开主线程忙碌记录(只供测量,空闲的应用照样不做事)。实测改动:Doc、Table、Show 原先要等内核启动完(约 2.5 至 3.9 秒)才开窗口,ElyDoc 日常启动 P95 3.9 秒不达标;现 `Worker::launch` 只起进程,内核的版本与图块核对在第一个请求前做(`Worker::ready`,任何请求都先过它,未核对的 worker 收不到请求),`ely_office::start` 在后台核对并设作者名,失败即 `startup::fail`;窗口约 0.15 秒可操作,空白文档在内核首次载入后出现(中位约 2.7 秒,其中约 2.2 秒是内核首次载入文档)。启动参数改为按当前目录取绝对路径(原相对路径成了 `file://相对` 而打不开)。补丁 `0039-skia-macos-no-unused-font-manager.patch`:macOS Skia 首次画字时以全部系统字体再建一个无人使用的 `SkFontMgr`(约 370 个字体枚举加排序,主线程约 175 毫秒),新文档的第一个字与打开文档的首块带字图块因此迟到;去掉后 PERF-03 的 10 次对比 P95 由 2.6 秒降到 1.5 秒。PERF-02 的计时原以输入落地时的代际为基准,内核的作废事件先于落地回报到达时,该次编辑的图块不计,钟一直走到下一次按键(记下的是按键间隔约 250 毫秒);现按编辑的修订认定(`view/clock.rs`:修订不低于该编辑的批次所升的代际,其图块即为所求),实测中位 2 毫秒、P95 5 毫秒。PERF-08:关闭的标签原把其图块留在 GPUI 的图集里(每个文档约 20 MB 显存,40 次新建关闭后应用涨约 250 MB);`Engine::retire` 交出关闭视图的全部图像(LOK 图块、PDF 图块与缩略图、ElyMark 页面的画面),外壳在窗口的下一帧释放,修后 40 次在 454 至 462 MB 之间。历史首屏改为从按下“历史”计(原从进程启动计,含打开仓库)。R1 后基准更严:PERF-03、04 到视图收下的第一块图块(`lok: tile (x, y) shown`),其间有图块画失败即整次作废;DOCX 样本原以单独一段的分页符分页,内核只排出 60 余页(`Heading1` 段上的 `pageBreakBefore` 同样丢页),改为分页符放在标题段内,PERF-03 之前先核对恰为 100 页(视图记录页数);PERF-02 每键须有自己的计时,缺即失败;PERF-08 循环前记基线,记内核中的文档数(worker 每次载入关闭写 `N documents open`),文档数须回到基线,文件数自第一次之后不变(第一次打开模板对话框时进程一次性载入两个系统字体与两个 AppKit 本地化表)。worker 的启动核对失败后保持失败,此后一切请求被拒(原核对失败的状态与成功相同,第二次即放行)。空白文档就绪由约 2.5 秒降到约 1.0 秒,发生在本次重建内核之后,与 0039 无关(对比实测),原因未查明。 diff --git a/packaging/windows/files.sh b/packaging/windows/files.sh new file mode 100644 index 0000000..5735d54 --- /dev/null +++ b/packaging/windows/files.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Prints NSIS macros naming what a stage installs. +# Usage: files.sh +set -euo pipefail + +stage=${1:?staged folder from packaging/stage.sh} +slug=${2:?app slug, such as ely-doc} +names=$({ ls -A "$stage"; echo "$slug.ico"; echo uninstall.exe; } | sort -u) +for name in $names; do + [[ $name =~ ^[A-Za-z0-9._-]+$ ]] || { + echo "files.sh: unsafe name $name" >&2 + exit 1 + } +done + +# The list the next install and the uninstaller remove. +echo '!macro ElyWriteOwned' +echo ' FileOpen $9 "$INSTDIR\.ely-files" w' +for name in $names; do + echo " FileWrite \$9 \"$name\$\\r\$\\n\"" +done +echo ' FileClose $9' +echo '!macroend' + +# Names this install creates, for a rollback to remove. +echo '!macro ElyNoteNew' +for name in $names; do + echo " IfFileExists \"\$INSTDIR\\$name\" +2" + echo " FileWrite \$R2 \"$name\$\\r\$\\n\"" +done +echo '!macroend' diff --git a/packaging/windows/installer.nsi b/packaging/windows/installer.nsi index 591bef8..214b4ae 100644 --- a/packaging/windows/installer.nsi +++ b/packaging/windows/installer.nsi @@ -1,4 +1,4 @@ -; Per-user installer; pass APP_NAME, APP_SLUG, VERSION, STAGE, ICON, TYPES. +; Per-user installer; pass each define the checks below name. Unicode true !include "MUI2.nsh" @@ -21,6 +21,10 @@ Unicode true !error "pass -DTYPES, from packaging/windows/types.sh" !endif !include "${TYPES}" +!ifndef FILES + !error "pass -DFILES, from packaging/windows/files.sh" +!endif +!include "${FILES}" !ifndef OUTFILE !define OUTFILE "${APP_SLUG}-${VERSION}-setup.exe" !endif @@ -34,6 +38,7 @@ RequestExecutionLevel user InstallDir "$LOCALAPPDATA\Programs\Ely Office\${APP_NAME}" InstallDirRegKey HKCU "Software\Ely Office\${APP_NAME}" "InstallDir" SetCompressor /SOLID lzma +AllowSkipFiles off !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES @@ -45,32 +50,193 @@ SetCompressor /SOLID lzma LangString DeleteData ${LANG_SIMPCHINESE} "同时删除 ${APP_NAME} 的缓存、账号与本地历史吗?你的文档不受影响。" LangString DeleteData ${LANG_ENGLISH} "Also delete the cache, accounts and local history of ${APP_NAME}? Your documents stay." -!macro ElyRemoveFiles - ; What stage.sh lays out, and nothing else. - RMDir /r "$INSTDIR\libreoffice" - RMDir /r "$INSTDIR\tessdata" - RMDir /r "$INSTDIR\git" - RMDir /r "$INSTDIR\content" - RMDir /r "$INSTDIR\locales" - RMDir /r "$INSTDIR\licenses" - Delete "$INSTDIR\*.exe" - Delete "$INSTDIR\*.dll" - Delete "$INSTDIR\*.pak" - Delete "$INSTDIR\*.bin" - Delete "$INSTDIR\*.dat" - Delete "$INSTDIR\*.json" - Delete "$INSTDIR\${APP_SLUG}.ico" +LangString Busy ${LANG_SIMPCHINESE} "${APP_NAME} 的文件正被占用,未作任何改动。请关闭 ${APP_NAME} 后重试:" +LangString Busy ${LANG_ENGLISH} "A file of ${APP_NAME} is in use; nothing was changed. Close ${APP_NAME} and try again:" +LangString Stuck ${LANG_SIMPCHINESE} "旧版的文件放回原处失败,留在:" +LangString Stuck ${LANG_ENGLISH} "Could not put the old files back; they are in:" +LangString Left ${LANG_SIMPCHINESE} "新版已装好,旧版的文件删不掉,留在:" +LangString Left ${LANG_ENGLISH} "The new version is installed; the old files could not be deleted from:" +LangString NoList ${LANG_SIMPCHINESE} "找不到安装清单,未删除任何文件:" +LangString NoList ${LANG_ENGLISH} "No install list found; nothing was deleted:" + +!macro ElyLegacy ID PATTERN + FindFirst $R3 $R4 "$INSTDIR\${PATTERN}" + ely_legacy_${ID}: + StrCmp $R4 "" ely_legacy_end_${ID} + FileWrite $R2 "$R4$\r$\n" + FindNext $R3 $R4 + Goto ely_legacy_${ID} + ely_legacy_end_${ID}: + FindClose $R3 !macroend +; Sets $R0 to what an earlier install owns. +Function ElyOwned + StrCpy $R0 "" + InitPluginsDir + ; A copy, as this install rewrites the list. + IfFileExists "$INSTDIR\.ely-files" 0 unlisted + StrCpy $R0 "$PLUGINSDIR\owned.txt" + StrCpy $R7 "listed" + CopyFiles /SILENT "$INSTDIR\.ely-files" "$R0" + Goto done +unlisted: + ; Older releases: only the folder our key names. + ReadRegStr $R1 HKCU "Software\Ely Office\${APP_NAME}" "InstallDir" + StrCmp $R1 "$INSTDIR" 0 done + IfFileExists "$INSTDIR\${APP_SLUG}.exe" 0 done + StrCpy $R0 "$PLUGINSDIR\legacy.txt" + FileOpen $R2 "$R0" w + FileWrite $R2 "libreoffice$\r$\ntessdata$\r$\ngit$\r$\ncontent$\r$\nlocales$\r$\nlicenses$\r$\n" + FileWrite $R2 "${APP_SLUG}.ico$\r$\n" + !insertmacro ElyLegacy exe "*.exe" + !insertmacro ElyLegacy dll "*.dll" + !insertmacro ElyLegacy pak "*.pak" + !insertmacro ElyLegacy bin "*.bin" + !insertmacro ElyLegacy dat "*.dat" + !insertmacro ElyLegacy json "*.json" + FileClose $R2 +done: +FunctionEnd + +!macro ElyAside UN +; Strips the line end from $R3. +Function ${UN}ElyTrim +again: + StrCpy $R6 "$R3" 1 -1 + StrCmp $R6 "$\r" cut + StrCmp $R6 "$\n" cut + Return +cut: + StrCpy $R3 "$R3" -1 + Goto again +FunctionEnd + +; Moves $R0's names into .ely-old; $R5 names a blocker. +Function ${UN}ElyAside + StrCpy $R5 "" + StrCpy $R9 "$INSTDIR\.ely-old" + RMDir /r "$R9" + IfFileExists "$R9" 0 +3 + StrCpy $R5 "$R9" + Return + CreateDirectory "$R9" + ClearErrors + FileOpen $R2 "$R0" r + IfErrors 0 check + StrCpy $R5 "$R0" + Return +check: + ; A running or locked file refuses writing. + ClearErrors + FileRead $R2 $R3 + IfErrors checked + Call ${UN}ElyTrim + StrCmp $R3 "" check + IfFileExists "$INSTDIR\$R3\*.*" check + IfFileExists "$INSTDIR\$R3" 0 check + ClearErrors + FileOpen $R4 "$INSTDIR\$R3" a + IfErrors refused + FileClose $R4 + Goto check +checked: + FileSeek $R2 0 SET +move: + ClearErrors + FileRead $R2 $R3 + IfErrors moved + Call ${UN}ElyTrim + StrCmp $R3 "" move + IfFileExists "$INSTDIR\$R3" 0 move + ClearErrors + Rename "$INSTDIR\$R3" "$R9\$R3" + IfErrors 0 move + StrCpy $R5 "$INSTDIR\$R3" + FileClose $R2 + Call ${UN}ElyBack + Return +moved: + FileClose $R2 + Return +refused: + StrCpy $R5 "$INSTDIR\$R3" + FileClose $R2 + RMDir "$R9" +FunctionEnd + +; Puts every name in $R0 back from .ely-old. +Function ${UN}ElyBack + FileOpen $R2 "$R0" r +back: + ClearErrors + FileRead $R2 $R3 + IfErrors done + Call ${UN}ElyTrim + StrCmp $R3 "" back + IfFileExists "$R9\$R3" 0 back + Rename "$R9\$R3" "$INSTDIR\$R3" + Goto back +done: + FileClose $R2 + RMDir "$R9" + IfFileExists "$R9" 0 +2 + MessageBox MB_ICONSTOP "$(Stuck)$\r$\n$R9" /SD IDOK +FunctionEnd +!macroend +!insertmacro ElyAside "" +!insertmacro ElyAside "un." + +; Removes what this install created, then restores the old. +Function .onInstFailed + StrCmp $R8 "aside" 0 done + FileOpen $R2 "$PLUGINSDIR\created.txt" r +drop: + ClearErrors + FileRead $R2 $R3 + IfErrors dropped + Call ElyTrim + StrCmp $R3 "" drop + RMDir /r "$INSTDIR\$R3" + Delete "$INSTDIR\$R3" + Goto drop +dropped: + FileClose $R2 + Call ElyBack + Delete "$INSTDIR\.ely-files" + StrCmp $R7 "listed" 0 done + CopyFiles /SILENT "$R0" "$INSTDIR\.ely-files" +done: +FunctionEnd + Section "Install" - ; An older install's files go first, so none linger. - IfFileExists "$INSTDIR\${APP_SLUG}.exe" 0 fresh - !insertmacro ElyRemoveFiles + ; An earlier install moves aside whole, or nothing changes. + Call ElyOwned + StrCmp $R0 "" fresh + Call ElyAside + StrCmp $R5 "" 0 +2 + StrCpy $R8 "aside" + StrCmp $R5 "" fresh + MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK + SetErrorLevel 2 + Quit fresh: + InitPluginsDir + FileOpen $R2 "$PLUGINSDIR\created.txt" w + !insertmacro ElyNoteNew + FileClose $R2 + ; A file that cannot be written fails the install. + ClearErrors SetOutPath "$INSTDIR" File /r "${STAGE}\*.*" File "/oname=${APP_SLUG}.ico" "${ICON}" WriteUninstaller "$INSTDIR\uninstall.exe" + !insertmacro ElyWriteOwned + IfErrors 0 +2 + Abort + RMDir /r "$INSTDIR\.ely-old" + IfFileExists "$INSTDIR\.ely-old" 0 +2 + MessageBox MB_ICONEXCLAMATION "$(Left)$\r$\n$INSTDIR\.ely-old" /SD IDOK CreateShortcut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${APP_SLUG}.exe" "" "$INSTDIR\${APP_SLUG}.ico" WriteRegStr HKCU "Software\Ely Office\${APP_NAME}" "InstallDir" "$INSTDIR" WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayName" "${APP_NAME}" @@ -84,13 +250,26 @@ fresh: SectionEnd Section "Uninstall" - !insertmacro ElyRemoveFiles + StrCpy $R0 "$INSTDIR\.ely-files" + IfFileExists "$R0" listed + MessageBox MB_ICONSTOP "$(NoList)$\r$\n$INSTDIR" /SD IDOK + SetErrorLevel 2 + Quit +listed: + Call un.ElyAside + StrCmp $R5 "" gone + MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK + SetErrorLevel 2 + Quit +gone: + RMDir /r "$INSTDIR\.ely-old" + Delete "$R0" RMDir "$INSTDIR" Delete "$SMPROGRAMS\${APP_NAME}.lnk" DeleteRegKey HKCU "Software\Ely Office\${APP_NAME}" DeleteRegKey HKCU "${UNINSTALL_KEY}" !insertmacro ElyTypesUninstall - MessageBox MB_YESNO|MB_DEFBUTTON2 "$(DeleteData)" IDNO done + MessageBox MB_YESNO|MB_DEFBUTTON2 "$(DeleteData)" /SD IDNO IDNO done RMDir /r "$LOCALAPPDATA\ElyOffice\${APP_SLUG}" done: SectionEnd diff --git a/packaging/windows/installer.sh b/packaging/windows/installer.sh index 8c2f5c7..5fb6a05 100755 --- a/packaging/windows/installer.sh +++ b/packaging/windows/installer.sh @@ -20,10 +20,12 @@ stage=$root/target/stage/$slug "$root/packaging/stage.sh" "$slug" "$stage" types=$root/target/stage/$slug-types.nsh "$root/packaging/windows/types.sh" "$slug" >"$types" +files=$root/target/stage/$slug-files.nsh +"$root/packaging/windows/files.sh" "$stage" "$slug" >"$files" mkdir -p "$dist" out=$dist/$name-$version-windows-x86_64-setup.exe "$makensis" -V2 -INPUTCHARSET UTF8 -DAPP_NAME="$name" -DAPP_SLUG="$slug" -DVERSION="$version" \ -DSTAGE="$(cygpath -w "$stage")" -DICON="$(cygpath -w "$root/assets/icons/$slug.ico")" \ - -DTYPES="$(cygpath -w "$types")" -DOUTFILE="$(cygpath -w "$out")" \ + -DTYPES="$(cygpath -w "$types")" -DFILES="$(cygpath -w "$files")" -DOUTFILE="$(cygpath -w "$out")" \ "$(cygpath -w "$root/packaging/windows/installer.nsi")" echo "$out" diff --git a/tasks/TASKS.md b/tasks/TASKS.md index d9f56eb..c71d966 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -268,5 +268,5 @@ - [x] R6.7 ElyMark 可视与预览保留单个换行(用户报:代码块后的“你好↵测试”离开输入框后变成“你好 测试”):照 Typora 默认(编辑时保留单个换行,导出与打印照 GFM 读作空格);组件库的读法分屏上与输出两种 - [x] R6.8 源码模式插入点落在字中(用户报:“测”后的插入点画在“测”中间):代码编辑器按“列 × m 的宽”定位,中日韩字宽约两格;插入点、选区、按下、输入法框改按排版后的实际位置(组件库 `CodeEditor`) - [x] R6.9 ElyMark 三、四级标题不显眼(用户报):各级字号随正文按 Typora 的倍数(2.25、1.75、1.5、1.25、1、1),一律加粗,六级为次要色(组件库渲染) -- [ ] R6.11 Windows 升级后旧文件仍在(R6.10 本机重装查出):安装程序只覆盖不删除,旧版 ElyMark 的 `git` 与 CEF 文件留在安装目录;装前照卸载的同一清单删去,只在目录里已有本应用的程序时 +- [ ] R6.11 Windows 升级后旧文件仍在(R6.10 本机重装查出):安装程序只覆盖不删除,旧版 ElyMark 的 `git` 与 CEF 文件留在安装目录;安装程序记下所装的文件,升级与卸载只移去这些;文件被占用即什么也不改,失败时放回旧版 - [ ] R6.10 重发 0.1.5(用户定:仍为 0.1.5):删去 v0.1.5 的全部文件,标签移到修复的提交,三端全部包重建并上传,本机重装 diff --git a/tests/scripts/windows-installer.sh b/tests/scripts/windows-installer.sh new file mode 100644 index 0000000..3307f36 --- /dev/null +++ b/tests/scripts/windows-installer.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Installer upgrades remove only owned files; busy files change nothing. +set -euo pipefail + +root=$(cd "$(dirname "$0")/../.." && pwd) +makensis=${MAKENSIS:-"/c/Program Files (x86)/NSIS/makensis.exe"} +case "$(uname -s)" in MINGW* | MSYS* | CYGWIN*) ;; *) echo "SKIP: not Windows"; exit 0 ;; esac +[ -x "$makensis" ] || { echo "SKIP: no makensis"; exit 0; } +export MSYS2_ARG_CONV_EXCL='*' + +work=$root/target/installer-test +key='HKCU\Software\Ely Office\ElyProbe' +uninstall_key='HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\Ely Office ElyProbe' +clean() { + reg delete "$key" /f >/dev/null 2>&1 || true + reg delete "$uninstall_key" /f >/dev/null 2>&1 || true + rm -f "$APPDATA/Microsoft/Windows/Start Menu/Programs/ElyProbe.lnk" +} +trap 'kill $(jobs -p) 2>/dev/null || true; clean' EXIT +clean +rm -rf "$work" +mkdir -p "$work" +fail() { echo "FAIL: $*" >&2; exit 1; } + +# Two releases of a fake app; v2 adds extra.txt. +build() { + local version=$1 stage=$work/stage-$1 + mkdir -p "$stage/licenses" "$stage/data" + cp /c/Windows/System32/PING.EXE "$stage/ely-probe.exe" + echo "$version" >"$stage/ely-probe-worker.exe" + echo "$version" >"$stage/licenses/LICENSE" + echo "$version" >"$stage/data/info.txt" + [ "$version" = 1 ] || echo "$version" >"$stage/extra.txt" + printf '!macro ElyTypesInstall\n!macroend\n!macro ElyTypesUninstall\n!macroend\n' >"$work/types.nsh" + "$root/packaging/windows/files.sh" "$stage" ely-probe >"$work/files-$version.nsh" + "$makensis" -V1 -INPUTCHARSET UTF8 -DAPP_NAME=ElyProbe -DAPP_SLUG=ely-probe -DVERSION="$version" \ + -DSTAGE="$(cygpath -w "$stage")" -DICON="$(cygpath -w "$root/assets/icons/ely-mark.ico")" \ + -DTYPES="$(cygpath -w "$work/types.nsh")" -DFILES="$(cygpath -w "$work/files-$version.nsh")" \ + -DOUTFILE="$(cygpath -w "$work/setup-$version.exe")" "$(cygpath -w "$root/packaging/windows/installer.nsi")" +} +build 1 +build 2 + +install() { "$work/setup-$1.exe" /S "/D=$(cygpath -w "$2")" && echo 0 || echo $?; } +snapshot() { (cd "$1" && find . -type f -exec md5sum {} + | sort); } +put() { mkdir -p "$(dirname "$1")" && echo user >"$1"; } +must() { [ -e "$1" ] || fail "$2: $1 is gone"; } +mustnt() { [ ! -e "$1" ] || fail "$2: $1 is still there"; } +# Holds a file open without delete or write sharing. +hold() { + rm -f "$work/held" + powershell -NoProfile -Command "\$f=[IO.File]::Open('$(cygpath -w "$1")','Open','Read','Read'); \ + New-Item '$(cygpath -w "$work/held")' | Out-Null; Start-Sleep 60" & + until [ -e "$work/held" ]; do sleep 0.2; done +} + +# A chosen folder's own files survive install and reinstall. +dir=$work/stranger +for f in keep.exe keep.dll keep.json git/keep.txt notes/note.md; do put "$dir/$f"; done +[ "$(install 1 "$dir")" = 0 ] || fail "stranger install" +[ "$(install 2 "$dir")" = 0 ] || fail "stranger reinstall" +for f in keep.exe keep.dll keep.json git/keep.txt notes/note.md; do must "$dir/$f" stranger; done +must "$dir/extra.txt" "stranger reinstall" +grep -q '^2$' "$dir/licenses/LICENSE" || fail "stranger reinstall kept the old licence" + +# Older releases are cleaned only where our key points. +legacy() { + for f in ely-probe.exe libcef.dll chrome_100.pak old.json git/cmd/git.exe locales/en.pak \ + notes/note.md keep.md; do put "$1/$f"; done +} +dir=$work/unkeyed +legacy "$dir" +[ "$(install 2 "$dir")" = 0 ] || fail "unkeyed install" +must "$dir/git/cmd/git.exe" "unkeyed folder" +dir=$work/legacy +legacy "$dir" +reg add "$key" /v InstallDir /d "$(cygpath -w "$dir")" /f >/dev/null +[ "$(install 2 "$dir")" = 0 ] || fail "legacy upgrade" +for f in libcef.dll chrome_100.pak old.json git locales; do mustnt "$dir/$f" "legacy upgrade"; done +for f in notes/note.md keep.md extra.txt; do must "$dir/$f" "legacy upgrade"; done + +# Busy files: the installer refuses; the old install stays whole. +busy() { + local name=$1 dir=$2 file=$3 before + before=$(snapshot "$dir") + hold "$file" + [ "$(install 2 "$dir")" = 2 ] || fail "$name: installer did not refuse" + kill %% 2>/dev/null; wait 2>/dev/null || true + [ "$(snapshot "$dir")" = "$before" ] || fail "$name: the old install changed" + mustnt "$dir/.ely-old" "$name" +} +dir=$work/busy +[ "$(install 1 "$dir")" = 0 ] || fail "busy setup" +busy "held program" "$dir" "$dir/ely-probe-worker.exe" +busy "held file in a folder" "$dir" "$dir/licenses/LICENSE" +dir=$work/busy-legacy +legacy "$dir" +reg add "$key" /v InstallDir /d "$(cygpath -w "$dir")" /f >/dev/null +busy "held legacy git" "$dir" "$dir/git/cmd/git.exe" + +dir=$work/running +[ "$(install 1 "$dir")" = 0 ] || fail "running setup" +before=$(snapshot "$dir") +"$dir/ely-probe.exe" -n 30 127.0.0.1 >/dev/null & +sleep 1 +[ "$(install 2 "$dir")" = 2 ] || fail "running: installer did not refuse" +kill %% 2>/dev/null; wait 2>/dev/null || true +[ "$(snapshot "$dir")" = "$before" ] || fail "running: the old install changed" + +# A failed copy puts the old release back. +dir=$work/rollback +[ "$(install 1 "$dir")" = 0 ] || fail "rollback setup" +put "$dir/extra.txt/blocker" +before=$(snapshot "$dir") +[ "$(install 2 "$dir")" != 0 ] || fail "rollback: blocked copy succeeded" +[ "$(snapshot "$dir")" = "$before" ] || fail "rollback: the old install changed" +mustnt "$dir/.ely-old" rollback + +# Uninstalling removes only what was installed. +dir=$work/stranger +cp "$dir/uninstall.exe" "$work/uninstall.exe" +"$work/uninstall.exe" /S "_?=$(cygpath -w "$dir")" || fail "uninstall" +for f in keep.exe keep.dll keep.json git/keep.txt notes/note.md; do must "$dir/$f" uninstall; done +for f in ely-probe.exe ely-probe-worker.exe extra.txt licenses data uninstall.exe .ely-files; do + mustnt "$dir/$f" uninstall +done +rm -rf "$work" +echo "PASS: windows installer"