Windows and Linux build environments; check clean on both (E-01)

MuPDF builds with MSBuild on Windows (/MT, OpenSSL from vcpkg);
Git for Windows' MinGit stands in for the source build there. CEF
loads from where libcef lives and sets its API version first on
every platform. File identity on Windows compares volume and index.
The checkout and reference/ keep LF on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 03:53:56 -04:00
1 parent d59159cea2
commit 68b5af76e0
40 files changed
+380 -82

No files matched your search

+1
View File
@@ -16,3 +16,4 @@ node_modules/
/docs/acceptance.md
/docs/test-map.md
/assets/papirus_icons-master.zip
/Gitea.md
Generated
+1
View File
@@ -2229,6 +2229,7 @@ dependencies = [
"objc2 0.6.4",
"serde_json",
"tempfile",
"windows-sys 0.60.2",
]
[[package]]
+2 -1
View File
@@ -191,7 +191,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-01:上游版本以 `scripts/fetch-references.sh` 为准。LibreOffice 26.2.6(8221e31)、MuPDF 1.28.5(8ad45e9)、gpui 为 `gpui-pre` =0.3.8 与 `gpui-pre-platform` =0.3.8(crates.io,2026-10-07 起,随组件库 K01)、组件库 5d6efd4(2026-10-09 起)。
- 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;本机 `codex` 别名已带 `--yolo`,重复传参会报错)审查,最多三轮。此前各轮用 gpt-6-astra xhigh。
- 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"`,每条最多五轮;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-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 到所选文件夹,任一目标已存在即整体拒绝,从不发邮件。
@@ -278,4 +278,5 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-09:退出(缺陷清单 X-01)。Cmd+Q 与应用菜单的“退出”由应用级动作接收:主窗口的宿主在 `Closing::own_window` 里登记自己的退出流程(`ely_ui::commands::route_quit`),任何窗口(含设置窗口)发出的退出都经它逐个关闭标签、询问未保存的;宿主的窗口已不在时才直接退出;没有宿主的应用(ElyTree)直接退出。`Host::quitting` 在退出开始时运行(ElyMark 先记下打开的标签)。原先只有主窗口根上截退出,设置窗口在前时 Cmd+Q 不问即退,并把这次运行记为干净结束、删掉恢复点。
- 2026-10-09:作者名与读不出的设置(缺陷清单 X-02)。签名用的名字取启动时读入的通用设置(`ely_ui::settings::current`;文件读不出时为默认值,设置窗口另以 Banner 写明),`ely_platform::user::named(作者名)` 为空即用账户名;不再在引擎启动时重读文件(原先 `settings.json` 读不出时 Doc、Table、Show 以致命错误退出)。LOK worker 记下设定的作者名,引擎重启照用它。`tests/scripts/bad-settings.sh` 核对读不出的设置下 ElyDoc 照样启动、文件不被覆盖。
- 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: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。仓库与 `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-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 地址取。
+9 -4
View File
@@ -181,7 +181,6 @@ fn the_current_heading_follows_the_cursor_across_columns() {
settle(&cx, |app| current(app) == Some(0));
}
#[cfg(unix)]
#[test]
fn pdfs_export_a_range_and_locked_files_ask() {
let dir = tempfile::tempdir().expect("a temp dir");
@@ -205,11 +204,17 @@ fn pdfs_export_a_range_and_locked_files_ask() {
});
written(&cx, &pdf, None);
let source = dir.path().join("long.fodt");
let alias = dir.path().join("alias.pdf");
std::os::unix::fs::symlink(&source, &alias).expect("the alias");
let link = dir.path().join("link.pdf");
std::fs::hard_link(&source, &link).expect("the hard link");
for target in [&source, &alias, &link] {
#[cfg(unix)]
let aliases = {
let alias = dir.path().join("alias.pdf");
std::os::unix::fs::symlink(&source, &alias).expect("the alias");
vec![alias]
};
#[cfg(not(unix))]
let aliases: Vec<std::path::PathBuf> = Vec::new();
for target in [source.clone(), link].iter().chain(&aliases) {
cx.update(|app| {
docs.update(app, |docs, cx| {
docs.export_to(id, target.clone(), ely_lok::Pdf::default(), cx)
+1 -1
View File
@@ -158,7 +158,7 @@ fn a_signed_file_saved_as_a_copy_reopens_as_the_copy() {
settle(cx, |app| view.read(app).edited == 1);
let copy = dir.path().join("copy.pdf");
let reopened = save_signed_as(cx, &viewer, &view, &copy);
let tab = cx.update(|_, app| (viewer.read(app).active_tab().map(|tab| (tab.risk, tab.dirty(app)))));
let tab = cx.update(|_, app| viewer.read(app).active_tab().map(|tab| (tab.risk, tab.dirty(app))));
assert_eq!(tab, Some((None, false)), "an unsigned, saved copy");
assert!(!shows_signed(cx, &viewer, &reopened), "the copy holds no signature");
assert_eq!(cx.update(|_, app| reopened.read(app).sizes[0]), (792.0, 612.0), "the edit is in the copy");
+2 -1
View File
@@ -13,7 +13,7 @@ use ely_lok::{LokView, show::Media};
use ely_office::prelude::*;
use ely_platform::media::{Player, State};
use gpui::{
AnyElement, App, Context, Entity, IntoElement, ParentElement as _, Styled as _, SurfaceSource,
AnyElement, App, Context, Entity, ParentElement as _, Styled as _, SurfaceSource,
TaskExt as _, Window, div,
};
@@ -275,6 +275,7 @@ pub(crate) fn video(
frame: SurfaceSource,
(at, size): (gpui::Point<gpui::Pixels>, gpui::Point<gpui::Pixels>),
) -> AnyElement {
use gpui::IntoElement as _;
gpui::surface(frame)
.absolute()
.left(at.x)
+7
View File
@@ -461,7 +461,14 @@ fn a_worktree_registered_through_a_link_lists_its_real_path() {
)
.expect("adds");
// Other tools may write the path through a link.
#[cfg(unix)]
std::os::unix::fs::symlink(dir.path(), dir.path().join("link")).expect("links");
#[cfg(windows)]
{
let link = dir.path().join("link");
let made = std::process::Command::new("cmd").arg("/C").arg("mklink").arg("/J").arg(&link).arg(dir.path()).output().expect("cmd");
assert!(made.status.success(), "a junction: {}", String::from_utf8_lossy(&made.stderr));
}
let gitdir = root.join(".git/worktrees/tree/gitdir");
std::fs::write(
&gitdir,
+3 -6
View File
@@ -203,12 +203,9 @@ pub fn same_file(a: &Path, b: &Path) -> io::Result<bool> {
if resolved(a)? == resolved(b)? {
return Ok(true);
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let (Ok(ma), Ok(mb)) = (std::fs::metadata(a), std::fs::metadata(b)) {
return Ok((ma.dev(), ma.ino()) == (mb.dev(), mb.ino()));
}
// A missing side is another file.
if let (Ok(Some(ia)), Ok(Some(ib))) = (crate::session::file_id(a), crate::session::file_id(b)) {
return Ok(ia == ib);
}
Ok(false)
}
+2 -2
View File
@@ -462,8 +462,8 @@ fn save_as_onto_its_own_file_checks_for_outside_changes() {
let alias = dir.path().join("alias.docx");
#[cfg(unix)]
std::os::unix::fs::symlink(&own, &alias).expect("the alias");
#[cfg(not(unix))]
let alias = own.clone();
#[cfg(windows)]
std::fs::hard_link(&own, &alias).expect("the alias");
assert!(matches!(
save_as(&mut session, &alias, b"local"),
Err(SaveError::Conflict)
+1
View File
@@ -386,6 +386,7 @@ impl Session {
}
mod registry;
pub(crate) use registry::file_id;
pub use registry::{Open, Sessions};
#[cfg(test)]
+3 -3
View File
@@ -33,14 +33,14 @@ struct Key {
}
#[cfg(unix)]
fn file_id(path: &Path) -> io::Result<Option<(u64, u64)>> {
pub(crate) fn file_id(path: &Path) -> io::Result<Option<(u64, u64)>> {
use std::os::unix::fs::MetadataExt;
let meta = std::fs::metadata(path)?;
Ok(Some((meta.dev(), meta.ino())))
}
#[cfg(windows)]
fn file_id(path: &Path) -> io::Result<Option<(u64, u64)>> {
pub(crate) fn file_id(path: &Path) -> io::Result<Option<(u64, u64)>> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{
@@ -75,7 +75,7 @@ fn file_id(path: &Path) -> io::Result<Option<(u64, u64)>> {
}
#[cfg(not(any(unix, windows)))]
fn file_id(_: &Path) -> io::Result<Option<(u64, u64)>> {
pub(crate) fn file_id(_: &Path) -> io::Result<Option<(u64, u64)>> {
Ok(None)
}
+1 -1
View File
@@ -448,7 +448,7 @@ fn an_empty_sheet_scrolls_on_without_waiting() {
eprintln!("SKIP: no pinned LOK build");
return;
};
let mut cx = gpui::TestAppContext::single();
let cx = gpui::TestAppContext::single();
let (view, worker) = cx.update(|app| live_view_of(app, &install, "short.fods", SHORT_ROWS));
let json = cx.update(|app| worker.lock().expect("the worker lock").values(&view.read(app).doc, crate::sheet::GEOMETRY).expect("geometry"));
let geometry = crate::sheet::geometry(&json).expect("parsed");
+36 -9
View File
@@ -57,7 +57,11 @@ fn main() {
}
println!("cargo:rustc-env=ELY_PDF_BUILD={stamp:016x}");
println!("cargo:rerun-if-env-changed=ELY_MUPDF_DIR");
for lib in ["libmupdf.a", "libmupdf-third.a", "libmupdf-pkcs7.a"] {
let windows = std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows");
// MSVC's libmupdf.lib holds its third parties and PKCS#7.
let libs: &[&str] =
if windows { &["libmupdf.lib"] } else { &["libmupdf-pkcs7.a", "libmupdf.a", "libmupdf-third.a"] };
for lib in libs {
println!("cargo:rerun-if-changed={}", mupdf.join(lib).display());
if !mupdf.join(lib).is_file() {
panic!("no {lib} in {} (run scripts/build-mupdf.sh or set ELY_MUPDF_DIR)", mupdf.display());
@@ -83,26 +87,49 @@ fn main() {
.file("shim/ely_sign.c")
.file("shim/ely_output.c")
.include(root.join("reference/mupdf/include"))
.include(openssl("includedir"))
.include(openssl(&root, "includedir"))
.warnings(true)
.cargo_metadata(false)
.compile("ely_mupdf");
let out = PathBuf::from(std::env::var("OUT_DIR").expect("set by cargo"));
let bin = "ely-pdf-worker";
println!("cargo:rustc-link-arg-bin={bin}={}", out.join("libely_mupdf.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf-pkcs7.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf-third.a").display());
let shim = if windows { "ely_mupdf.lib" } else { "libely_mupdf.a" };
println!("cargo:rustc-link-arg-bin={bin}={}", out.join(shim).display());
for lib in libs {
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join(lib).display());
}
// Signing and checking need OpenSSL, linked in whole.
println!("cargo:rustc-link-arg-bin={bin}={}", PathBuf::from(openssl("libdir")).join("libcrypto.a").display());
let crypto = if windows { "libcrypto.lib" } else { "libcrypto.a" };
println!("cargo:rustc-link-arg-bin={bin}={}", PathBuf::from(openssl(&root, "libdir")).join(crypto).display());
if windows {
for lib in ["crypt32.lib", "ws2_32.lib", "advapi32.lib", "user32.lib"] {
println!("cargo:rustc-link-arg-bin={bin}={lib}");
}
return;
}
// What a static libcrypto needs, as its pkg-config says.
let needs = std::process::Command::new("pkg-config")
.args(["--static", "--libs-only-l", "libcrypto"])
.output()
.expect("pkg-config, to find OpenSSL");
assert!(needs.status.success(), "pkg-config knows no static libcrypto");
for lib in String::from_utf8(needs.stdout).expect("UTF-8 flags").split_whitespace().filter(|lib| *lib != "-lcrypto") {
println!("cargo:rustc-link-arg-bin={bin}={lib}");
}
println!("cargo:rustc-link-arg-bin={bin}=-lm");
// Tesseract inside MuPDF is C++.
let cxx = if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { "-lc++" } else { "-lstdc++" };
println!("cargo:rustc-link-arg-bin={bin}={cxx}");
}
/// An OpenSSL directory, from pkg-config.
fn openssl(variable: &str) -> String {
/// An OpenSSL directory: vcpkg's static build on Windows, else pkg-config's.
fn openssl(root: &std::path::Path, variable: &str) -> String {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
let dir = root.join("engines/vcpkg/installed/x64-windows-static");
let sub = if variable == "includedir" { "include" } else { "lib" };
assert!(dir.join("lib/libcrypto.lib").is_file(), "no static OpenSSL in {}", dir.display());
return dir.join(sub).display().to_string();
}
let out = std::process::Command::new("pkg-config")
.args([&format!("--variable={variable}"), "libcrypto"])
.output()
+1 -1
View File
@@ -146,7 +146,7 @@ static void write_file(fz_context *ctx, fz_output *out, const char *key, const c
fz_write_string(ctx, out, ",\"name\":");
ely_json_string(ctx, out, name);
fz_write_printf(ctx, out, ",\"page\":%d,\"size\":%ld,\"created\":%ld,\"modified\":%ld,\"type\":", page,
(long)file_size(ctx, fs), (long)params.created, (long)params.modified);
file_size(ctx, fs), (int64_t)params.created, (int64_t)params.modified);
ely_json_string(ctx, out, params.mimetype ? params.mimetype : "");
fz_write_string(ctx, out, ",\"note\":");
ely_json_string(ctx, out, pdf_dict_get_text_string(ctx, fs, PDF_NAME(Desc)));
+1 -1
View File
@@ -255,7 +255,7 @@ int ely_text_layer(fz_context *ctx, fz_document *doc, const char *json, int repl
{
if (!font)
font = glyphless(ctx, pdf);
ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyOCR", font, name, sizeof name);
ely_add_resource(ctx, page->obj, PDF_NAME(Font), "ElyOCR", font, name, sizeof name);
buf = fz_new_buffer(ctx, 1024);
/* Its own state, so later text stays seen. */
fz_append_string(ctx, buf, "q\n");
+1 -1
View File
@@ -132,7 +132,7 @@ const char *ely_pen_resource(fz_context *ctx, pdf_document *pdf, pdf_page *page,
p->ref = pdf_add_cjk_font(ctx, pdf, p->font, p->ordering, 0, 0);
break;
}
return ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyF", p->ref, name, room);
return ely_add_resource(ctx, page->obj, PDF_NAME(Font), "ElyF", p->ref, name, room);
}
/* The own font's code for `u`, or -1. */
+7 -2
View File
@@ -38,6 +38,7 @@ static void inv_q(fz_context *ctx, pdf_processor *proc)
static void inv_Q(fz_context *ctx, pdf_processor *proc)
{
inventory *inv = (inventory *)proc;
(void)ctx;
if (inv->depth > 0)
inv->ctm = inv->stack[--inv->depth];
}
@@ -45,6 +46,7 @@ static void inv_Q(fz_context *ctx, pdf_processor *proc)
static void inv_cm(fz_context *ctx, pdf_processor *proc, float a, float b, float c, float d, float e, float f)
{
inventory *inv = (inventory *)proc;
(void)ctx;
inv->ctm = fz_concat(fz_make_matrix(a, b, c, d, e, f), inv->ctm);
}
@@ -62,12 +64,15 @@ static void found(fz_context *ctx, inventory *inv, int form, fz_rect box)
static void inv_image(fz_context *ctx, pdf_processor *proc, const char *name, fz_image *image)
{
(void)name;
(void)image;
found(ctx, (inventory *)proc, 0, fz_unit_rect);
}
static void inv_form(fz_context *ctx, pdf_processor *proc, const char *name, pdf_obj *form)
{
fz_rect box = fz_transform_rect(pdf_xobject_bbox(ctx, form), pdf_xobject_matrix(ctx, form));
(void)name;
found(ctx, (inventory *)proc, 1, box);
}
@@ -269,7 +274,7 @@ int ely_place(fz_context *ctx, fz_document *doc, int number, int index, const fl
own = (float)image->w / image->h;
change = fz_concat(fz_concat(fz_translate(-cx, -cy), fz_scale(fminf(1, own / box), fminf(1, box / own))), fz_translate(cx, cy));
ref = pdf_add_image(ctx, pdf, image);
h.rename = ely_add_resource(ctx, pdf, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
h.rename = ely_add_resource(ctx, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
}
if (fz_matrix_expansion(change) < 0.001f)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a change that leaves nothing");
@@ -315,7 +320,7 @@ int ely_add_image(fz_context *ctx, fz_document *doc, int number, const char *pat
pdf_begin_operation(ctx, pdf, "Add an image");
begun = 1;
ref = pdf_add_image(ctx, pdf, image);
ely_add_resource(ctx, pdf, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
ely_add_resource(ctx, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
/* The unit square, bottom left first, onto the box. */
m = fz_concat(fz_make_matrix(rect[2] - rect[0], 0, 0, rect[1] - rect[3], rect[0], rect[3]), ely_to_user(ctx, page));
buf = fz_new_buffer(ctx, 128);
+1 -1
View File
@@ -38,7 +38,7 @@ void ely_read_block(fz_context *ctx, fz_document *doc, pdf_page *page, int numbe
void ely_remove_block(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_shape *shape);
void ely_drop_shape(fz_context *ctx, ely_shape *shape);
pdf_obj *ely_append_content(fz_context *ctx, pdf_document *pdf, pdf_obj *page, fz_buffer *buf);
const char *ely_add_resource(fz_context *ctx, pdf_document *pdf, pdf_obj *page, pdf_obj *type, const char *stem,
const char *ely_add_resource(fz_context *ctx, pdf_obj *page, pdf_obj *type, const char *stem,
pdf_obj *ref, char *name, size_t room);
void ely_json_string(fz_context *ctx, fz_output *out, const char *s);
fz_json *ely_member(fz_context *ctx, fz_json *obj, const char *key, int type);
+4
View File
@@ -97,6 +97,10 @@ static int take_glyph(fz_context *ctx, void *opaque, int *ucs, int ucslen, fz_ma
fz_rect bbox, int tr, float ca, float CA)
{
targets *t = opaque;
(void)bbox;
(void)tr;
(void)ca;
(void)CA;
fz_point at = fz_transform_point(fz_transform_point(fz_make_point(0, 0), trm), ctm);
/* MuPDF gives at most PDF_MRANGE_CAP; each spells to 18. */
int i, k, found = 0, spelt[18 * PDF_MRANGE_CAP], n = 0;
+1 -1
View File
@@ -68,7 +68,7 @@ static int lay_out(fz_context *ctx, ely_pen *p, const int *u, int n, float size,
}
/* A resource name in `type` that is free, given `ref`. */
const char *ely_add_resource(fz_context *ctx, pdf_document *pdf, pdf_obj *page, pdf_obj *type, const char *stem,
const char *ely_add_resource(fz_context *ctx, pdf_obj *page, pdf_obj *type, const char *stem,
pdf_obj *ref, char *name, size_t room)
{
pdf_obj *res = pdf_dict_get_inheritable(ctx, page, PDF_NAME(Resources));
+1
View File
@@ -186,6 +186,7 @@ fn a_reply_to_another_request_is_refused() {
}
/// A noisy RGB PNG, stored uncompressed: large on disk.
#[cfg(unix)]
fn big_png(path: &Path, side: u32) {
let crc = |bytes: &[u8]| {
let mut crc = 0xffff_ffffu32;
@@ -56,6 +56,28 @@ fn an_unreadable_log_is_an_error_not_nothing() {
assert!(log_part(log).is_err(), "a log that cannot be read shows");
}
#[cfg(windows)]
#[test]
fn an_unreadable_log_is_an_error_not_nothing() {
let dir = tempfile::tempdir().unwrap();
let logs = dir.path().join("logs");
std::fs::create_dir(&logs).unwrap();
let log = logs.join("ely.log");
assert_eq!(log_part(log.clone()).unwrap(), None, "no log yet is nothing");
std::fs::write(&log, "a line\n").unwrap();
let icacls = |path: &std::path::Path, args: [&str; 2]| {
let done = std::process::Command::new("icacls").arg(path).args(args).output().unwrap();
assert!(done.status.success(), "icacls: {}", String::from_utf8_lossy(&done.stdout));
};
// Everyone loses its attributes and its folder's listing.
icacls(&log, ["/deny", "*S-1-1-0:(RA)"]);
icacls(&logs, ["/deny", "*S-1-1-0:(RD)"]);
let read = log_part(log.clone());
icacls(&logs, ["/remove:d", "*S-1-1-0"]);
icacls(&log, ["/remove:d", "*S-1-1-0"]);
assert!(read.is_err(), "a log that cannot be read shows: {read:?}");
}
#[test]
fn a_cut_log_starts_at_a_whole_line() {
let dir = tempfile::tempdir().unwrap();
+17 -11
View File
@@ -14,6 +14,12 @@ pub const PROGRAM: &str = "git";
/// The oldest system Git counted as verified (user decision).
pub const LEAST: (u32, u32) = (2, 40);
/// Git inside a Git install: MinGit's on Windows.
#[cfg(windows)]
const GIT: &str = "cmd/git.exe";
#[cfg(not(windows))]
const GIT: &str = "bin/git";
/// `ELY_GIT`, ElyTree's setting, else Ely's own Git.
pub fn locate() -> io::Result<(PathBuf, bool)> {
if let Some(path) = std::env::var_os("ELY_GIT") {
@@ -62,12 +68,12 @@ pub fn system_version(path: &Path) -> io::Result<String> {
/// Beside `dir`, in its bundle, in ElyTree's, or built.
fn own(dir: &Path) -> PathBuf {
// ElyTree carries Git; the other apps use its copy.
let tree = format!("../../../{}.app/Contents/Resources/git/bin/git", AppId::Tree.name());
let built = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../engines/git/bin/git");
let tree = format!("../../../{}.app/Contents/Resources/git", AppId::Tree.name());
let built = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../engines/git").join(GIT);
[
dir.join("git/bin/git"),
dir.join("../Resources/git/bin/git"),
dir.join(tree),
dir.join("git").join(GIT),
dir.join("../Resources/git").join(GIT),
dir.join(tree).join(GIT),
]
.into_iter()
.find(|path| path.is_file())
@@ -113,17 +119,17 @@ mod tests {
assert_eq!(super::parse_version("hello"), None);
}
use super::own;
use super::{GIT, own};
#[test]
fn apps_beside_elytree_use_its_git() {
let apps = tempfile::tempdir().expect("a temp dir");
let mark = apps.path().join("ElyMark.app/Contents/MacOS");
let git = apps.path().join("ElyTree.app/Contents/Resources/git/bin");
let git = apps.path().join("ElyTree.app/Contents/Resources/git").join(GIT);
std::fs::create_dir_all(&mark).expect("ElyMark");
assert!(own(&mark).ends_with("engines/git/bin/git"));
std::fs::create_dir_all(&git).expect("ElyTree's Git");
std::fs::write(git.join("git"), "").expect("git");
assert_eq!(own(&mark), mark.join("../../../ElyTree.app/Contents/Resources/git/bin/git"));
assert!(own(&mark).ends_with(std::path::Path::new("engines/git").join(GIT)));
std::fs::create_dir_all(git.parent().expect("a folder")).expect("ElyTree's Git");
std::fs::write(&git, "").expect("git");
assert_eq!(own(&mark), mark.join("../../../ElyTree.app/Contents/Resources/git").join(GIT));
}
}
+3 -1
View File
@@ -3,8 +3,9 @@
use std::{
io,
path::{Path, PathBuf},
process::{Command, Stdio},
};
#[cfg(target_os = "macos")]
use std::process::{Command, Stdio};
/// The `.app` running this process; none in dev builds.
pub fn running_bundle() -> io::Result<Option<PathBuf>> {
@@ -50,6 +51,7 @@ fn stage(from: &Path, parent: &Path, name: &std::ffi::OsStr) -> io::Result<PathB
}
/// Puts `staged` at `bundle`; on failure the old returns.
#[cfg(any(target_os = "macos", test))]
fn swap(staged: &Path, bundle: &Path) -> io::Result<()> {
let name = bundle.file_name().ok_or_else(|| io::Error::other("the app has no name"))?;
let old = bundle.with_file_name(format!(".{}.old", name.to_string_lossy()));
+1
View File
@@ -73,6 +73,7 @@ pub fn print_pdf(pdf: &Path) -> anyhow::Result<bool> {
}
/// One paper holding every page, turned as needed.
#[cfg(any(target_os = "macos", test))]
fn paper_for(sizes: &[(f64, f64)]) -> Option<(f64, f64)> {
let long = sizes.iter().map(|(w, h)| w.max(*h)).fold(None, |most: Option<f64>, side| Some(most.map_or(side, |m| m.max(side))))?;
let short = sizes.iter().map(|(w, h)| w.min(*h)).fold(0.0, f64::max);
+1 -1
View File
@@ -51,7 +51,7 @@ fn full_name() -> io::Result<String> {
return Ok(String::new());
}
let mut buffer = vec![0u16; size as usize];
if unsafe { GetUserNameExW(NameDisplay, buffer.as_mut_ptr(), &mut size) } == 0 {
if !unsafe { GetUserNameExW(NameDisplay, buffer.as_mut_ptr(), &mut size) } {
return Err(io::Error::last_os_error());
}
Ok(String::from_utf16_lossy(&buffer[..size as usize]))
+3
View File
@@ -21,6 +21,9 @@ serde_json.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2.workspace = true
[target.'cfg(windows)'.dependencies]
windows-sys.workspace = true
[lints]
workspace = true
+80 -13
View File
@@ -6,7 +6,7 @@ use std::{
time::Duration,
};
use anyhow::{Context as _, Result};
use anyhow::Result;
use cef::{args::Args, *};
use futures::{FutureExt as _, StreamExt as _, channel::mpsc};
use gpui::{App, AsyncApp, Global, WeakEntity};
@@ -14,7 +14,9 @@ use gpui::{App, AsyncApp, Global, WeakEntity};
use crate::{handlers, surface::Shared, view::WebView};
/// The framework beside the app, else the fetched one.
#[cfg(target_os = "macos")]
fn framework_dir() -> Result<PathBuf> {
use anyhow::Context as _;
const NAME: &str = "Chromium Embedded Framework.framework";
let exe = std::env::current_exe()?;
let mut places: Vec<PathBuf> = Vec::new();
@@ -39,17 +41,12 @@ fn framework_dir() -> Result<PathBuf> {
static LOADED: OnceLock<std::result::Result<PathBuf, String>> = OnceLock::new();
/// Loads the framework once; every process needs it first.
/// Loads CEF once and sets its API version.
fn load() -> Result<PathBuf> {
LOADED
.get_or_init(|| {
let dir = framework_dir().map_err(|error| format!("{error:#}"))?;
let library = dir.join("Chromium Embedded Framework");
let name = std::ffi::CString::new(library.as_os_str().as_encoded_bytes())
.map_err(|error| error.to_string())?;
if unsafe { load_library(Some(&*name.as_ptr().cast())) } != 1 {
return Err(format!("CEF did not load from {}", library.display()));
}
let dir = open()?;
// Versioned calls exit the process until this runs.
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
log::info!("web: CEF loaded from {}", dir.display());
Ok(dir)
@@ -58,6 +55,51 @@ fn load() -> Result<PathBuf> {
.map_err(anyhow::Error::msg)
}
/// Loads the framework; its folder.
#[cfg(target_os = "macos")]
fn open() -> std::result::Result<PathBuf, String> {
let dir = framework_dir().map_err(|error| format!("{error:#}"))?;
let library = dir.join("Chromium Embedded Framework");
let name = std::ffi::CString::new(library.as_os_str().as_encoded_bytes()).map_err(|error| error.to_string())?;
if unsafe { load_library(Some(&*name.as_ptr().cast())) } != 1 {
return Err(format!("CEF did not load from {}", library.display()));
}
Ok(dir)
}
/// The folder of the libcef.dll this process linked.
#[cfg(windows)]
fn open() -> std::result::Result<PathBuf, String> {
use std::os::windows::ffi::OsStringExt as _;
use windows_sys::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
let name: Vec<u16> = "libcef.dll".encode_utf16().chain([0]).collect();
let module = unsafe { GetModuleHandleW(name.as_ptr()) };
if module.is_null() {
return Err(format!("libcef.dll is not loaded: {}", std::io::Error::last_os_error()));
}
let mut path = vec![0u16; 32_768];
let len = unsafe { GetModuleFileNameW(module, path.as_mut_ptr(), path.len() as u32) } as usize;
if len == 0 || len == path.len() {
return Err(format!("libcef.dll has no path: {}", std::io::Error::last_os_error()));
}
let file = PathBuf::from(std::ffi::OsString::from_wide(&path[..len]));
file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display()))
}
/// The folder of the libcef.so this process linked.
#[cfg(target_os = "linux")]
fn open() -> std::result::Result<PathBuf, String> {
use std::os::unix::ffi::OsStrExt as _;
let mut info: libc::Dl_info = unsafe { std::mem::zeroed() };
let entry = sys::cef_api_hash as *const libc::c_void;
if unsafe { libc::dladdr(entry, &mut info) } == 0 || info.dli_fname.is_null() {
return Err("libcef.so is not loaded".into());
}
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
let file = PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()));
file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display()))
}
/// In a CEF helper process, runs it to its end.
pub fn subprocess() -> Option<i32> {
// CEF's own parser needs the framework; plain args do not.
@@ -185,10 +227,11 @@ pub fn start(data: &Path, cx: &mut App) -> Result<()> {
crate::mac::adopt_app_protocol()?;
let exe = std::env::current_exe()?;
// Bundled, CEF finds its helper apps; else this binary helps.
let bundled = framework.parent().is_some_and(|dir| {
let name = exe.file_name().unwrap_or_default().to_string_lossy();
dir.join(format!("{name} Helper.app")).is_dir()
});
let bundled = cfg!(target_os = "macos")
&& framework.parent().is_some_and(|dir| {
let name = exe.file_name().unwrap_or_default().to_string_lossy();
dir.join(format!("{name} Helper.app")).is_dir()
});
let helper = if bundled {
String::new()
} else {
@@ -201,7 +244,12 @@ pub fn start(data: &Path, cx: &mut App) -> Result<()> {
external_message_pump: true as _,
no_sandbox: true as _,
browser_subprocess_path: helper.as_str().into(),
#[cfg(target_os = "macos")]
framework_dir_path: framework.to_string_lossy().as_ref().into(),
#[cfg(not(target_os = "macos"))]
resources_dir_path: framework.to_string_lossy().as_ref().into(),
#[cfg(not(target_os = "macos"))]
locales_dir_path: framework.join("locales").to_string_lossy().as_ref().into(),
root_cache_path: cache.to_string_lossy().as_ref().into(),
persist_session_cookies: false as _,
log_severity: LogSeverity::WARNING,
@@ -267,6 +315,7 @@ fn stop() {
}
/// Why CEF would refuse this cache: another app holds it.
#[cfg(unix)]
fn held(cache: &Path) -> Option<String> {
let lock = std::fs::read_link(cache.join("SingletonLock")).ok()?;
let lock = lock.to_string_lossy();
@@ -275,3 +324,21 @@ fn held(cache: &Path) -> Option<String> {
let alive = unsafe { libc::kill(pid, 0) } == 0;
alive.then(|| format!("another copy (process {pid}) is using {}", cache.display()))
}
/// Chromium's Windows lock names no process.
#[cfg(windows)]
fn held(_cache: &Path) -> Option<String> {
None
}
#[cfg(test)]
mod tests {
#[test]
fn cef_loads_with_its_api_version() {
let dir = super::load().expect("CEF loads");
#[cfg(not(target_os = "macos"))]
assert!(dir.join("locales").is_dir(), "{} holds CEF's files", dir.display());
// Without the version this call exits the process.
assert!(cef::command_line_create().is_some());
}
}
+1 -1
View File
@@ -280,7 +280,7 @@ wrap_display_handler! {
fn on_cursor_change(
&self,
_browser: Option<&mut Browser>,
_cursor: *mut u8,
_cursor: CursorHandle,
type_: CursorType,
_custom_cursor_info: Option<&CursorInfo>,
) -> ::std::os::raw::c_int {
+20
View File
@@ -7,6 +7,26 @@ src=$root/engines/git-src
out=$root/engines/git
jobs=$(sysctl -n hw.ncpu 2>/dev/null || nproc)
# Windows: Git for Windows' MinGit at the same version, checked.
case $(uname -s) in
MINGW* | MSYS*)
asset=MinGit-2.56.0-64-bit.zip
sum=064b440ff870ed5198527e8f3a92cdf5bd2fd0fedf5e718af95e3fdaddeff718
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
curl -fsSL -o "$work/$asset" "https://github.com/git-for-windows/git/releases/download/v2.56.0.windows.1/$asset"
got=$(shasum -a 256 "$work/$asset" | cut -d' ' -f1)
[ "$got" = "$sum" ] || {
echo "build-git.sh: $asset hash $got, want $sum" >&2
exit 1
}
rm -rf "$out"
unzip -q "$work/$asset" -d "$out"
"$out/cmd/git.exe" --version
exit 0
;;
esac
# reference/ stays read-only; the build runs in a copy.
mkdir -p "$src"
rsync -a --delete --exclude .git "$root/reference/git/" "$src/"
+52 -14
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Builds the pinned LibreOffice with the macOS LOK patches.
# Builds the pinned LibreOffice with Ely's LOK patches.
set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
@@ -40,10 +40,44 @@ options=(
--with-myspell-dicts
)
[ "$(uname)" = Darwin ] || {
echo "build-libreoffice.sh: macOS only; Linux and Windows use the TDF builds" >&2
exit 1
}
case $(uname) in
Darwin)
app=$src/instdir/LibreOffice.app/Contents/Resources
jobs=$(sysctl -n hw.ncpu)
;;
Linux)
# Collabora's LOKit set: headless, bundled libraries.
options+=(
--disable-gui
--enable-cairo-rgba
--disable-dbus
--disable-gio
--disable-dconf
--disable-randr
--disable-gstreamer-1-0
--without-system-cairo
--without-system-fontconfig
--without-system-freetype
--without-system-graphite
--without-system-harfbuzz
--without-system-openssl
--without-system-libpng
--without-system-libxml
--without-system-jpeg
--without-system-expat
--without-system-curl
--without-system-icu
--without-system-nss
--without-system-zlib
)
app=$src/instdir/share
jobs=$(nproc)
;;
*)
echo "build-libreoffice.sh: no build for $(uname)" >&2
exit 1
;;
esac
if [ ! -d "$src/.git" ]; then
git init -q "$src"
git -C "$src" remote add origin https://github.com/LibreOffice/core.git
@@ -59,21 +93,23 @@ git -C "$src" submodule update -q --init --depth 1 dictionaries
}
"$root/scripts/apply-lo-patches.sh" "$src"
for tool in make gnu-sed bison flex gettext; do
prefix=$(brew --prefix "$tool")
PATH=$prefix/libexec/gnubin:$prefix/bin:$PATH
done
export PATH PKG_CONFIG_LIBDIR=/var/empty PKG_CONFIG_PATH=
if [ "$(uname)" = Darwin ]; then
for tool in make gnu-sed bison flex gettext; do
prefix=$(brew --prefix "$tool")
PATH=$prefix/libexec/gnubin:$prefix/bin:$PATH
done
export PATH PKG_CONFIG_LIBDIR=/var/empty PKG_CONFIG_PATH=
fi
cd "$src"
if [ ! -f config_host.mk ] || ! printf '%s\n' "${options[@]}" | cmp -s - autogen.lastrun; then
./autogen.sh "${options[@]}"
fi
make -j"$(sysctl -n hw.ncpu)"
make -j"$jobs"
# Bundled dictionaries: English only; each one slows LOK start (PERF-01).
keep=(dict-en)
extensions=$src/instdir/LibreOffice.app/Contents/Resources/extensions
extensions=$app/extensions
for dict in "$extensions"/dict-*; do
name=$(basename "$dict")
if [[ " ${keep[*]} " != *" $name "* ]]; then
@@ -82,6 +118,8 @@ for dict in "$extensions"/dict-*; do
done
echo "build-libreoffice.sh: dictionaries kept: $(ls "$extensions" | tr '\n' ' ')"
# Ely's kernel defaults: Excel A1 formulas.
cp "$root/packaging/libreoffice/ely.xcd" "$src/instdir/LibreOffice.app/Contents/Resources/registry/"
cp "$root/packaging/libreoffice/ely.xcd" "$app/registry/"
"$root/scripts/inert-lo.sh" "$src/instdir/LibreOffice.app"
if [ "$(uname)" = Darwin ]; then
"$root/scripts/inert-lo.sh" "$src/instdir/LibreOffice.app"
fi
+36 -1
View File
@@ -9,10 +9,45 @@ jobs=$(sysctl -n hw.ncpu 2>/dev/null || nproc)
# reference/ stays read-only; the copy takes the patches.
mkdir -p "$src"
rsync -a --delete --exclude .git "$root/reference/mupdf/" "$src/"
case $(uname -s) in
MINGW* | MSYS*)
rm -rf "$src" && mkdir -p "$src"
tar -C "$root/reference/mupdf" --exclude .git -cf - . | tar -C "$src" -xf -
;;
*) rsync -a --delete --exclude .git "$root/reference/mupdf/" "$src/" ;;
esac
for patch in "$root"/patches/mupdf/*.patch; do
patch -d "$src" -p1 --forward --batch --quiet <"$patch"
done
case $(uname -s) in
MINGW* | MSYS*)
# MSVC: the static runtime Rust links, OpenSSL from vcpkg.
ssl=$(cygpath -w "$root/engines/vcpkg/installed/x64-windows-static")
[ -f "$ssl/lib/libcrypto.lib" ] || {
echo "build-mupdf.sh: no static OpenSSL; run engines/vcpkg/vcpkg install openssl:x64-windows-static" >&2
exit 1
}
cat >"$src/ely.props" <<PROPS
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemDefinitionGroup>
<ClCompile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<PreprocessorDefinitions>HAVE_LIBCRYPTO;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$ssl\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
</Project>
PROPS
msbuild=$("/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -latest -find 'MSBuild\**\Bin\MSBuild.exe' | tr -d '\r')
"$msbuild" "$(cygpath -w "$src/platform/win32/mupdf.sln")" -m -nologo -v:minimal \
-t:libmupdf -p:Configuration=Release -p:Platform=x64 -p:PlatformToolset=v143 \
-p:ForceImportAfterCppTargets="$(cygpath -w "$src/ely.props")"
rm -rf "$out" && mkdir -p "$out"
cp "$src"/platform/win32/x64/Release/*.lib "$out/"
ls "$out"
exit 0
;;
esac
flags="build=release HAVE_X11=no HAVE_GLUT=no HAVE_CURL=no HAVE_LIBCRYPTO=yes tesseract=yes"
# make does not track flags; a change rebuilds all.
if [ "$(cat "$out/.flags" 2>/dev/null)" != "$flags" ]; then
+3 -1
View File
@@ -4,7 +4,9 @@ set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
[ -d "$root/reference/cef-rs" ] || { echo "fetch-cef: run scripts/fetch-references.sh first" >&2; exit 1; }
work=$(mktemp -d)
# Not the system temp: Windows may block programs there.
mkdir -p "$root/engines"
work=$(mktemp -d "$root/engines/cef-export.XXXXXX")
trap 'rm -rf "$work"' EXIT
cd "$root/reference/cef-rs"
# The exporter checks each archive against CEF's own index.
+8 -2
View File
@@ -4,12 +4,18 @@ set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
exec_dir=$root/engines/git/libexec/git-core
git=$root/engines/git/bin/git
exe=
version=v3.8.0
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) asset=git-lfs-darwin-arm64-$version.zip sum=caff76a7d070d8160c89bc39b6e85d98f24135b6fed038a3b4de2590d25102d8 ;;
Darwin-x86_64) asset=git-lfs-darwin-amd64-$version.zip sum=f1c17aeca0b4eaab9ea606226477dbed3b84b56fe0811a9f967d2ea2b2393c53 ;;
Linux-x86_64) asset=git-lfs-linux-amd64-$version.tar.gz sum=e455e00f15d9b95661b8d53498ffb0c3367962cf1ec73c31ab7369516cd6ab8d ;;
Linux-aarch64) asset=git-lfs-linux-arm64-$version.tar.gz sum=ac9c8efac980bb0505ead384d087e2acb6486fd8498691a2165fa174ec6118c2 ;;
MINGW*-x86_64 | MSYS*-x86_64)
asset=git-lfs-windows-amd64-$version.zip sum=b62e7b8ceddee635f691233d77de8eaa4b213e9209e0173811d8cfa77f7882c1
exec_dir=$root/engines/git/ucrt64/libexec/git-core git=$root/engines/git/cmd/git.exe exe=.exe
;;
*) echo "fetch-git-lfs: no pinned build for $(uname -s)-$(uname -m)" >&2; exit 1 ;;
esac
[ -d "$exec_dir" ] || { echo "fetch-git-lfs: build Git first (scripts/build-git.sh)" >&2; exit 1; }
@@ -25,7 +31,7 @@ case "$asset" in
*.zip) unzip -q "$work/$asset" -d "$work/x" ;;
*) mkdir "$work/x" && tar -xzf "$work/$asset" -C "$work/x" ;;
esac
install -m 755 "$(find "$work/x" -type f -name git-lfs | head -1)" "$exec_dir/git-lfs"
install -m 755 "$(find "$work/x" -type f -name "git-lfs$exe" | head -1)" "$exec_dir/git-lfs$exe"
# The archive has no licence; the tag's own, checked.
curl -fsSL -o "$work/LICENSE.md" "https://raw.githubusercontent.com/git-lfs/git-lfs/$version/LICENSE.md"
got=$(shasum -a 256 "$work/LICENSE.md" | cut -d' ' -f1)
@@ -35,4 +41,4 @@ got=$(shasum -a 256 "$work/LICENSE.md" | cut -d' ' -f1)
}
mkdir -p "$root/engines/git/share/doc/git-lfs"
install -m 644 "$work/LICENSE.md" "$root/engines/git/share/doc/git-lfs/LICENSE.md"
"$root/engines/git/bin/git" lfs version
"$git" lfs version
+3
View File
@@ -21,6 +21,9 @@ fetch() { # name url rev [sparse paths...]
fi
rm -rf "$dir"
git init -q "$dir"
# Upstream bytes, whatever the host's line-ending setting.
git -C "$dir" config core.autocrlf false
git -C "$dir" config core.eol lf
git -C "$dir" remote add origin "$url"
if [ $# -gt 0 ]; then
git -C "$dir" sparse-checkout init --cone
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Usage: review.sh <item-id> <round> [path...] — codex high review.
# Usage: review.sh <item-id> <round> [path...] — codex high, fast review.
set -euo pipefail
[ "${HERDR_ENV:-}" = 1 ] || { echo "review.sh: not inside herdr" >&2; exit 2; }
@@ -22,6 +22,7 @@ Read the line for $id in $list and the PRD.md sections it cites (for tasks/BUGS.
Scope: $scope
Find defects only: behavior that breaks the PRD, errors swallowed, silent fallbacks, dead code, files over 500 lines, comments over one line or ten words, boxes inside boxes or useless subtitles in UI, abstractions with one user, missing or weak tests, claims no evidence backs. Build and run the tests yourself.
Copies of the tree you build for probes use their own CARGO_TARGET_DIR, never ./target: crates bake their source path in. Delete those copies and their target dirs before you write the verdict; keep only small logs.
On Windows, build and run nothing under %TEMP%: Application Control blocks programs there. Linux runs in WSL (wsl -d Ubuntu); its clone at ~/Documents/Github/ElyOffice holds this tree's changes.
Do not suggest enhancements. Do not reject for taste. Edit no file except the verdict.
Round 2 and later: read the previous verdict tasks/reviews/$id-r$((round - 1)).md, confirm each defect is fixed, then look for new defects in changed code.
Write the verdict to $verdict. Line one is PASS or FAIL. After that, one entry per defect: file:line, what is wrong, a concrete failing scenario, the direction of the fix.
@@ -36,7 +37,7 @@ pane=$(herdr pane split --current --direction right --cwd "$PWD" --no-focus | jq
echo "$pane" >"$last"
started=
for _ in 1 2 3 4 5; do
if err=$(herdr agent start "$name" --kind codex --pane "$pane" --timeout 120000 -- --model gpt-6.1-sol -c 'model_reasoning_effort="high"' -c check_for_update_on_startup=false 2>&1); then
if err=$(herdr agent start "$name" --kind codex --pane "$pane" --timeout 120000 -- --yolo --model gpt-6.1-sol -c 'model_reasoning_effort="high"' -c 'service_tier="fast"' -c check_for_update_on_startup=false 2>&1); then
started=1
break
fi
+26
View File
@@ -2,6 +2,12 @@
`docs/BugList.md` 的 93 条,按清单次序逐条处理:先核对当前代码,已由先前改动修好的写明出处,其余修好、审查、提交、推送后勾上。
## 0.1.3(用户 2026-10-10 定:修完全部缺陷与 K-01,Windows、Linux 与 macOS 同等可用,零错误零警告;Omarchy 上主题随 Omarchy;三端确认后经 Gitea 运行器发布 0.1.3)
次序:先 E-01(审查要能编译),再按下列清单次序逐条审查未审的修复与 K-01,最后三端与发布。
- [x] E-01 Windows 与 Linux(WSL)的构建环境:工具链、MuPDF、CEF、ElyMark 页面、tessdata、Git,`cargo check --workspace --all-targets` 两端通过(LibreOffice 随 L-01、W-01) — R1 CEF 未设 API 版本、macOS 编译回归、日志测试不触发错误、Windows 硬链接不认作同一文件、WSL 无 Git;R2 注释超长;R3 PASS
## 0.1.2 先做(用户 2026-10-09 定:先修致命与高,加自动更新,发布 0.1.2;其余随后按清单次序)
次序:S-03、P-01、X-01、D-01、D-02、P-02、P-03、P-04、M-01、X-02、X-03、K-01(Linux、Windows,本机不能运行)、UPD-1、REL-1、REL-2。K-01 不进 0.1.2:只影响 Linux 与 Windows 的 worker,本机不能运行验证,0.1.2 只发 macOS;留在下面的清单里,未勾。T-05 已改一半(审查查出缩放后尺寸不重测),暂放,补丁存在草稿区。
@@ -107,3 +113,23 @@
- [x] UPD-2 复核 UPD-1 第五轮的两处修复:启动检查(含 Gitea 查询)失败写日志;失败记录先于重开,以替身 open 断言 — R1 PASS
- [x] REL-1 dmg 里有指向“应用程序”的快捷方式,可直接拖入安装(用户 2026-10-09 报) — release.sh 在 dmg 里放“应用程序”替身,随 UPD-1 审查
- [x] REL-2 修完上列各条后打包、发布 0.1.2,官网指向新发布页 — v0.1.2 发在 git.zacharyzhang.com(六个 dmg、两个源码包、六份更新清单;release-macos.sh 通过;经 API 取清单与 dmg 散列核对一致),官网显示 0.1.2
## 0.1.3 三端与发布
- [ ] L-01 Linux 的 LibreOffice:钉住的源码加全部补丁在 Linux 构建,LOK 探测通过
- [ ] L-02 Linux 全工作区零警告构建,`cargo test --workspace` 通过(引擎齐全,无 SKIP)
- [ ] N-01 打印:Windows、Linux 交系统打印(与 macOS 同一份 PDF、纸张与取消规则),不再禁用
- [ ] N-02 ElyShow 媒体:Windows 以 Media Foundation、Linux 以 GStreamer 实现 `media::Player`(载入、可播、失败原因、帧、循环与播完)
- [ ] N-03 自动更新的安装与重开:Windows 安装程序、Linux AppImage,照 macOS 的退出后替换、失败记录与重开
- [ ] N-04 签名验证的证书信任:Windows 系统信任库、Linux 系统 CA,禁网,不核吊销
- [ ] N-05 ElyTree 与系统:停止 Git(Windows 结束进程树)、打开终端、在文件管理器中显示,Windows、Linux
- [ ] N-06 应用之间:Windows、Linux 的同级应用查找与启动、系统默认程序打开、单实例与诊断的崩溃报告位置
- [ ] L-03 Linux 真窗口:六个应用开样本、编辑、保存、打印禁用说明、设置、更新检查
- [ ] O-01 Omarchy:外观为“系统”时随 Omarchy 主题,切换主题即刻跟随(Omarchy 虚拟机里实测)
- [ ] L-04 Linux 打包:六个 AppImage,带引擎、许可与桌面项;更新清单含 linux-x86_64
- [ ] W-01 Windows 的 LibreOffice:同一源码与补丁在 Windows 构建,LOK 探测通过
- [ ] W-02 Windows 全工作区零警告构建,`cargo test --workspace` 通过(引擎齐全,无 SKIP)
- [ ] W-03 Windows 真窗口:六个应用开样本、编辑、保存、设置、更新检查
- [ ] W-04 Windows 打包:六个安装程序,带引擎、许可、文件类型;更新清单含 windows-x86_64
- [ ] CI-01 Gitea 工作流:打标签即在 macOS、Windows、Linux 运行器上构建、测试、打包并上传到发布
- [ ] REL-3 版本号 0.1.3,打标签触发运行器,三端产物与清单上传,官网指向新发布
+11
View File
@@ -0,0 +1,11 @@
FAIL
- crates/ely-web-content/src/engine.rs:66 — Windows/Linux 的 `load()` 漏掉 `api_hash(CEF_API_VERSION_LAST, 0)`,版本初始化只留在 macOS 分支。新进程中 CEF API 版本仍为 -1,后续需要版本化接口的调用会退出,ElyMark 的页面无法正常启动。两端直接载入本机 CEF 后调用 `cef_command_line_create()` 均以码 1 退出;同一探针先调用 `cef_api_hash(15400, 0)` 后成功返回。修法:各平台、主进程及子进程都在首次使用 CEF 前初始化并核对 API 版本,增加覆盖原生启动路径的测试。
- crates/ely-web-content/src/engine.rs:42 — 删除 `OnceLock` 的导入后,类型声明虽改成全限定名,初始化仍用 `OnceLock::new()`。macOS 编译该模块时报 E0433,导致原有 ElyMark 构建回归;提取该声明与改后的导入交给 rustc 已重现。修法:初始化也使用全限定名,或在 macOS 下恢复导入。
- crates/ely-platform/src/diagnostics/tests.rs:54 — 用目录代替循环符号链接,不能触发此处被测的错误:`log_part()` 只读取 metadata,目录的 metadata 成功,因而返回 `Ok(Some(...))`。Windows 与 WSL 运行 `cargo test -p ely-platform --lib diagnostics::tests::an_unreadable_log_is_an_error_not_nothing -- --exact` 均在第 55 行失败。修法:构造真正使 metadata 失败的跨平台场景,或明确要求拒绝非普通文件并补齐对应实现;保留对错误传播的有效断言。
- crates/ely-files/src/save/tests.rs:466 — 新增 Windows 硬链接场景实际不通过:`same_file()` 仅在 Unix 比较文件身份,Windows 只比规范路径,故把同一文件的硬链接认成新文件,绕过外部修改冲突检查。原生运行 `cargo test -p ely-platform -p ely-files --lib`,此测试在第 467 行失败,92 条通过、1 条失败。修法:在 Windows 按卷与文件标识比较文件身份,让该场景返回 `SaveError::Conflict`,不能靠撤掉硬链接断言过关。
- tasks/BUGS.md:9 — WSL 的 Git 尚未就位,不满足 E-01 的环境验收条件。`~/Documents/Github/ElyOffice/engines/git` 整个目录不存在,`engines/git/bin/git --version` 报不存在;未设置 `ELY_GIT`,也无 ElyTree Git 设置。系统虽有 Git/LFS,应用默认的 `git::locate()` 不使用它们,`Git::find()` 因默认路径缺失而拒绝运行。修法:在 WSL 完成 `scripts/build-git.sh` 与 `scripts/fetch-git-lfs.sh`,验证应用默认找到的 Git 及其 LFS 都能运行。
+3
View File
@@ -0,0 +1,3 @@
FAIL
- crates/ely-web-content/src/engine.rs:44 — 新增注释含 13 个单词,超过 agents.md 规定的十词上限。Windows 执行 `python tests/scripts/comments.py`、WSL 执行 `python3 tests/scripts/comments.py` 均在此行报错并以码 1 退出。修法:将该注释缩至十词以内,再运行两端的注释规则检查。
+1
View File
@@ -0,0 +1 @@
PASS