LOK runs on Windows and Linux; packaging stages engines (W-01, K-01)

The kernel is the merged library on every platform; Windows and
Linux find it beside the app in libreoffice/program. All three run
unipoll: macOS through GCD, Linux by answering one request per poll,
Windows through a message-only window on the loop's thread (LOK's
own main thread had deadlocked Calc loads). Patch 0043 flushes
callbacks inline everywhere, so a request sees its result; 0042 stops
MSVC's Boost auto-link. Linux paints with cairo, BGRA; the start-up
check now paints red text and finds red in byte two on every
platform, since getTileMode does not see Skia.

ely-lok on Windows: 180 of 189 pass; on Linux 187. The rest are
listed after 0.1.3 (spelling, Calc dark tiles, show layers and
fields, a11y selection). Tests build file URLs portably and accept
CRLF where Windows writes it.

packaging/stage.sh lays out an app with its engines for Windows and
Linux; licenses.sh finds each licence on all three. ElyMark finds its
page beside the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 18:27:53 -04:00
1 parent dc4c1ed0fe
commit c96d39a342
34 files changed
+658 -214

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ serde_json = "1"
sha2 = "0.10"
tempfile = "3"
ureq = "3"
windows-sys = { version = "0.60", features = ["Win32_UI_WindowsAndMessaging", "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Threading", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_UI_Shell", "Win32_Security_Authentication_Identity"] }
windows-sys = { version = "0.60", features = ["Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Threading", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Com", "Win32_UI_Shell", "Win32_Security_Authentication_Identity"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["NSLocale", "NSPathUtilities", "NSString", "NSURL", "NSDictionary", "NSValue", "NSError", "NSRunLoop", "NSDate", "NSUserDefaults"] }
objc2-av-foundation = { version = "0.3", default-features = false, features = ["std", "AVPlayer", "AVPlayerItem", "AVPlayerItemOutput", "AVAsset", "objc2-core-media", "objc2-core-video", "objc2-core-foundation"] }
objc2-core-media = { version = "0.3", default-features = false, features = ["std", "CMTime"] }
+2 -1
View File
@@ -194,7 +194,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-02:组件库 7380afb 起:对话框与面板的 Enter 提交改走字段的 key-down 动作(macOS IME 天然守卫,不再提前提交);面板族文案经组件 `i18n::text`(内建 en/zh-CN,应用目录可覆盖);核心控件带 role/aria;对话框带角色、名称与说明,Cancel/Close 与禁用说明本地化。同仓库 `~/Documents/GitHub/Ely-GPUI-Components` 修复并推送(2026-10-09 起推到 git.zacharyzhang.com,HTTPS,令牌在本机钥匙串)。2026-10-04 组件库 b6cb94c:`NumberInput::on_commit` 只在 Enter、失焦、步进、拖动时触发,键入不触发;改文档的数值框用它,否则每个键入数字都成一条内核命令与一步撤销。同批带入上游的 Checkbox、Switch、Select 等 role/aria 与 RenderSurface。c0f17ee:`ToggleButton::disabled`(不可按、无 Tab 停靠、说明不可用,提示可写原因)。967d1e4:`ContextMenu::manual`(按属主编号的请求在某点打开,右键不开),供行在按下之后才到的菜单,如内核给的拼写建议。
- 2026-10-06:审查者为探针复制的源码树须用自己的 `CARGO_TARGET_DIR`(`scripts/review.sh` 写明);写完结论前删掉这些副本与其构建目录(曾积到 /tmp 近 900 GB,磁盘写满、链接失败):曾编进本仓库 `target/`,ely-lok 里烘焙的源码路径指向已删的副本,单测找不到 worker。
- 2026-10-03:每条任务由 `scripts/review.sh` 在 herdr 分栏里交给 codex(gpt-6.1-sol,effort high)审查,最多三轮。此前各轮用 gpt-6-astra xhigh。2026-10-10(用户定):`--yolo`、`service_tier="fast"`、effort xhigh,每条最多五轮;herdr 直接启动 codex 程序,不经 shell 别名。
- 2026-10-01:macOS 的 LOK 用自建 LibreOffice 26.2.6.3 加 `patches/libreoffice/0001-lok-macos-tile-painting.patch`(已验证出图块)。2026-10-03 加 `0002-lok-dispatch-to-document-frame.patch`:`postUnoCommand` 原发往桌面的活动框架,另一文档关闭后命令落空或落到别的文档;改发本文档的框架。2026-10-04 加 `0003-lok-char-style-and-spacing-states.patch`:LOK 原不发当前字符样式与段前段后,属性栏无从同步;补发 `.uno:CharStyle`(界面名,无字符样式为空)与 `.uno:ULSpacing`(`上,下`,1/100 mm);混合选区的状态发 `{"commandName","dontcare":true}`,`ely_lok::MIXED` 表示,段落样式跨段不一致也算混合(原样报首段),字符样式带来的字体、字号、粗斜体等在选区内不一致也算混合(原样只看直接格式)。`.uno:ULSpacing` 缺的成员归零,故两值同发;属性栏记住已发未回显的两值状态(段距、页边距),连续编辑以它为底。同日加 `0004-lok-panel-states.patch`:补发页面尺寸、页面项(横向、版式、页码格式)、页边距、分栏、页眉页脚开关与版式、四种锚点与六种环绕(真假或禁用)、标题行重复;页面项缺的成员同样归零,故转向时页面项与尺寸都发。`0005-jsdialog-skip-disposed-dialogs.patch`:对话框守卫关闭对话框后,JSDialog 空闲任务仍为它生成整窗更新,读已释放窗口崩溃(实测崩溃报告),补丁让已释放的对话框不再生成消息。`0006-page-size-takes-the-page-item.patch`:`.uno:AttributePageSize` 也收页面项,转向是一次改动、一步撤销。`0007-lok-aqua-anyinput-without-peeking.patch`:worker 的请求经主 GCD 队列派发,而 Aqua 的 `AnyInput` 用 `nextEventMatchingMask` 窥探时会转动 run loop、在内核空闲任务中途执行请求(实测:表格删列与排版空闲任务交错,`SwLayIdle::DoIdleJob_` 段错误,约三成全量测试复现);补丁让 LOK 下的 `AnyInput` 改问客户端,worker 以 `registerAnyInputCallback` 报告是否有待处理请求,请求只在顶层让出时执行(补丁后连续十次全绿)。`0008-lok-toc-caption-formula-without-dialogs.patch`:目录、题注、公式在内核只有对话框入口;补丁给 `.uno:InsertMultiIndex` 加 `Title`(默认目录)、`.uno:InsertCaptionDialog` 加 `Category`(程序名)与 `Text`(照内核题注对话框的选项)、`.uno:InsertObjectStarMath` 加 `Formula`(文字转公式,选中公式时整体替换,一步撤销),并加 `getCommandValues(".uno:Formula")` 读选中公式。插入目录后光标落在目录内(内核原样)。`0009-lok-word-count-values.patch`:`getCommandValues(".uno:WordCount")` 给文档与选区的字数、字符数;内核的 `.uno:StateWordCount` 状态是英文串,只作重读的信号。构建加 `--with-myspell-dicts`,随包词典取自 LibreOffice 的 dictionaries 子模块(core 钉的 9925407,来源与许可见各目录 README 与 license),装在 `Resources/extensions`。构建后只留 `dict-en`(25 MB,英语拼写、断词、同义词):界面语言是英文与简体中文,中文无 Hunspell 词典;全部 65 个(473 MB)时 LOK 首启 5.4 秒、再启 2.3 秒,只留英文为 1.1 秒与 0.7 秒(PERF-01 要求 3 秒内可操作)。词典语种已由用户定:英文。`0010-lok-outline-values.patch`:`getCommandValues(".uno:Outline")` 给各标题的文字、级别、位置(twips)与是否当前(光标所在处按文档顺序之前最近的标题,`GetOutlinePos`;分栏时位置比不出先后),`.uno:Bookmarks` 的每项加位置(版面不显示的书签无位置);导航栏的跳转都按位置。`0011-lok-font-lists-present-when-a-member-is-used.patch`:LOK 把以 `;` 连写的字体列表整串当作字体名,列表中有字体已用上也报缺失(实测报出界面字体的回退列表);补丁让列表中任一成员用上即不算缺失,文档里写的列表全缺时照报。`0012-lok-calc-cursor-references-merge.patch`:Calc 在回调注册后补发单元格光标;`.uno:SheetReferences?sheet=N` 给引用某表的公式格数、命名区域数、第一个公式格,以及用 INDIRECT 的公式与名称数(这类引用运行时才知指向,删除说明里单独列出);合并带 `MoveContents` 时不弹对话框,并修上游“多个数据格的 API 合并什么也不做”。worker 以 `SAL_LOK_OPTIONS=sc_print_twips_msgs`(macOS 另加 `unipoll`)运行,Calc 的光标、选区与 `.uno:SheetGeometryData` 同为文档 twips;视图把可见区经 `setClientVisibleArea` 报给内核。`0013-lok-author-option.patch`:`lo_setOption("author", 名)` 设内核用户全名,新批注与修订以此署名。`0014-lok-calc-formula-entry-and-names.patch`:公式语法错误不弹询问、补全不切走编辑、提交清引用标记、名称的可撤销定Line truncated
- 2026-10-01:macOS 的 LOK 用自建 LibreOffice 26.2.6.3 加 `patches/libreoffice/0001-lok-macos-tile-painting.patch`(已验证出图块)。2026-10-03 加 `0002-lok-dispatch-to-document-frame.patch`:`postUnoCommand` 原发往桌面的活动框架,另一文档关闭后命令落空或落到别的文档;改发本文档的框架。2026-10-04 加 `0003-lok-char-style-and-spacing-states.patch`:LOK 原不发当前字符样式与段前段后,属性栏无从同步;补发 `.uno:CharStyle`(界面名,无字符样式为空)与 `.uno:ULSpacing`(`上,下`,1/100 mm);混合选区的状态发 `{"commandName","dontcare":true}`,`ely_lok::MIXED` 表示,段落样式跨段不一致也算混合(原样报首段),字符样式带来的字体、字号、粗斜体等在选区内不一致也算混合(原样只看直接格式)。`.uno:ULSpacing` 缺的成员归零,故两值同发;属性栏记住已发未回显的两值状态(段距、页边距),连续编辑以它为底。同日加 `0004-lok-panel-states.patch`:补发页面尺寸、页面项(横向、版式、页码格式)、页边距、分栏、页眉页脚开关与版式、四种锚点与六种环绕(真假或禁用)、标题行重复;页面项缺的成员同样归零,故转向时页面项与尺寸都发。`0005-jsdialog-skip-disposed-dialogs.patch`:对话框守卫关闭对话框后,JSDialog 空闲任务仍为它生成整窗更新,读已释放窗口崩溃(实测崩溃报告),补丁让已释放的对话框不再生成消息。`0006-page-size-takes-the-page-item.patch`:`.uno:AttributePageSize` 也收页面项,转向是一次改动、一步撤销。`0007-lok-aqua-anyinput-without-peeking.patch`:worker 的请求经主 GCD 队列派发,而 Aqua 的 `AnyInput` 用 `nextEventMatchingMask` 窥探时会转动 run loop、在内核空闲任务中途执行请求(实测:表格删列与排版空闲任务交错,`SwLayIdle::DoIdleJob_` 段错误,约三成全量测试复现);补丁让 LOK 下的 `AnyInput` 改问客户端,worker 以 `registerAnyInputCallback` 报告是否有待处理请求,请求只在顶层让出时执行(补丁后连续十次全绿)。`0008-lok-toc-caption-formula-without-dialogs.patch`:目录、题注、公式在内核只有对话框入口;补丁给 `.uno:InsertMultiIndex` 加 `Title`(默认目录)、`.uno:InsertCaptionDialog` 加 `Category`(程序名)与 `Text`(照内核题注对话框的选项)、`.uno:InsertObjectStarMath` 加 `Formula`(文字转公式,选中公式时整体替换,一步撤销),并加 `getCommandValues(".uno:Formula")` 读选中公式。插入目录后光标落在目录内(内核原样)。`0009-lok-word-count-values.patch`:`getCommandValues(".uno:WordCount")` 给文档与选区的字数、字符数;内核的 `.uno:StateWordCount` 状态是英文串,只作重读的信号。构建加 `--with-myspell-dicts`,随包词典取自 LibreOffice 的 dictionaries 子模块(core 钉的 9925407,来源与许可见各目录 README 与 license),装在 `Resources/extensions`。构建后只留 `dict-en`(25 MB,英语拼写、断词、同义词):界面语言是英文与简体中文,中文无 Hunspell 词典;全部 65 个(473 MB)时 LOK 首启 5.4 秒、再启 2.3 秒,只留英文为 1.1 秒与 0.7 秒(PERF-01 要求 3 秒内可操作)。词典语种已由用户定:英文。`0010-lok-outline-values.patch`:`getCommandValues(".uno:Outline")` 给各标题的文字、级别、位置(twips)与是否当前(光标所在处按文档顺序之前最近的标题,`GetOutlinePos`;分栏时位置比不出先后),`.uno:Bookmarks` 的每项加位置(版面不显示的书签无位置);导航栏的跳转都按位置。`0011-lok-font-lists-present-when-a-member-is-used.patch`:LOK 把以 `;` 连写的字体列表整串当作字体名,列表中有字体已用上也报缺失(实测报出界面字体的回退列表);补丁让列表中任一成员用上即不算缺失,文档里写的列表全缺时照报。`0012-lok-calc-cursor-references-merge.patch`:Calc 在回调注册后补发单元格光标;`.uno:SheetReferences?sheet=N` 给引用某表的公式格数、命名区域数、第一个公式格,以及用 INDIRECT 的公式与名称数(这类引用运行时才知指向,删除说明里单独列出);合并带 `MoveContents` 时不弹对话框,并修上游“多个数据格的 API 合并什么也不做”。worker 以 `SAL_LOK_OPTIONS=sc_print_twips_msgs`(另加 `unipoll`)运行,Calc 的光标、选区与 `.uno:SheetGeometryData` 同为文档 twips;视图把可见区经 `setClientVisibleArea` 报给内核。`0013-lok-author-option.patch`:`lo_setOption("author", 名)` 设内核用户全名,新批注与修订以此署名。`0014-lok-calc-formula-entry-and-names.patch`:公式语法错误不弹询问、补全不切走编辑、提交清引用标记、名称的可撤销定义删Line truncated
- 2026-10-04:LOK 命令与对话框。界面只经 `ely_lok::commands::COMMANDS` 发 UNO 命令,worker 拒绝表外命令;内核弹出的对话框(`LOK_CALLBACK_WINDOW` created 或 JSDialog)在回调里即刻关闭并经 `LokError` 报给应用,需要的对话框由 Ely 自建。起因:Calc 的 Backspace 弹同步模态“删除内容”,曾卡死 worker。
- 2026-10-04:ElyDoc 查找替换、拼写、比较、邮件合并(DOC-12 至 14)。查找经 `.uno:ExecuteSearch`:查全部计数并高亮,下一处按全部的矩形定当前序号;替换一处后重新计数,无当前项时“替换”先选下一处。拼写建议来自右键时内核给的上下文菜单,只取拼写部分,`ContextMenu::manual` 在插入点下打开。比较把当前文档存成工作副本、开成新标签、对它发 `.uno:CompareDocuments`,两个源文件都不动。邮件合并的字段在正文里写成 `«列名»`(同 Word 的显示,已定),预览与生成都是对模板副本整体替换;生成逐行写 DOCX 或 PDF 到所选文件夹,任一目标已存在即整体拒绝,从不发邮件。
- 2026-10-04:ElyDoc 输出与导航(DOC-15、16)。导出 PDF 经 worker 的 `EXPORT_PDF` 请求,滤镜选项为 JSON(页码范围、批注印在页边、标出修订);打印先导出同一 PDF,macOS 用 PDFKit 的 `NSPrintOperation` 弹系统打印面板(`ely_platform::print`),其他平台报错,未实现。加密文档经 LOK 的 `DOCUMENT_PASSWORD` 回调询问密码,错的密码再问一次,取消即不打开。左栏导航默认打开(大纲、页面、书签),跳转前记下所在处,“返回”逐级回到原处;上一跳的输入未落到内核、或落下后光标尚未移动时,以其目标为所在处,连续跳转不丢位置;落下之后即按实际光标。当前标题取内核的标记,当前页按光标所在页。导出 PDF 与另存为的目标按文件身份(符号链接与硬链接)比对打开着的文档:导出遇之即拒绝,另存为遇本文件时只许原格式。缺字体时页面上方给出提示条。QA-10 与 Microsoft Office 互开未验证:本机无 Office。
@@ -282,4 +282,5 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-09:每个应用只跑一份(缺陷清单 X-03)。`ely_ui::run` 在写日志之前取应用数据目录的 `instance.lock`(`ely_platform::instance::hold`;Unix 为 fcntl 记录锁,进程结束即放,被拒时以 `F_GETLK` 向系统问出持锁的进程号,不另存进程号文件,同时启动的几份也报得准;Windows 为 LockFileEx,不报进程号);已有一份在跑时第二份不启动,报“已在运行(进程号)”。原先第二份启动即清空第一份的工作目录、把它的恢复点当作干净结束删掉、改写崩溃锁。macOS 经访达打开文件本就交给运行中的那份;`open -n` 与 Linux 每文件一个进程时第二份被拒,文件须在已开的窗口里打开(未做转交)。`tests/scripts/one-instance.sh` 核对。
- 2026-10-10:ElyMark 的链接(M-05,我定,同常见编辑器):可视模式单击链接是编辑,Cmd 单击(Windows、Linux 为 Ctrl)交应用;预览单击即交应用;应用照 ElyPDF:http、https、mailto 交系统打开,其余提示拒绝。
- 2026-10-10:Windows 与 Linux 的构建环境(E-01)。Windows:MSVC(VS 2022,v143);MuPDF 由 `build-mupdf.sh` 以 MSBuild 编上游 `platform/win32` 的 `libmupdf`(静态运行库 /MT,同 Rust 的 `crt-static`;`HAVE_LIBCRYPTO`,`libmupdf.lib` 内含第三方、Tesseract 与 PKCS#7);静态 OpenSSL 取自钉在 2026.07.29 的 vcpkg(`engines/vcpkg`,`openssl:x64-windows-static`);Git 为 Git for Windows 的 MinGit 2.56.0(钉散列,`engines/git/cmd/git.exe`,LFS 在 `ucrt64/libexec/git-core`);CEF 照 cef-dll-sys 放在可执行文件旁(Windows、Linux 都是),不另载入;`%TEMP%` 下的程序被系统的应用控制拦下,构建与探针都不放那里;cmake 与 ninja 须在 PATH;LibreOffice 的 configure 只认 VS 自带的 CMake(组件 `VC.CMake.Project`)。仓库与 `reference/` 一律 LF(`core.autocrlf false`、`core.eol lf`;`fetch-references.sh` 对每个上游照设)。Linux:WSL 的 Ubuntu 26.04;静态 libcrypto 另需其 pkg-config 列出的库(jitterentropy、zstd、z)。LibreOffice 在 Linux 照 Collabora 的 LOKit 配置构建(无图形后端、cairo RGBA、自带各库)。
- 2026-10-10:Windows 与 Linux 的内核与 worker(W-01、K-01,用户定:0.1.3 先三端发布)。内核只认合并库(`ffi::KERNEL`:`libmergedlo.dylib`、`libmergedlo.so`、`mergedlo.dll`;`sofficeapp` 在合并构建里是文本占位);Windows 载入时一并搜索该 DLL 所在的 `program/`。查找次序照旧,Windows 与 Linux 的包内位置是应用旁的 `libreoffice/program`,开发构建为 `engines/lo-src/instdir/program`。worker 的主循环:macOS 照旧以 GCD;Linux 以 unipoll,`runLoop` 的轮询回调里每次答一个请求,内核的唤醒回调往同一队列放信号(`worker/unipoll.rs`;回调的数据指针须非空,VCL 见空即不调用);Windows 也以 unipoll:LOK 不另起主线程,worker 的主线程跑 `runLoop`(Windows 的消息循环),请求经只收消息的窗口投到这条线程上执行(`worker/messages.rs`),同 macOS 的 GCD;原先 LOK 自起主线程、worker 在另一线程作答,载入表格即死锁。Windows 的循环不调轮询回调,但须传非空的回调,`isUnipoll` 只看它。补丁 `0043-lok-callbacks-flush-inline-everywhere.patch`:回调队列在各平台都即时冲出(原只 macOS),否则请求里等的命令结果与状态要等下一轮主循环才到。`0042-svx-boost-json-needs-no-library.patch`:svxcore 与 sd 定义 `BOOST_ALL_NO_LIB`,MSVC 不再按 Boost 的自动链接索要 `libboost_json`、`libboost_container`。Linux 的内核以 cairo 画图块(worker 设 `SAL_DISABLESKIA`;构建去掉 `--enable-cairo-rgba`,自带的 cairo 要清掉重建),字节为 BGRA,同 macOS 与 Windows;`getTileMode` 只看 cairo 开关,不可信。worker 启动核对在三端都画一段红字,按 BGRA 找不到红色即拒绝。`scripts/apply-lo-patches.sh` 的临时库设 `core.autocrlf false`(Windows 默认转换使任何前缀都对不上)。Windows 的 LibreOffice 在 Cygwin 下构建(`build-libreoffice.sh` 的 CYGWIN 分支:去掉 Unix 专用开关,加 VS 2022,用 `/opt/lo/bin` 的 make,清掉 Git Bash 带进来的 `MSYSTEM`);构建目录与 Cygwin 排除在 Defender 扫描之外(否则并行 configure 删不掉 `conftest.dir`)。
- 2026-10-09(用户定):代码与发布从 GitHub 搬到自建的 Gitea(git.zacharyzhang.com;GitHub 账号被封,推送与发布都被拒)。公开仓库 `ZacharyZhang-NY/ElyOffice`(原样推上 main、缺陷清单分支与 v0.1.0、v0.1.1 标签),组件库为同站的 `ZacharyZhang-NY/Ely-GPUI-Components`(Cargo 的 git 依赖与 `fetch-references.sh` 随之改址,钉的提交不变)。v0.1.1 的六个 dmg 与两个源码包重新发在 Gitea 的 v0.1.1 发布里。官网的下载与源码链接指向 Gitea(`releases/latest` 会跳到最新发布页)。`.github/workflows` 删去(它们靠 GitHub 的 macOS 机器与 `gh`,发布改在本机以 `packaging/macos/release.sh` 构建、经 Gitea API 上传)。发布视频里写的地址已在源文件改为 Gitea,已发布的成片仍是旧地址,重渲染后才更新。上游项目(LibreOffice、MuPDF、git-lfs、tessdata 等)照旧从它们自己的 GitHub 地址取。
+4 -5
View File
@@ -9,13 +9,12 @@ use serde_json::{Value, json};
use crate::view::Heading;
/// The app's Resources, else this build's `engines/content`.
/// Beside the app, its Resources, else this build's.
pub fn content() -> Result<PathBuf> {
let bundled = std::env::current_exe()?
.parent()
.and_then(|dir| Some(dir.parent()?.join("Resources/content")));
let exe = std::env::current_exe()?;
let dir = exe.parent().unwrap_or(Path::new("."));
let built = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../engines/content/ely-mark");
for dir in bundled.into_iter().chain([built]) {
for dir in [dir.join("content"), dir.join("../Resources/content"), built] {
if dir.join("index.html").is_file() {
log::info!("mark: page from {}", dir.display());
return Ok(dir);
+10 -8
View File
@@ -150,14 +150,14 @@ impl Worker {
command
.arg(install)
.arg(profile_dir)
.env("SAL_FORCE_HIDPI_SCALING", "1")
.env("SAL_SKIA", "raster");
// Calc reports in document twips, as its geometry does.
let options = match cfg!(target_os = "macos") {
true => "unipoll:sc_print_twips_msgs",
false => "sc_print_twips_msgs",
.env("SAL_FORCE_HIDPI_SCALING", "1");
// Linux tiles via cairo are BGRA; Skia there is RGBA.
match cfg!(target_os = "linux") {
true => command.env("SAL_DISABLESKIA", "1"),
false => command.env("SAL_SKIA", "raster"),
};
command.env("SAL_LOK_OPTIONS", options);
// Calc reports in document twips; unipoll lends LOK's loop.
command.env("SAL_LOK_OPTIONS", "unipoll:sc_print_twips_msgs");
if dev {
command.arg("--dev");
}
@@ -370,7 +370,9 @@ impl Worker {
// Nothing reaches a worker that has not passed its checks.
self.ready()?;
self.asked += 1;
ipc::write_frame(&mut self.stdin, request, self.asked, payload)?;
if let Err(error) = ipc::write_frame(&mut self.stdin, request, self.asked, payload) {
return Err(self.broken(request, error));
}
let (reply, id, body) = match self.replies.recv() {
Ok(Ok(frame)) => frame,
Ok(Err(error)) => return Err(self.broken(request, error)),
+12 -8
View File
@@ -22,6 +22,12 @@ pub fn templates(kind: &str) -> io::Result<Vec<PathBuf>> {
Ok(found)
}
/// The kernel's folder beside the app, and in this build.
#[cfg(target_os = "macos")]
const LAYOUT: (&str, &str) = ("../Frameworks", "instdir/LibreOffice.app/Contents/Frameworks");
#[cfg(not(target_os = "macos"))]
const LAYOUT: (&str, &str) = ("libreoffice/program", "instdir/program");
/// The LOK build: `ELY_LOK_DIR`, the app bundle's, or engines/.
pub fn install_dir() -> io::Result<PathBuf> {
if let Ok(dir) = std::env::var("ELY_LOK_DIR") {
@@ -35,17 +41,15 @@ pub fn install_dir() -> io::Result<PathBuf> {
))
};
}
let (beside, built) = LAYOUT;
let library = crate::ffi::KERNEL;
let exe = std::env::current_exe()?;
let bundled = exe
.parent()
.unwrap_or(Path::new("."))
.join("../Frameworks");
if cfg!(target_os = "macos") && bundled.join("libmergedlo.dylib").try_exists()? {
let bundled = exe.parent().unwrap_or(Path::new(".")).join(beside);
if bundled.join(library).try_exists()? {
return Ok(bundled);
}
let dev = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../engines/lo-src/instdir/LibreOffice.app/Contents/Frameworks");
if dev.try_exists()? {
let dev = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../engines/lo-src").join(built);
if dev.join(library).try_exists()? {
return Ok(dev);
}
Err(io::Error::new(
+14 -9
View File
@@ -157,20 +157,20 @@ fn the_worker_round_trips_a_document() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let url = format!("file://{}", source.display());
let url = crate::file_url(&source);
let doc = worker.load(&url).expect("the document loads");
let (width, height) = doc.size(&mut worker).expect("the size");
assert!(width > 0 || height > 0, "a nonzero document size");
let odt = dir.path().join("note.odt");
doc.save_as(&mut worker, &format!("file://{}", odt.display()), "odt")
doc.save_as(&mut worker, &crate::file_url(&odt), "odt")
.expect("the odt save");
assert!(odt.try_exists().expect("the odt exists"));
let doc2 = worker
.load(&format!("file://{}", odt.display()))
.load(&crate::file_url(&odt))
.expect("the odt reloads");
let pdf = dir.path().join("note.pdf");
doc2.save_as(&mut worker, &format!("file://{}", pdf.display()), "pdf")
doc2.save_as(&mut worker, &crate::file_url(&pdf), "pdf")
.expect("the pdf save");
let head = std::fs::read(&pdf).expect("the pdf reads");
assert!(head.starts_with(b"%PDF"), "a real PDF comes out");
@@ -187,7 +187,7 @@ fn a_missing_file_is_an_error_never_a_success() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker");
let missing = dir.path().join("gone.odt");
let outcome = worker.load(&format!("file://{}", missing.display()));
let outcome = worker.load(&crate::file_url(&missing));
assert!(outcome.is_err(), "LOK-09: the failure is an error");
}
@@ -209,7 +209,12 @@ fn a_failed_check_refuses_every_later_request() {
/// Initializes the profile fully, the CLI way (registers Basic).
fn seed_basic(install: &Path, profile: &Path) {
#[cfg(target_os = "macos")]
let soffice = install.parent().expect("the app dir").join("MacOS/soffice");
#[cfg(target_os = "linux")]
let soffice = install.join("soffice.bin");
#[cfg(windows)]
let soffice = install.join("soffice.exe");
let url = ipc::file_url_of(profile);
let status = std::process::Command::new(&soffice)
.args(["--headless", "--norestore", "--terminate_after_init"])
@@ -249,7 +254,7 @@ fn a_macro_never_writes_its_marker() {
let source = dir.path().join("macro.fodt");
std::fs::write(&source, macro_fodt(&marker)).expect("the macro fodt writes");
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the macro document loads");
std::thread::sleep(std::time::Duration::from_millis(800));
assert!(
@@ -313,7 +318,7 @@ fn shutdown_leaves_no_locks_or_temp() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let lock = dir.path().join(".~lock.note.fodt#");
worker.close_doc(doc).expect("the document closes");
@@ -347,7 +352,7 @@ fn each_load_is_its_own_session() {
let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker spawns");
let source = dir.path().join("note.fodt");
write(&source, FODT);
let url = format!("file://{}", source.display());
let url = crate::file_url(&source);
// Freed native documents reuse addresses; sessions never repeat.
let mut old = Vec::new();
for _ in 0..4 {
@@ -375,7 +380,7 @@ fn a_document_belongs_to_its_worker() {
let dir = tempfile::tempdir().expect("a temp dir");
let source = dir.path().join("note.fodt");
write(&source, FODT);
let url = format!("file://{}", source.display());
let url = crate::file_url(&source);
let mut first = Worker::spawn(&install, &dir.path().join("one")).expect("a worker");
let mut second = Worker::spawn(&install, &dir.path().join("two")).expect("another worker");
let theirs = first.load(&url).expect("loads");
+2 -2
View File
@@ -240,7 +240,7 @@ fn a_kernel_dialog_is_closed_and_reported() {
let source = dir.path().join("in.fods");
write(&source, FODS);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the sheet loads");
let base = poll_until(&mut worker, &doc, &Events::default(), SETTLE, |_| false);
// Calc's Backspace opens Delete Contents.
@@ -436,7 +436,7 @@ fn an_owned_save_clears_the_modified_state() {
state(seen, ".uno:ModifiedStatus") == Some("true")
});
let copy = dir.path().join("copy.docx");
doc.save_owned(&mut worker, &format!("file://{}", copy.display()), "docx")
doc.save_owned(&mut worker, &crate::file_url(&copy), "docx")
.expect("the owned save");
let saved = poll_until(&mut worker, &doc, &base, SETTLE, |seen| {
state(seen, ".uno:ModifiedStatus") == Some("false")
+3 -2
View File
@@ -12,7 +12,7 @@ fn open() -> (tempfile::TempDir, Worker, Doc, Events) {
let source = dir.path().join("in.fods");
write(&source, FODS);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the sheet loads");
let seen = poll_until(&mut worker, &doc, &Events::default(), SETTLE, |seen| {
seen.cell.cursor.is_some()
@@ -29,7 +29,8 @@ fn type_text(worker: &mut Worker, doc: &Doc, text: &str) {
/// The cells of `range`, as the kernel copies them.
fn values(worker: &mut Worker, doc: &Doc, range: &str) -> String {
worker.run(doc, "cell.goto", &[range]).expect("the range");
doc.selection_text(worker).expect("the text")
// Windows ends copied lines with CRLF.
doc.selection_text(worker).expect("the text").replace("\r\n", "\n")
}
#[test]
+6 -6
View File
@@ -24,7 +24,7 @@ pub(super) fn open(body: &str) -> (tempfile::TempDir, Worker, Doc) {
let source = dir.path().join("in.fodt");
write(&source, body);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
(dir, worker, doc)
}
@@ -32,7 +32,7 @@ pub(super) fn open(body: &str) -> (tempfile::TempDir, Worker, Doc) {
/// The document saved as flat XML.
pub(super) fn saved(dir: &tempfile::TempDir, worker: &mut Worker, doc: &Doc, name: &str) -> String {
let out = dir.path().join(name);
doc.save_as(worker, &format!("file://{}", out.display()), "fodt")
doc.save_as(worker, &crate::file_url(&out), "fodt")
.expect("the save works");
std::fs::read_to_string(&out).expect("the save reads")
}
@@ -210,7 +210,7 @@ fn copy_gives_text_and_html_and_paste_takes_html() {
let target = dir.path().join("b.fodt");
write(&target, FODT);
let other = worker
.load(&format!("file://{}", target.display()))
.load(&crate::file_url(&target))
.expect("b loads");
click(&mut worker, &other, 150, 150);
worker
@@ -312,7 +312,7 @@ fn a_calc_cut_finishes_without_a_dialog() {
let source = root.join("in.fods");
write(&source, FODS);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the sheet loads");
worker
.post_key(&doc, 0, 1027 | 0x1000)
@@ -320,7 +320,7 @@ fn a_calc_cut_finishes_without_a_dialog() {
let copied = worker.copy(&doc).expect("copy runs");
worker.cut(&doc).expect("the cut runs");
let out = root.join("out.fods");
doc.save_as(&mut worker, &format!("file://{}", out.display()), "fods")
doc.save_as(&mut worker, &crate::file_url(&out), "fods")
.expect("the save works");
let xml = std::fs::read_to_string(&out).expect("the save reads");
done.send((copied.len(), xml.contains("Hello")))
@@ -367,7 +367,7 @@ fn a_table_survives_copy_and_paste() {
let target = dir.path().join("b.fodt");
write(&target, FODT);
let other = worker
.load(&format!("file://{}", target.display()))
.load(&crate::file_url(&target))
.expect("b loads");
click(&mut worker, &other, 150, 150);
worker
+1 -1
View File
@@ -13,7 +13,7 @@ fn keys_never_wait_behind_the_event_watch() {
let source = dir.path().join("in.fods");
write(&source, FODS);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.unwrap();
let feed = worker.feed();
let shared = std::sync::Arc::new(std::sync::Mutex::new(worker));
+1 -2
View File
@@ -44,8 +44,7 @@ fn a_locked_document_asks_for_its_password() {
for password in ["", "wrong"] {
let error = worker
.unlock(&url, password)
.err()
.expect("a locked load fails");
.expect_err("a locked load fails");
assert_eq!(
error.kind(),
std::io::ErrorKind::PermissionDenied,
+15 -15
View File
@@ -16,7 +16,7 @@ fn a_tile_paints_real_pixels() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let tile = doc
.paint_tile(&mut worker, (0, 0, 960, 960), (64, 64))
@@ -51,7 +51,7 @@ fn an_edit_bumps_the_generation_and_invalidates() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let base = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false);
let before = doc
@@ -95,7 +95,7 @@ fn uno_commands_invalidate_and_repaint() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let base = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false);
let before = doc
@@ -137,7 +137,7 @@ fn a_red_span_paints_red_bytes() {
let source = dir.path().join("red.html");
write(&source, RED_HTML);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let tile = doc
.paint_tile(&mut worker, (0, 0, 2000, 1000), (64, 64))
@@ -162,12 +162,12 @@ fn a_key_press_edits_the_document() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
click(&mut worker, &doc, 150, 150);
worker.post_key(&doc, 0, 1280).expect("Return posts");
let out = dir.path().join("after.fodt");
doc.save_as(&mut worker, &format!("file://{}", out.display()), "fodt")
doc.save_as(&mut worker, &crate::file_url(&out), "fodt")
.expect("the save works");
let before = std::fs::read_to_string(&source).expect("the source reads");
let after = std::fs::read_to_string(&out).expect("the save reads");
@@ -187,7 +187,7 @@ fn selection_only_requests_keep_the_generation() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let base = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false);
click(&mut worker, &doc, 150, 150);
@@ -211,7 +211,7 @@ fn bad_tile_geometry_is_an_error_and_the_worker_lives() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
for (x, y, w, h, pw, ph) in [
(0, 0, 0, 960, 64, 64),
@@ -243,7 +243,7 @@ fn a_long_wait_is_clamped_and_requests_flow() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let started = std::time::Instant::now();
let batch = worker
@@ -270,7 +270,7 @@ fn polling_times_out_with_the_same_generation() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let base = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false);
let again = poll_until(&mut worker, &doc, &base, QUIET, |_| false);
@@ -291,16 +291,16 @@ fn uno_commands_reach_their_own_document() {
write(&first, FODT);
write(&second, FODT);
let a = worker
.load(&format!("file://{}", first.display()))
.load(&crate::file_url(&first))
.expect("a loads");
let b = worker
.load(&format!("file://{}", second.display()))
.load(&crate::file_url(&second))
.expect("b loads");
worker.run(&a, "edit.select_all", &[]).expect("select all");
worker.cut(&a).expect("the cut runs");
let saved = |doc: &Doc, worker: &mut Worker, name: &str| {
let out = dir.path().join(name);
doc.save_as(worker, &format!("file://{}", out.display()), "fodt")
doc.save_as(worker, &crate::file_url(&out), "fodt")
.expect("the save works");
std::fs::read_to_string(&out).expect("the save reads")
};
@@ -327,7 +327,7 @@ fn writer_reports_its_page_rects() {
let source = dir.path().join("note.fodt");
write(&source, FODT);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let pages = doc.pages(&mut worker).expect("the page rects");
let (width, height) = doc.size(&mut worker).expect("the size");
@@ -361,7 +361,7 @@ fn the_page_follows_a_dark_theme_and_back() {
let source = dir.path().join(name);
write(&source, text);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
let area = (1440, 1440, 3840, 3840);
let seen = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false);
+11 -5
View File
@@ -154,12 +154,13 @@ fn text_exports_write_the_shown_sheet_as_values() {
let out = dir.path().join("out.csv");
doc.export_text(&mut worker, &crate::file_url(&out), ';')
.expect("exports S1");
let first = std::fs::read_to_string(&out).expect("reads");
// Windows writes CSV lines with CRLF, as RFC 4180 does.
let first = std::fs::read_to_string(&out).expect("reads").replace("\r\n", "\n");
assert_eq!(first, "Name;Total\nSum;3\n", "values, not formulas");
doc.show_sheet(&mut worker, 1).expect("S2");
doc.export_text(&mut worker, &crate::file_url(&out), ';')
.expect("exports S2");
let second = std::fs::read_to_string(&out).expect("reads");
let second = std::fs::read_to_string(&out).expect("reads").replace("\r\n", "\n");
assert_eq!(second, "Item;Note\nPen;\"a;b\"\nInk;2\n", "only S2");
}
@@ -284,11 +285,16 @@ fn excel_caches_recompute_and_unknown_functions_show_errors() {
let end = xml[at..].find("</row>").expect("row one") + at + "</row>".len();
std::fs::write(&sheet, format!("{}{row}{}", &xml[..at], &xml[end..])).expect("writes");
let excel = dir.path().join("excel.xlsx");
let zip = std::process::Command::new("zip")
// Python's zipfile, as ooxml.py uses: no zip on Windows.
let pack = "import os, sys, zipfile
with zipfile.ZipFile(sys.argv[1], 'w', zipfile.ZIP_DEFLATED) as z:
for d, _, fs in os.walk('.'):
for f in fs:
z.write(os.path.join(d, f))";
let zip = std::process::Command::new("python3")
.current_dir(&unpacked)
.args(["-q", "-r"])
.args(["-c", pack])
.arg(&excel)
.arg(".")
.status()
.expect("zip runs");
assert!(zip.success());
+3 -2
View File
@@ -50,7 +50,8 @@ fn open() -> (tempfile::TempDir, Worker, Doc, Events) {
/// The cells of `range`, as the kernel copies them.
fn cells(worker: &mut Worker, doc: &Doc, range: &str) -> String {
worker.run(doc, "cell.goto", &[range]).expect("the range");
doc.selection_text(worker).expect("the text")
// Windows ends copied lines with CRLF.
doc.selection_text(worker).expect("the text").replace("\r\n", "\n")
}
/// The latest report of `name`.
@@ -112,7 +113,7 @@ fn number_formats_alignment_and_fills_apply() {
// The cell properties of the document's own cell formats.
let saved = |worker: &mut Worker| {
let out = _dir.path().join("borders.fods");
doc.save_as(worker, &format!("file://{}", out.display()), "fods")
doc.save_as(worker, &crate::file_url(&out), "fods")
.expect("the save");
let xml = std::fs::read_to_string(&out).expect("the saved book");
let start = xml.find("<office:automatic-styles>").expect("cell formats");
+2 -2
View File
@@ -37,7 +37,7 @@ fn open_sheet() -> (tempfile::TempDir, Worker, Doc) {
let source = dir.path().join("in.fods");
write(&source, FODS);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the sheet loads");
(dir, worker, doc)
}
@@ -174,7 +174,7 @@ pub(super) fn open_book(text: &str) -> (tempfile::TempDir, Worker, Doc) {
let source = dir.path().join("book.fods");
write(&source, text);
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the book loads");
doc.visible_area(&mut worker, (0, 0, 15000, 10000), (256, 3840))
.expect("the area");
+46 -73
View File
@@ -185,7 +185,6 @@ impl Lok {
call!(class, 10, F, self.kit, url_c.as_ptr())
}
#[cfg(target_os = "macos")]
/// Answers the kernel's "input waiting?" without a run-loop peek.
pub fn register_any_input(&self, waiting: extern "C" fn(*mut c_void, i32) -> bool) {
static DATA: u8 = 0;
@@ -196,29 +195,30 @@ impl Lok {
call!(class, 24, F, self.kit, waiting, data);
}
#[cfg(target_os = "macos")]
/// Runs LOK's loop until kit exit (R-01).
pub fn run_loop(&self) {
type PollCb = Option<extern "C" fn(*mut c_void, i32) -> i32>;
type WakeCb = Option<extern "C" fn(*mut c_void)>;
extern "C" fn poll(_: *mut c_void, _: i32) -> i32 {
0
}
extern "C" fn wake(_: *mut c_void) {}
/// Runs LOK's loop until kit exit; unipoll calls `poll`.
pub fn run_loop(&self, poll: Option<Poll>, wake: Option<Wake>) {
// VCL calls neither hook while their data is null.
static DATA: u8 = 0;
let data = std::ptr::addr_of!(DATA).cast_mut().cast();
let class = unsafe { (*self.kit).class };
type F = unsafe extern "C" fn(*mut Kit, PollCb, WakeCb, *mut c_void);
call!(
class,
12,
F,
self.kit,
Some(poll),
Some(wake),
std::ptr::null_mut()
);
type F = unsafe extern "C" fn(*mut Kit, Option<Poll>, Option<Wake>, *mut c_void);
call!(class, 12, F, self.kit, poll, wake, data);
}
}
/// The kernel library: every build here merges its libraries.
#[cfg(target_os = "macos")]
pub const KERNEL: &str = "libmergedlo.dylib";
#[cfg(target_os = "linux")]
pub const KERNEL: &str = "libmergedlo.so";
#[cfg(windows)]
pub const KERNEL: &str = "mergedlo.dll";
/// Waits up to its microseconds; above zero, work was done.
pub type Poll = extern "C" fn(*mut c_void, i32) -> i32;
/// The kernel has work for its loop.
pub type Wake = extern "C" fn(*mut c_void);
impl Drop for Lok {
fn drop(&mut self) {
if self.kit.is_null() {
@@ -244,37 +244,17 @@ struct Library {
#[cfg(unix)]
impl Library {
/// dlopens the LOK library, merged candidates in order.
/// dlopens the merged kernel from `install`.
fn open(install: &Path) -> io::Result<Self> {
use std::os::unix::ffi::OsStrExt;
let mut last = "no candidate".to_string();
for name in [
"libsofficeapp.dylib",
"libmergedlo.dylib",
"libsofficeapp.so",
"libmergedlo.so",
] {
let path = install.join(name);
if !path.try_exists()? {
continue;
}
let c = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "the path holds a NUL"))?;
let handle = unsafe { libc::dlopen(c.as_ptr(), libc::RTLD_LAZY | libc::RTLD_GLOBAL) };
if handle.is_null() {
last = dl_error();
log::warn!("lok: dlopen {} failed: {last}", path.display());
continue;
}
return Ok(Self { handle });
let path = install.join(KERNEL);
let c = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "the path holds a NUL"))?;
let handle = unsafe { libc::dlopen(c.as_ptr(), libc::RTLD_LAZY | libc::RTLD_GLOBAL) };
if handle.is_null() {
return Err(io::Error::other(format!("dlopen {}: {}", path.display(), dl_error())));
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"no loadable LOK library under {}: {last}",
install.display()
),
))
Ok(Self { handle })
}
/// dlsyms `name` from the library.
@@ -308,36 +288,29 @@ struct Library {
#[cfg(windows)]
impl Library {
/// Loads `<install>/sofficeapp.dll`, merged fallback.
/// Loads the merged kernel from `install`.
fn open(install: &Path) -> io::Result<Self> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::System::LibraryLoader::{
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LoadLibraryExW,
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LoadLibraryExW,
};
for name in ["sofficeapp.dll", "mergedlo.dll"] {
let path = install.join(name);
if !path.try_exists()? {
continue;
}
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let handle = unsafe {
LoadLibraryExW(
wide.as_ptr(),
std::ptr::null_mut(),
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
)
};
if handle.is_null() {
return Err(io::Error::last_os_error());
}
return Ok(Self {
handle: handle as isize,
});
let path = install.join(KERNEL);
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let handle = unsafe {
LoadLibraryExW(
wide.as_ptr(),
std::ptr::null_mut(),
// Its dependencies sit beside it, in program/.
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
)
};
if handle.is_null() {
let error = io::Error::last_os_error();
return Err(io::Error::other(format!("LoadLibrary {}: {error}", path.display())));
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no LOK library under {}", install.display()),
))
Ok(Self {
handle: handle as isize,
})
}
/// Gets `name` from the library.
-19
View File
@@ -80,25 +80,6 @@ impl Document {
call!(class, 13, F, self.doc, callback, data);
}
/// Paints the document into `buffer` at `width` x `height` (R-01).
pub fn paint_tile(&self, buffer: &mut [u8], width: i32, height: i32) {
let class = unsafe { (*self.doc).class };
type F = unsafe extern "C" fn(*mut Doc, *mut u8, i32, i32, i32, i32, i32, i32);
call!(
class,
9,
F,
self.doc,
buffer.as_mut_ptr(),
width,
height,
0,
0,
width * 15,
height * 15
);
}
/// Runs a UNO command with JSON `args` (LOK-08).
pub fn post_uno(&self, command: &str, args: Option<&str>) -> io::Result<()> {
self.dispatch(command, args, false)
+6 -11
View File
@@ -3,12 +3,10 @@
use std::{io, path::Path};
use crate::client::{BUILD_ID, Worker};
#[cfg(target_os = "macos")]
use crate::ipc::{self, Pack, Unpack, tag};
/// A tiny flat-XML document for the tile self-check.
#[cfg(target_os = "macos")]
const VERIFY_DOC: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:document xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\" office:version=\"1.3\" office:mimetype=\"application/vnd.oasis.opendocument.text\"><office:body><office:text><text:p>verify</text:p></office:text></office:body></office:document>";
/// Red text: a BGRA tile shows red in byte two.
const VERIFY_DOC: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:document xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\" office:version=\"1.3\" office:mimetype=\"application/vnd.oasis.opendocument.text\"><office:automatic-styles><style:style style:name=\"T1\" style:family=\"text\"><style:text-properties fo:color=\"#ff0000\" fo:font-size=\"28pt\" fo:font-weight=\"bold\"/></style:style></office:automatic-styles><office:body><office:text><text:p><text:span text:style-name=\"T1\">verify</text:span></text:p></office:text></office:body></office:document>";
impl Worker {
/// Rejects builds that are not the pinned one (LOK-01).
@@ -16,13 +14,11 @@ impl Worker {
if !build.contains(BUILD_ID) {
return self.reject(format!("not the pinned build {BUILD_ID}: {build}"));
}
#[cfg(target_os = "macos")]
if !self.tile_self_check(profile_dir)? {
return self.reject(format!(
"the pinned build {BUILD_ID} does not paint tiles (unpatched)"
"the pinned build {BUILD_ID} paints no BGRA red (unpatched, or another order)"
));
}
let _ = profile_dir;
Ok(())
}
@@ -53,15 +49,14 @@ impl Worker {
Ok(build)
}
/// Paints one tile; the bytes it gained.
#[cfg(target_os = "macos")]
/// Paints the red text; whether BGRA red came out.
pub(crate) fn tile_self_check(&mut self, profile_dir: &Path) -> io::Result<bool> {
let doc = profile_dir.join("lok-verify.fodt");
std::fs::write(&doc, VERIFY_DOC)?;
let url = ipc::file_url_of(&doc.canonicalize()?);
let payload = self.request(tag::VERIFY, &Pack::new().str(&url).bytes())?;
let gained = Unpack::new(&payload).u64()?;
Ok(gained > 0)
let red = Unpack::new(&payload).u64()?;
Ok(red > 0)
}
}
+3 -3
View File
@@ -92,6 +92,9 @@ pub mod tag {
pub const ERR: u8 = 101;
}
/// How a refusal for a changed document begins.
pub const STALE: &str = "stale: ";
#[cfg(test)]
mod tests {
#[test]
@@ -103,6 +106,3 @@ mod tests {
assert!(super::path_of_file_url("file:///bad%zz").is_err());
}
}
/// How a refusal for a changed document begins.
pub const STALE: &str = "stale: ";
+1 -1
View File
@@ -81,7 +81,7 @@ fn a_failed_input_still_reports_the_revision_it_reached() {
let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker");
let source = dir.path().join("book.fods");
std::fs::write(&source, crate::client::tests::input::FODS).expect("the fixture writes");
let doc = worker.load(&format!("file://{}", source.display())).expect("the book loads");
let doc = worker.load(&crate::file_url(&source)).expect("the book loads");
let worker = Mutex::new(worker);
// Nothing merged to split: the kernel refuses.
let input = Input::Run { commands: vec![("cells.split", Vec::new())], at: None };
+1 -1
View File
@@ -50,7 +50,7 @@ pub(super) fn live_view_of(
let source = dir.path().join(name);
std::fs::write(&source, body).expect("the fixture writes");
let doc = worker
.load(&format!("file://{}", source.display()))
.load(&crate::file_url(&source))
.expect("the document loads");
std::mem::forget(dir);
let shared = Arc::new(Mutex::new(worker));
+1 -1
View File
@@ -39,7 +39,7 @@ fn saved(view: &Entity<LokView>, cx: &mut TestAppContext) -> String {
let view = view.read(app);
let mut worker = view.worker.lock().expect("the worker lock");
view.doc
.save_as(&mut worker, &format!("file://{}", out.display()), "fods")
.save_as(&mut worker, &crate::file_url(&out), "fods")
.expect("the save works");
});
std::fs::read_to_string(&out).expect("the save reads")
+2 -2
View File
@@ -20,7 +20,7 @@ fn saved(view: &Entity<LokView>, cx: &mut TestAppContext) -> Vec<String> {
let view = view.read(app);
let mut worker = view.worker.lock().expect("the worker lock");
view.doc
.save_as(&mut worker, &format!("file://{}", out.display()), "fodt")
.save_as(&mut worker, &crate::file_url(&out), "fodt")
.expect("the save works");
});
paragraphs(&std::fs::read_to_string(&out).expect("the save reads"))
@@ -263,7 +263,7 @@ fn the_clipboard_moves_html_tables_and_images() {
let view = view.read(app);
let mut worker = view.worker.lock().expect("the worker lock");
view.doc
.save_as(&mut worker, &format!("file://{}", out.display()), "fodt")
.save_as(&mut worker, &crate::file_url(&out), "fodt")
.expect("the save works");
});
let xml = std::fs::read_to_string(&out).expect("the save reads");
+20 -23
View File
@@ -16,6 +16,14 @@ mod revision;
mod slides;
mod teardown;
mod text;
#[cfg(windows)]
mod messages;
#[cfg(windows)]
use messages::serve;
#[cfg(target_os = "linux")]
mod unipoll;
#[cfg(target_os = "linux")]
use unipoll::serve;
/// English kernel, desktop hints; macros stay off (LOK-11).
const LOAD_OPTIONS: &str = "Language=en-US,DeviceFormFactor=desktop";
@@ -49,7 +57,12 @@ fn serve(lok: Lok, input: impl io::Read + Send + 'static) -> io::Result<()> {
}
std::thread::spawn(move || reader(input));
std::thread::spawn(events::push);
lok.run_loop();
// Aqua never polls: GCD brings the requests.
extern "C" fn poll(_: *mut std::ffi::c_void, _: i32) -> i32 {
0
}
extern "C" fn wake(_: *mut std::ffi::c_void) {}
lok.run_loop(Some(poll), Some(wake));
Ok(())
}
@@ -146,23 +159,6 @@ mod gcd {
use _dispatch_main_q as _raw;
}
#[cfg(not(target_os = "macos"))]
fn serve(lok: Lok, mut input: impl io::Read) -> io::Result<()> {
let lok = Box::leak(Box::new(lok));
pin(lok);
lok.register_callback(password::on_kit);
std::thread::spawn(events::push);
loop {
match ipc::read_frame(&mut input)? {
Some((tag, id, payload)) => answer(lok, tag, id, &payload),
None => {
log::info!("lok-worker: stdin closed");
teardown::teardown(0);
}
}
}
}
/// Handles one frame and writes its answer.
fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) {
let out = OUT.get().expect("the output is set");
@@ -375,15 +371,16 @@ fn docs() -> &'static Mutex<std::collections::HashMap<u64, Document>> {
DOCS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
}
/// Loads and paints; the bytes gained (R-01).
/// Loads and paints red text; its BGRA red pixels (R-01).
fn verify(lok: &Lok, url: &str) -> io::Result<u64> {
let doc = lok.load(url, LOAD_OPTIONS)?;
let mut tile = vec![0xABu8; 64 * 64 * 4];
doc.paint_tile(&mut tile, 64, 64);
// Past the 2 cm margin, where the text sits.
doc.paint_rect(&mut tile, (64, 64), (0, 0, 3000, 3000));
drop(doc);
let gained = tile.iter().filter(|byte| **byte != 0xAB).count() as u64;
log::info!("lok-worker: tile self-check gained {gained} bytes");
Ok(gained)
let red = tile.chunks_exact(4).filter(|px| px[2] > 200 && px[0] < 80 && px[1] < 80).count();
log::info!("lok-worker: tile self-check found {red} red pixels");
Ok(red as u64)
}
/// Borrows a live document for a read call.
+133
View File
@@ -0,0 +1,133 @@
//! Windows: requests ride the kernel's message loop to a window.
use std::{
ffi::c_void,
io,
sync::atomic::{AtomicPtr, AtomicUsize, Ordering},
};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, HWND_MESSAGE, PostMessageW, RegisterClassW, WM_APP,
WNDCLASSW,
},
};
use super::{answer, events, lok, password, pin, quit, teardown};
use crate::ffi::Lok;
use crate::ipc;
const ASK: u32 = WM_APP + 1;
const CLOSED: u32 = WM_APP + 2;
/// The message-only window the requests go to.
static WINDOW: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
/// Requests posted and not yet answered.
static PENDING: AtomicUsize = AtomicUsize::new(0);
pub(super) fn serve(lok: Lok, input: impl io::Read + Send + 'static) -> io::Result<()> {
let lok = Box::leak(Box::new(lok));
pin(lok);
lok.register_callback(password::on_kit);
lok.register_any_input(waiting);
WINDOW.store(window()?, Ordering::SeqCst);
std::thread::spawn(move || reader(input));
std::thread::spawn(events::push);
// Unipoll needs hooks; the Windows loop never calls them.
extern "C" fn poll(_: *mut c_void, _: i32) -> i32 {
0
}
extern "C" fn wake(_: *mut c_void) {}
lok.run_loop(Some(poll), Some(wake));
log::info!("lok-worker: the kernel's loop ended");
teardown::teardown(0);
}
/// A window on this thread that only takes messages.
fn window() -> io::Result<HWND> {
let name: Vec<u16> = "ElyLokWorker\0".encode_utf16().collect();
let instance = unsafe { GetModuleHandleW(std::ptr::null()) };
let class = WNDCLASSW {
lpfnWndProc: Some(procedure),
hInstance: instance,
lpszClassName: name.as_ptr(),
..unsafe { std::mem::zeroed() }
};
if unsafe { RegisterClassW(&class) } == 0 {
return Err(io::Error::last_os_error());
}
let made = unsafe {
CreateWindowExW(
0,
name.as_ptr(),
std::ptr::null(),
0,
0,
0,
0,
0,
HWND_MESSAGE,
std::ptr::null_mut(),
instance,
std::ptr::null(),
)
};
match made.is_null() {
true => Err(io::Error::last_os_error()),
false => Ok(made),
}
}
fn reader(mut input: impl io::Read) {
loop {
let (message, request) = match ipc::read_frame(&mut input) {
Ok(Some(request)) => {
PENDING.fetch_add(1, Ordering::SeqCst);
(ASK, Box::into_raw(Box::new(request)))
}
Ok(None) => {
log::info!("lok-worker: stdin closed");
(CLOSED, std::ptr::null_mut())
}
Err(error) => {
log::warn!("lok-worker: frame read failed: {error}");
quit(1);
}
};
let window = WINDOW.load(Ordering::SeqCst);
if unsafe { PostMessageW(window, message, 0, request as LPARAM) } == 0 {
log::error!("lok-worker: a request did not post: {}", io::Error::last_os_error());
quit(1);
}
if message == CLOSED {
return;
}
}
}
/// Idle jobs yield to a waiting request.
extern "C" fn waiting(_: *mut c_void, _: i32) -> bool {
PENDING.load(Ordering::SeqCst) > 0
}
unsafe extern "system" fn procedure(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match message {
ASK => {
let request = unsafe { Box::from_raw(lparam as *mut (u8, u64, Vec<u8>)) };
let (tag, id, payload) = *request;
PENDING.fetch_sub(1, Ordering::SeqCst);
answer(lok(), tag, id, &payload);
0
}
CLOSED => teardown::teardown(0),
_ => unsafe { DefWindowProcW(window, message, wparam, lparam) },
}
}
+107
View File
@@ -0,0 +1,107 @@
//! Linux: LOK runs its loop; requests arrive in its poll.
use std::{
ffi::c_void,
io,
sync::{
Mutex, OnceLock,
atomic::{AtomicUsize, Ordering},
mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel},
},
time::Duration,
};
use super::{answer, events, lok, password, pin, quit, teardown};
use crate::ffi::Lok;
use crate::ipc;
/// A request, the kernel's wake, or the end.
enum Note {
Ask(u8, u64, Vec<u8>),
Woken,
Closed,
}
type Notes = (Mutex<Sender<Note>>, Mutex<Receiver<Note>>);
static NOTES: OnceLock<Notes> = OnceLock::new();
/// Requests read and not yet answered.
static PENDING: AtomicUsize = AtomicUsize::new(0);
pub(super) fn serve(lok: Lok, input: impl io::Read + Send + 'static) -> io::Result<()> {
let lok = Box::leak(Box::new(lok));
pin(lok);
lok.register_callback(password::on_kit);
lok.register_any_input(waiting);
let (send, receive) = channel();
let reading = send.clone();
NOTES
.set((Mutex::new(send), Mutex::new(receive)))
.map_err(|_| io::Error::other("the loop is served twice"))?;
std::thread::spawn(move || reader(input, reading));
std::thread::spawn(events::push);
lok.run_loop(Some(poll), Some(wake));
log::info!("lok-worker: the kernel's loop ended");
teardown::teardown(0);
}
fn reader(mut input: impl io::Read, notes: Sender<Note>) {
loop {
let note = match ipc::read_frame(&mut input) {
Ok(Some((tag, id, payload))) => {
PENDING.fetch_add(1, Ordering::SeqCst);
Note::Ask(tag, id, payload)
}
Ok(None) => {
log::info!("lok-worker: stdin closed");
Note::Closed
}
Err(error) => {
log::warn!("lok-worker: frame read failed: {error}");
quit(1);
}
};
let closed = matches!(note, Note::Closed);
if notes.send(note).is_err() || closed {
return;
}
}
}
/// Idle jobs yield to a waiting request.
extern "C" fn waiting(_: *mut c_void, _: i32) -> bool {
PENDING.load(Ordering::SeqCst) > 0
}
/// The kernel's wait: one request answered, or its own wake.
extern "C" fn poll(_: *mut c_void, timeout_us: i32) -> i32 {
let notes = &NOTES.get().expect("the loop is served").1;
let notes = notes.lock().expect("the notes lock");
let note = match timeout_us {
0 => notes.try_recv().map_err(|error| error == TryRecvError::Disconnected),
wait if wait < 0 => notes.recv().map_err(|_| true),
wait => notes
.recv_timeout(Duration::from_micros(wait as u64))
.map_err(|error| error == RecvTimeoutError::Disconnected),
};
drop(notes);
match note {
Ok(Note::Ask(tag, id, payload)) => {
PENDING.fetch_sub(1, Ordering::SeqCst);
answer(lok(), tag, id, &payload);
1
}
Ok(Note::Woken) => 1,
Ok(Note::Closed) | Err(true) => teardown::teardown(0),
Err(false) => 0,
}
}
/// Another thread posted work for the kernel's loop.
extern "C" fn wake(_: *mut c_void) {
let notes = &NOTES.get().expect("the loop is served").0;
if notes.lock().expect("the notes lock").send(Note::Woken).is_err() {
log::info!("lok-worker: a wake after the loop ended");
}
}
+20 -7
View File
@@ -25,10 +25,16 @@ case $slug in
*) crates=() ;;
esac
python3 "$root/packaging/crates.py" "$slug" ${crates[@]+"${crates[@]}"} >"$out/rust-crates.txt"
lo=${ELY_LO_APP:-$root/engines/lo-src/instdir/LibreOffice.app}/Contents/Resources
case $(uname) in
Darwin) lo=${ELY_LO_APP:-$root/engines/lo-src/instdir/LibreOffice.app}/Contents/Resources ;;
*) lo=${ELY_LO_DIR:-$root/engines/lo-src/instdir} ;;
esac
# Windows builds name LibreOffice's licence license.txt.
lo_licence=$lo/LICENSE
[ -e "$lo_licence" ] || lo_licence=$lo/license.txt
case $slug in
ely-doc | ely-table | ely-show)
put LibreOffice "$lo/LICENSE" "$lo/LICENSE.html" "$lo/NOTICE" "$lo/CREDITS.fodt"
put LibreOffice "$lo_licence" "$lo/LICENSE.html" "$lo/NOTICE" "$lo/CREDITS.fodt"
;;
ely-pdf)
put MuPDF "$root/engines/mupdf-src/COPYING"
@@ -53,16 +59,23 @@ case $slug in
# shellcheck disable=SC2086
put "MuPDF/fonts/$name" $found
done
put OpenSSL "$(pkg-config --variable=prefix libcrypto)/LICENSE.txt"
case $(uname) in
MINGW* | MSYS*) put OpenSSL "$root/engines/vcpkg/installed/x64-windows-static/share/openssl/copyright" ;;
*) put OpenSSL "$(pkg-config --variable=prefix libcrypto)/LICENSE.txt" ;;
esac
put tessdata "${ELY_TESSDATA:-$root/engines/tessdata}/LICENSE"
put GreatVibes "$root/crates/ely-pdf-engine/fonts/OFL.txt"
;;
ely-tree)
put Git "$root/engines/git-src/COPYING" "$root/engines/git-src/LGPL-2.1"
put git-lfs "$root/engines/git/share/doc/git-lfs/LICENSE.md"
;;
ely-mark)
put CEF "$root/reference/cef/LICENSE.txt" "${ELY_CEF:-$root/engines/cef}/CREDITS.html"
put content "$root/engines/content/ely-mark/THIRD-PARTY-NOTICES.txt"
;;
esac
# ElyTree carries Git; on Windows and Linux ElyMark does too.
if [ "$slug" = ely-tree ] || { [ "$slug" = ely-mark ] && [ "$(uname)" != Darwin ]; }; then
case $(uname) in
MINGW* | MSYS*) put Git "$root/engines/git/LICENSE.txt" ;;
*) put Git "$root/engines/git-src/COPYING" "$root/engines/git-src/LGPL-2.1" ;;
esac
put git-lfs "$root/engines/git/share/doc/git-lfs/LICENSE.md"
fi
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Stages an app with its engines. Usage: stage.sh <slug> <dir>
set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
slug=${1:?app slug, such as ely-doc}
out=${2:?the folder to fill}
case $(uname) in
MINGW* | MSYS*) exe=.exe ;;
Linux) exe= ;;
*)
echo "stage.sh: Windows or Linux only" >&2
exit 1
;;
esac
grep -q "^$slug " "$root/packaging/apps.tsv" || {
echo "stage.sh: unknown app $slug" >&2
exit 1
}
uses() { grep -q "^$1" "$root/apps/$slug/Cargo.toml"; }
need() { # path, what to run
[ -e "$1" ] || {
echo "stage.sh: no $1; run $2" >&2
exit 1
}
}
build() { cargo build --manifest-path "$root/Cargo.toml" --release "$@"; }
rm -rf "$out"
mkdir -p "$out"
build -p "$slug"
cp "$root/target/release/$slug$exe" "$out/"
if uses ely-lok; then
lo=${ELY_LO_DIR:-$root/engines/lo-src/instdir}
need "$lo/program" scripts/build-libreoffice.sh
build -p ely-lok --bin ely-lok-worker
cp "$root/target/release/ely-lok-worker$exe" "$out/"
# The kernel in LibreOffice's layout, beside the app.
mkdir "$out/libreoffice"
cp -R "$lo/program" "$lo/share" "$lo/presets" "$out/libreoffice/"
cp "$lo/LICENSE.html" "$lo/NOTICE" "$lo/CREDITS.fodt" "$out/libreoffice/"
# Merged-library builds leave text placeholders.
while IFS= read -r stub; do
if [ "$(cat "$stub")" = "invalid - merged lib" ]; then
rm "$stub"
fi
done < <(find "$out/libreoffice/program" -type f -size -64c)
if [ -n "$exe" ]; then
# The kernel needs the VC++ runtime; it ships beside it.
crt=$(ls -d "/c/Program Files/Microsoft Visual Studio/2022/"*/VC/Redist/MSVC/*/x64/Microsoft.VC143.CRT | tail -n 1)
need "$crt" "the Visual Studio 2022 C++ tools"
cp "$crt"/*.dll "$out/libreoffice/program/"
fi
fi
if uses ely-pdf-engine; then
tessdata=${ELY_TESSDATA:-$root/engines/tessdata}
need "$tessdata/eng.traineddata" scripts/fetch-tessdata.sh
build -p ely-pdf-engine --bin ely-pdf-worker
cp "$root/target/release/ely-pdf-worker$exe" "$out/"
mkdir "$out/tessdata"
cp "$tessdata"/{eng,chi_sim,chi_tra}.traineddata "$out/tessdata/"
fi
if [ "$slug" = ely-tree ] || [ "$slug" = ely-mark ]; then
need "$root/engines/git" "scripts/build-git.sh and scripts/fetch-git-lfs.sh"
cp -R "$root/engines/git" "$out/git"
fi
if uses ely-web-content; then
cef=${ELY_CEF:-$root/engines/cef}
need "$cef/icudtl.dat" scripts/fetch-cef.sh
# CEF's runtime sits beside the app, as cef-dll-sys has it.
find "$cef" -maxdepth 1 -type f \( -name '*.dll' -o -name '*.so' -o -name '*.so.*' -o -name '*.pak' \
-o -name '*.bin' -o -name '*.dat' -o -name '*.json' -o -name chrome-sandbox \) -exec cp {} "$out/" \;
cp -R "$cef/locales" "$out/"
content=$root/engines/content/$slug
need "$content/index.html" "its scripts/build-*-content.sh"
cp -R "$content" "$out/content"
fi
"$root/packaging/licenses.sh" "$slug" "$out/licenses"
echo "$out"
@@ -0,0 +1,26 @@
svx, sd: Boost.JSON needs no library to link
diff --git a/svx/Library_svxcore.mk b/svx/Library_svxcore.mk
index 08daa4dd7..e20b9c529 100644
--- a/svx/Library_svxcore.mk
+++ b/svx/Library_svxcore.mk
@@ -51,6 +51,7 @@ $(eval $(call gb_Library_set_precompiled_header,svxcore,svx/inc/pch/precompiled_
$(eval $(call gb_Library_add_defs,svxcore,\
-DSVXCORE_DLLIMPLEMENTATION \
+ -DBOOST_ALL_NO_LIB \
))
$(eval $(call gb_Library_use_libraries,svxcore,\
diff --git a/sd/Library_sd.mk b/sd/Library_sd.mk
index f29e8f280..7d04eb6be 100644
--- a/sd/Library_sd.mk
+++ b/sd/Library_sd.mk
@@ -49,6 +49,7 @@ $(eval $(call gb_Library_set_include,sd,\
$(eval $(call gb_Library_add_defs,sd,\
-DSD_DLLIMPLEMENTATION \
+ -DBOOST_ALL_NO_LIB \
-DSDUI_DLL_NAME=\"$(call gb_Library_get_runtime_filename,$(call gb_Library__get_name,sdui))\" \
))
@@ -0,0 +1,93 @@
LOK: callbacks flush inline on every platform
0001 made the callback queue flush inline on macOS, where Aqua never
runs posted user events. Elsewhere scheduleFlush posted a user event,
so a callback reached the client only after the next turn of the
main loop. Ely's worker posts a command and waits for its result and
states inside the same request, which runs inside that loop (unipoll
on Linux, a window message on Windows): the result never came. The
queue now flushes inline everywhere; the flush event, its link and
stop() are gone.
diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx
index 76a02127d..39b72fb1a 100644
--- a/desktop/source/lib/init.cxx
+++ b/desktop/source/lib/init.cxx
@@ -1609,7 +1609,6 @@ extern "C" {
CallbackFlushHandler::CallbackFlushHandler(LibreOfficeKitDocument* pDocument, LibreOfficeKitCallback pCallback, void* pData)
: m_pDocument(pDocument),
m_pCallback(pCallback),
- m_pFlushEvent(nullptr),
m_pData(pData),
m_nDisableCallbacks(0)
{
@@ -1638,18 +1637,8 @@ CallbackFlushHandler::CallbackFlushHandler(LibreOfficeKitDocument* pDocument, Li
}
}
-void CallbackFlushHandler::stop()
-{
- if (m_pFlushEvent)
- {
- Application::RemoveUserEvent(m_pFlushEvent);
- m_pFlushEvent = nullptr;
- }
-}
-
CallbackFlushHandler::~CallbackFlushHandler()
{
- stop();
}
CallbackFlushHandler::queue_type2::iterator CallbackFlushHandler::toQueue2(CallbackFlushHandler::queue_type1::iterator pos)
@@ -2600,23 +2589,11 @@ void CallbackFlushHandler::invoke()
m_queue1.clear();
m_queue2.clear();
- stop();
}
void CallbackFlushHandler::scheduleFlush()
{
-#if defined(MACOSX)
- // Aqua never runs posted user events: flush inline.
- invoke();
-#else
- if (!m_pFlushEvent)
- m_pFlushEvent = Application::PostUserEvent(LINK(this, CallbackFlushHandler, FlushQueue));
-#endif
-}
-
-IMPL_LINK_NOARG(CallbackFlushHandler, FlushQueue, void*, void)
-{
- m_pFlushEvent = nullptr;
+ // Ely's worker waits on callbacks inside a request: flush inline.
invoke();
}
diff --git a/desktop/inc/lib/init.hxx b/desktop/inc/lib/init.hxx
index 51d690307..6c21850fc 100644
--- a/desktop/inc/lib/init.hxx
+++ b/desktop/inc/lib/init.hxx
@@ -210,8 +210,6 @@ namespace desktop {
void enqueueUpdatedTypes();
void enqueueUpdatedType( int type, const SfxViewShell* sourceViewShell, int viewId );
- void stop();
-
/** we frequently want to scan the queue, and mostly when we do so, we only care about the element type
so we split the queue in 2 to make the scanning cache friendly. */
queue_type1 m_queue1;
@@ -246,12 +244,9 @@ namespace desktop {
OString m_aViewRenderState;
int m_viewId = -1; // view id of the associated SfxViewShell
LibreOfficeKitCallback m_pCallback;
- ImplSVEvent* m_pFlushEvent;
void *m_pData;
int m_nDisableCallbacks;
std::recursive_mutex m_mutex;
-
- DECL_LINK(FlushQueue, void*, void);
};
struct DESKTOP_DLLPUBLIC LibLODocument_Impl : public _LibreOfficeKitDocument
+2
View File
@@ -15,6 +15,8 @@ holds() {
rm -rf "$base"
mkdir -p "$base"
git -C "$base" init -q
# LF, as every tree here; Windows defaults convert.
git -C "$base" config core.autocrlf false
for f in $files; do
if git -C "$src" cat-file -e "HEAD:$f" 2>/dev/null; then
mkdir -p "$base/$(dirname "$f")"
+16 -1
View File
@@ -49,7 +49,6 @@ case $(uname) in
# Collabora's LOKit set: headless, bundled libraries.
options+=(
--disable-gui
--enable-cairo-rgba
--disable-dbus
--disable-gio
--disable-dconf
@@ -73,6 +72,22 @@ case $(uname) in
app=$src/instdir/share
jobs=$(nproc)
;;
CYGWIN*)
# Unix-only switches; configure refuses them here.
kept=()
for option in "${options[@]}"; do
case $option in
--enable-bogus-pkg-config | --disable-cups | --disable-evolution2 | --disable-python) ;;
*) kept+=("$option") ;;
esac
done
options=("${kept[@]}" --with-visual-studio=2022 --disable-ccache --disable-cli --without-dotnet)
# Cygwin's own make, never Git Bash's; MSYSTEM misleads the build.
unset MSYSTEM MSYSTEM_PREFIX MINGW_PREFIX MSYSTEM_CHOST MSYSTEM_CARCH
PATH=/opt/lo/bin:$PATH
app=$src/instdir/share
jobs=$(nproc)
;;
*)
echo "build-libreoffice.sh: no build for $(uname)" >&2
exit 1
+9
View File
@@ -138,3 +138,12 @@ P0 次序:W-01、K-01、L-01、O-01、W-02、L-02、W-03、L-03、W-04、L-04
- [ ] W-04 Windows 打包:六个安装程序,带引擎、许可、文件类型;更新清单含 windows-x86_64
- [ ] CI-01 Gitea 工作流:打标签即在 macOS、Windows、Linux 运行器上构建、测试、打包并上传到发布
- [ ] REL-3 版本号 0.1.3,打标签触发运行器,三端产物与清单上传,官网指向新发布
## 0.1.3 之后(三端实测查出,非 P0)
- [ ] P3-01 拼写建议:Windows 与 Linux 上右键不出拼写菜单(`tools::spelling_suggests_and_ignores`)
- [ ] P3-02 Windows:Calc 深色主题的图块为全黑(`paint::the_page_follows_a_dark_theme_and_back`)
- [ ] P3-03 Windows:放映图层大于一帧时分片缺行(`motion::a_layer_larger_than_one_frame_arrives_in_pieces`)
- [ ] P3-04 Windows:放映的页脚字段文字不在占位处(`playlist::field_text_lands_where_its_placeholder_is`)
- [ ] P3-05 Windows:辅助功能的段落选区不桥接(`commands::a11y_follows_the_focused_paragraph`)
+6 -3
View File
@@ -4,6 +4,7 @@
import datetime
import math
import posixpath
import os
import re
import shutil
import subprocess
@@ -255,12 +256,14 @@ def selftest(case, path):
missed.append(f"{part}: {pattern} found nothing to change")
continue
entries[part] = changed.encode()
with tempfile.NamedTemporaryFile(suffix=path[-5:]) as copy:
with zipfile.ZipFile(copy.name, "w") as archive:
# A folder, not an open file: Windows cannot reopen those.
with tempfile.TemporaryDirectory() as folder:
copy = os.path.join(folder, "mutated" + path[-5:])
with zipfile.ZipFile(copy, "w") as archive:
for name, body in entries.items():
archive.writestr(name, body)
failures = []
CASES[case](copy.name)
CASES[case](copy)
if not failures:
missed.append(f"{part}: {pattern} passed")
failures = [f"a mutation went unnoticed: {what}" for what in missed]