diff --git a/agents.md b/agents.md index c780cb3..c6ee601 100644 --- a/agents.md +++ b/agents.md @@ -226,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 安装程序只动自己装下的:装好后写下所装的顶层名(`.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:首版打包(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-installed`,由 `packaging/windows/files.sh` 按分装目录生成),升级与卸载只移去其中的文件,文件夹空了才删;清单之前的版本只在注册表记下的安装目录里按旧的通配名与其文件夹里的全部文件移去;先查每个文件能否写(运行中或被占用即不能,文件夹里的也查),再逐个挪进 `.ely-old`,任一项挪不动即放回、什么也不改、退出码 2;新文件的位置已有不属于本应用的文件即拒绝、什么也不改、退出码 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 index 09f62c8..9ed9ee4 100644 --- a/packaging/windows/files.sh +++ b/packaging/windows/files.sh @@ -1,30 +1,23 @@ #!/usr/bin/env bash -# Prints NSIS macros naming a stage: files.sh +# Prints the files and folders an install owns: 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 +cd "$stage" +[ -z "$(find . -type l)" ] || { + echo "files.sh: links in $stage" >&2 + exit 1 +} +# Folders first, parents before children; then files. +{ + find . -mindepth 1 -type d | sort | sed 's|$|/|' + { find . -type f; echo "./$slug.ico"; echo ./uninstall.exe; } | sort -u +} | sed 's|^\./||' | while IFS= read -r path; do + case $path in *\\* | *[[:cntrl:]]*) + echo "files.sh: unsafe name $path" >&2 exit 1 - } + ;; + esac + printf '%s\r\n' "${path//\//\\}" 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 214b4ae..ba2117b 100644 --- a/packaging/windows/installer.nsi +++ b/packaging/windows/installer.nsi @@ -21,10 +21,9 @@ Unicode true !error "pass -DTYPES, from packaging/windows/types.sh" !endif !include "${TYPES}" -!ifndef FILES - !error "pass -DFILES, from packaging/windows/files.sh" +!ifndef LIST + !error "pass -DLIST, from packaging/windows/files.sh" !endif -!include "${FILES}" !ifndef OUTFILE !define OUTFILE "${APP_SLUG}-${VERSION}-setup.exe" !endif @@ -59,6 +58,42 @@ LangString Left ${LANG_ENGLISH} "The new version is installed; the old files cou LangString NoList ${LANG_SIMPCHINESE} "找不到安装清单,未删除任何文件:" LangString NoList ${LANG_ENGLISH} "No install list found; nothing was deleted:" +LangString Taken ${LANG_SIMPCHINESE} "安装位置已有不属于 ${APP_NAME} 的文件,未作任何改动:" +LangString Taken ${LANG_ENGLISH} "A file ${APP_NAME} did not install is in the way; nothing was changed:" + +; Lists a legacy folder's files and folders to $R2. +Function ElyWalk + Push "|" + Push $R1 +walk: + Pop $R1 + StrCmp $R1 "|" done + FileWrite $R2 "$R1\$\r$\n" + FindFirst $R3 $R4 "$INSTDIR\$R1\*" +entry: + StrCmp $R4 "" closed + StrCmp $R4 "." skip + StrCmp $R4 ".." skip + IfFileExists "$INSTDIR\$R1\$R4\*.*" 0 file + Push "$R1\$R4" + Goto skip +file: + FileWrite $R2 "$R1\$R4$\r$\n" +skip: + FindNext $R3 $R4 + Goto entry +closed: + FindClose $R3 + Goto walk +done: +FunctionEnd + +!macro ElyLegacyDir NAME + IfFileExists "$INSTDIR\${NAME}\*.*" 0 +3 + StrCpy $R1 "${NAME}" + Call ElyWalk +!macroend + !macro ElyLegacy ID PATTERN FindFirst $R3 $R4 "$INSTDIR\${PATTERN}" ely_legacy_${ID}: @@ -70,15 +105,15 @@ LangString NoList ${LANG_ENGLISH} "No install list found; nothing was deleted:" FindClose $R3 !macroend -; Sets $R0 to what an earlier install owns. +; Sets $R0 to the list an earlier install owns. Function ElyOwned StrCpy $R0 "" - InitPluginsDir + StrCpy $R7 "" ; A copy, as this install rewrites the list. - IfFileExists "$INSTDIR\.ely-files" 0 unlisted + IfFileExists "$INSTDIR\.ely-installed" 0 unlisted StrCpy $R0 "$PLUGINSDIR\owned.txt" StrCpy $R7 "listed" - CopyFiles /SILENT "$INSTDIR\.ely-files" "$R0" + CopyFiles /SILENT "$INSTDIR\.ely-installed" "$R0" Goto done unlisted: ; Older releases: only the folder our key names. @@ -87,7 +122,12 @@ unlisted: 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" + !insertmacro ElyLegacyDir libreoffice + !insertmacro ElyLegacyDir tessdata + !insertmacro ElyLegacyDir git + !insertmacro ElyLegacyDir content + !insertmacro ElyLegacyDir locales + !insertmacro ElyLegacyDir licenses FileWrite $R2 "${APP_SLUG}.ico$\r$\n" !insertmacro ElyLegacy exe "*.exe" !insertmacro ElyLegacy dll "*.dll" @@ -100,19 +140,51 @@ done: FunctionEnd !macro ElyAside UN -; Strips the line end from $R3. -Function ${UN}ElyTrim -again: +; Reads $R2's next line: $R3 path, $R6 "d", "f" or "" at end. +Function ${UN}ElyNext +next: + ClearErrors + FileRead $R2 $R3 + IfErrors 0 trim + StrCpy $R6 "" + Return +trim: StrCpy $R6 "$R3" 1 -1 StrCmp $R6 "$\r" cut StrCmp $R6 "$\n" cut + StrCmp $R3 "" next + StrCmp $R6 "\" 0 +4 + StrCpy $R3 "$R3" -1 + StrCpy $R6 "d" + Return + StrCpy $R6 "f" Return cut: StrCpy $R3 "$R3" -1 - Goto again + Goto trim FunctionEnd -; Moves $R0's names into .ely-old; $R5 names a blocker. +; Removes the empty folders $R0 lists under $R1. +Function ${UN}ElyPrune +pass: + StrCpy $8 "" + FileOpen $R2 "$R0" r +next: + Call ${UN}ElyNext + StrCmp $R6 "" passed + StrCmp $R6 "f" next + IfFileExists "$R1\$R3\*.*" 0 next + ClearErrors + RMDir "$R1\$R3" + IfErrors next + StrCpy $8 "again" + Goto next +passed: + FileClose $R2 + StrCmp $8 "again" pass +FunctionEnd + +; Moves $R0's files into .ely-old; $R5 names a blocker. Function ${UN}ElyAside StrCpy $R5 "" StrCpy $R9 "$INSTDIR\.ely-old" @@ -120,19 +192,16 @@ Function ${UN}ElyAside 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 + ; Every file first: running or locked ones refuse writing. + Call ${UN}ElyNext + StrCmp $R6 "" checked + StrCmp $R6 "d" check IfFileExists "$INSTDIR\$R3\*.*" check IfFileExists "$INSTDIR\$R3" 0 check ClearErrors @@ -141,13 +210,17 @@ check: FileClose $R4 Goto check checked: + CreateDirectory "$R9" FileSeek $R2 0 SET move: - ClearErrors - FileRead $R2 $R3 - IfErrors moved - Call ${UN}ElyTrim - StrCmp $R3 "" move + Call ${UN}ElyNext + StrCmp $R6 "" moved + StrCmp $R6 "f" file + IfFileExists "$INSTDIR\$R3\*.*" 0 move + CreateDirectory "$R9\$R3" + Goto move +file: + IfFileExists "$INSTDIR\$R3\*.*" move IfFileExists "$INSTDIR\$R3" 0 move ClearErrors Rename "$INSTDIR\$R3" "$R9\$R3" @@ -162,23 +235,22 @@ moved: refused: StrCpy $R5 "$INSTDIR\$R3" FileClose $R2 - RMDir "$R9" FunctionEnd -; Puts every name in $R0 back from .ely-old. +; Puts $R0's files back from .ely-old. Function ${UN}ElyBack FileOpen $R2 "$R0" r back: - ClearErrors - FileRead $R2 $R3 - IfErrors done - Call ${UN}ElyTrim - StrCmp $R3 "" back + Call ${UN}ElyNext + StrCmp $R6 "" done + StrCmp $R6 "d" back IfFileExists "$R9\$R3" 0 back Rename "$R9\$R3" "$INSTDIR\$R3" Goto back done: FileClose $R2 + StrCpy $R1 "$R9" + Call ${UN}ElyPrune RMDir "$R9" IfFileExists "$R9" 0 +2 MessageBox MB_ICONSTOP "$(Stuck)$\r$\n$R9" /SD IDOK @@ -187,53 +259,90 @@ FunctionEnd !insertmacro ElyAside "" !insertmacro ElyAside "un." -; Removes what this install created, then restores the old. +; Sets $R5 to a path in the way; notes folders to create. +Function ElyClear + StrCpy $R5 "" + FileOpen $R2 "$PLUGINSDIR\new.txt" r + FileOpen $7 "$PLUGINSDIR\made.txt" w +next: + Call ElyNext + StrCmp $R6 "" done + StrCmp $R6 "d" folder + IfFileExists "$INSTDIR\$R3" taken next +folder: + IfFileExists "$INSTDIR\$R3\*.*" next + IfFileExists "$INSTDIR\$R3" taken + FileWrite $7 "$R3\$\r$\n" + Goto next +taken: + StrCpy $R5 "$INSTDIR\$R3" +done: + FileClose $7 + FileClose $R2 +FunctionEnd + +; Removes what this install wrote, then restores the old. Function .onInstFailed - StrCmp $R8 "aside" 0 done - FileOpen $R2 "$PLUGINSDIR\created.txt" r + StrCmp $R8 "copying" 0 done + FileOpen $R2 "$PLUGINSDIR\new.txt" r drop: - ClearErrors - FileRead $R2 $R3 - IfErrors dropped - Call ElyTrim - StrCmp $R3 "" drop - RMDir /r "$INSTDIR\$R3" + Call ElyNext + StrCmp $R6 "" dropped + StrCmp $R6 "d" drop Delete "$INSTDIR\$R3" Goto drop dropped: FileClose $R2 + Delete "$INSTDIR\.ely-installed" + Push $R0 + StrCpy $R0 "$PLUGINSDIR\made.txt" + StrCpy $R1 "$INSTDIR" + Call ElyPrune + Pop $R0 + StrCmp $R0 "" done Call ElyBack - Delete "$INSTDIR\.ely-files" StrCmp $R7 "listed" 0 done - CopyFiles /SILENT "$R0" "$INSTDIR\.ely-files" + CopyFiles /SILENT "$R0" "$INSTDIR\.ely-installed" done: FunctionEnd Section "Install" + InitPluginsDir + SetOutPath "$PLUGINSDIR" + File "/oname=new.txt" "${LIST}" ; An earlier install moves aside whole, or nothing changes. Call ElyOwned - StrCmp $R0 "" fresh + StrCmp $R0 "" clear Call ElyAside - StrCmp $R5 "" 0 +2 - StrCpy $R8 "aside" - StrCmp $R5 "" fresh + StrCmp $R5 "" clear MessageBox MB_ICONSTOP "$(Busy)$\r$\n$R5" /SD IDOK SetErrorLevel 2 Quit -fresh: - InitPluginsDir - FileOpen $R2 "$PLUGINSDIR\created.txt" w - !insertmacro ElyNoteNew - FileClose $R2 +clear: + ; Never write over a file this app did not install. + Call ElyClear + StrCmp $R5 "" copy + StrCmp $R0 "" +2 + Call ElyBack + MessageBox MB_ICONSTOP "$(Taken)$\r$\n$R5" /SD IDOK + SetErrorLevel 2 + Quit +copy: + StrCpy $R8 "copying" ; 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 + CopyFiles /SILENT "$PLUGINSDIR\new.txt" "$INSTDIR\.ely-installed" IfErrors 0 +2 Abort + StrCpy $R8 "" + ; Old folders the new release leaves empty. + StrCmp $R0 "" +3 + StrCpy $R1 "$INSTDIR" + Call ElyPrune RMDir /r "$INSTDIR\.ely-old" IfFileExists "$INSTDIR\.ely-old" 0 +2 MessageBox MB_ICONEXCLAMATION "$(Left)$\r$\n$INSTDIR\.ely-old" /SD IDOK @@ -250,7 +359,7 @@ fresh: SectionEnd Section "Uninstall" - StrCpy $R0 "$INSTDIR\.ely-files" + StrCpy $R0 "$INSTDIR\.ely-installed" IfFileExists "$R0" listed MessageBox MB_ICONSTOP "$(NoList)$\r$\n$INSTDIR" /SD IDOK SetErrorLevel 2 @@ -263,6 +372,8 @@ listed: Quit gone: RMDir /r "$INSTDIR\.ely-old" + StrCpy $R1 "$INSTDIR" + Call un.ElyPrune Delete "$R0" RMDir "$INSTDIR" Delete "$SMPROGRAMS\${APP_NAME}.lnk" diff --git a/packaging/windows/installer.sh b/packaging/windows/installer.sh index 5fb6a05..55058ee 100755 --- a/packaging/windows/installer.sh +++ b/packaging/windows/installer.sh @@ -20,12 +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" +list=$root/target/stage/$slug-files.txt +"$root/packaging/windows/files.sh" "$stage" "$slug" >"$list" 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")" -DFILES="$(cygpath -w "$files")" -DOUTFILE="$(cygpath -w "$out")" \ + -DTYPES="$(cygpath -w "$types")" -DLIST="$(cygpath -w "$list")" -DOUTFILE="$(cygpath -w "$out")" \ "$(cygpath -w "$root/packaging/windows/installer.nsi")" echo "$out" diff --git a/tests/scripts/windows-installer.sh b/tests/scripts/windows-installer.sh index 3307f36..fadfdcf 100644 --- a/tests/scripts/windows-installer.sh +++ b/tests/scripts/windows-installer.sh @@ -22,20 +22,21 @@ rm -rf "$work" mkdir -p "$work" fail() { echo "FAIL: $*" >&2; exit 1; } -# Two releases of a fake app; v2 adds extra.txt. +# Two releases of a fake app; v2 adds extra.txt and zz. build() { local version=$1 stage=$work/stage-$1 - mkdir -p "$stage/licenses" "$stage/data" + mkdir -p "$stage/licenses" "$stage/data" "$stage/bin" cp /c/Windows/System32/PING.EXE "$stage/ely-probe.exe" + cp /c/Windows/System32/PING.EXE "$stage/bin/helper.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" + [ "$version" = 1 ] || { echo "$version" >"$stage/extra.txt"; mkdir -p "$stage/zz"; echo 2 >"$stage/zz/deep.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" + "$root/packaging/windows/files.sh" "$stage" ely-probe >"$work/files-$version.txt" "$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")" \ + -DTYPES="$(cygpath -w "$work/types.nsh")" -DLIST="$(cygpath -w "$work/files-$version.txt")" \ -DOUTFILE="$(cygpath -w "$work/setup-$version.exe")" "$(cygpath -w "$root/packaging/windows/installer.nsi")" } build 1 @@ -56,10 +57,11 @@ hold() { # 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 +own="keep.exe keep.dll keep.json git/keep.txt notes/note.md licenses/my-notes.md" +for f in $own; 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 +for f in $own; 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" @@ -68,10 +70,21 @@ 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 } +# Another install's file in the way: refused, nothing changes. +untouched() { + local name=$1 dir=$2 before + before=$(snapshot "$dir") + [ "$(install 2 "$dir")" = 2 ] || fail "$name: installer did not refuse" + [ "$(snapshot "$dir")" = "$before" ] || fail "$name: the folder changed" + mustnt "$dir/.ely-old" "$name" +} dir=$work/unkeyed legacy "$dir" -[ "$(install 2 "$dir")" = 0 ] || fail "unkeyed install" -must "$dir/git/cmd/git.exe" "unkeyed folder" +untouched "unkeyed folder" "$dir" +dir=$work/taken +[ "$(install 1 "$dir")" = 0 ] || fail "taken setup" +put "$dir/extra.txt" +untouched "user file in the way" "$dir" dir=$work/legacy legacy "$dir" reg add "$key" /v InstallDir /d "$(cygpath -w "$dir")" /f >/dev/null @@ -98,21 +111,35 @@ legacy "$dir" reg add "$key" /v InstallDir /d "$(cygpath -w "$dir")" /f >/dev/null busy "held legacy git" "$dir" "$dir/git/cmd/git.exe" +running() { + local name=$1 dir=$2 program=$3 before + before=$(snapshot "$dir") + "$program" -n 30 127.0.0.1 >/dev/null & + sleep 1 + [ "$(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/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" +running "running program" "$dir" "$dir/ely-probe.exe" +running "running program in a folder" "$dir" "$dir/bin/helper.exe" +dir=$work/running-legacy +legacy "$dir" +cp /c/Windows/System32/PING.EXE "$dir/git/cmd/git.exe" +reg add "$key" /v InstallDir /d "$(cygpath -w "$dir")" /f >/dev/null +running "running legacy git" "$dir" "$dir/git/cmd/git.exe" -# A failed copy puts the old release back. +# A copy failing after earlier writes puts the old release back. dir=$work/rollback [ "$(install 1 "$dir")" = 0 ] || fail "rollback setup" -put "$dir/extra.txt/blocker" +put "$dir/zz/user.txt" +icacls "$(cygpath -w "$dir/zz")" /deny '*S-1-1-0:(WD)' >/dev/null before=$(snapshot "$dir") -[ "$(install 2 "$dir")" != 0 ] || fail "rollback: blocked copy succeeded" +result=$(install 2 "$dir") +icacls "$(cygpath -w "$dir/zz")" /remove:d '*S-1-1-0' >/dev/null +[ "$result" != 0 ] || fail "rollback: blocked copy succeeded" [ "$(snapshot "$dir")" = "$before" ] || fail "rollback: the old install changed" mustnt "$dir/.ely-old" rollback @@ -120,8 +147,8 @@ mustnt "$dir/.ely-old" rollback 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 +for f in $own; do must "$dir/$f" uninstall; done +for f in ely-probe.exe ely-probe-worker.exe extra.txt licenses/LICENSE data bin zz uninstall.exe .ely-installed; do mustnt "$dir/$f" uninstall done rm -rf "$work"