词库、用户配置和皮肤此前固定在 %LOCALAPPDATA%\metasequoiaime,整包几百 MB 全压在 C 盘。安装向导新增一页,允许把这部分数据放到任意盘。 程序本体仍固定装在 Program Files:MetasequoiaImeServer.exe 带 uiAccess=true, 只有在受信任目录里这个标志才生效。 运行期的权威来源是 HKLM\Software\Metasequoia\MetasequoiaIME 的 DataDir。 Server、TSF DLL 和引擎三个组件各自按同一顺序解析:METASEQUOIA_IME_DATA_DIR 环境变量 -> 该注册表值 -> %LOCALAPPDATA%\metasequoiaime。三处各持一份实现, 因为组件边界规则禁止 windows/ 与 server/ 互相 include。32 位 TSF DLL 用 KEY_WOW64_64KEY 读,避开 WOW64 重定向。 安全相关的两个约束: - 安装器在数据目录里写 .metasequoiaime-data 标记。覆盖安装的清理和卸载的整目录 删除只在看到这个标记(或目录就是历史默认位置)时才执行——用户可能把数据目录 指到一个本来就有自己文件的文件夹。 - 选择页与 /DATADIR= 走同一套校验:拒绝相对路径、盘符根目录、不存在的盘、程序 目录内部,以及 Windows/Program Files/各 AppData/用户主目录本身。 升级时改了位置,PrepareToInstall 在停掉输入法进程后用 robocopy /MOVE 搬运用户 拥有的数据(msime_user.db 及其 WAL/SHM/journal、config.toml、config.base.toml、 skins\),词库和前端资源由本次安装重新写入。跨卷移动目录 MoveFile 会失败,所以用 robocopy。用户词库搬运失败会中止安装:否则新版本对着空的 msime_user.db 启动, ssPostInstall 的回放只记一句 skipped,用户的自造词就静悄悄没了。 测试侧新增 METASEQUOIA_IME_CONFIG_DIR,只改 config.toml 的位置而不搬词库。原先 靠重定向 LOCALAPPDATA 做隔离的办法在装了正式版的机器上已不再成立——注册表值 的优先级高于它。
6.6 KiB
Windows 产品版本组合
一个 Windows 版本的完整一方源码就是本仓的一个提交:TSF、Server、引擎、GUI 框架、页面和安装器都是目录,不需要再被清单钉住。
product-lock.json 只覆盖仍来自仓外的输入,现在只剩词库 Release 的 source commit 和每个产物的 SHA256。辅助码在 engine/helpcode/,引擎本身也已内嵌,两者都由这个提交钉住,不再单列。
引擎没有 repositories 条目是刻意的:它不再有 gitlink,清单里写一个上游 commit 就等于声称这棵树仍等于上游,而第一个 Windows 专门化的改动落地时这句话就假了,也没有任何 CI 能发现。来源改记在 engine/UPSTREAM.md。
发布任务不解析 main 或 latest。递归 submodule(opencc、cpp-pinyin)沿固定 gitlink 检出,不执行 git submodule update --remote。
更新依赖
显式刷新后评审并提交清单,CI 通过后才能发布:
python scripts/product_lock.py refresh --dictionary-tag dict-v1.0.0
python scripts/product_lock.py validate
refresh 不解析任何浮动源码引用,--ref 已无可覆盖的对象:唯一被锁的源是词库发布,按指定 tag 取。
词库 tag 本身可能被上游修改,因此构建验证的是本清单中已提交的摘要,连同模型授权文件和上游校验文件一起验证。只更新上游 SHA256SUMS 无法让被替换的数据通过构建。词库清单另外声明构建它的 commit 和当时工作树是否干净,两者都要与清单相符——摘要证明字节没被换过,证明不了这批数据出自一个能重建的源。
重建与追溯
自动路径构建的是 release-please 刚刚建出的那个 draft,resolve-auto-release.sh 要求它钉在完整 commit SHA 上;发布时自动路径把产物挂到同一提交的 v<版本>-beta Pre-release,稳定的 v<版本> draft 保留给人工挑选后发布。手动 Release 只接受已有稳定 draft tag,由 validate-draft-release.sh 做同样的校验。两条路径都只读该提交内的清单。变更词库需要新的源码提交和产品版本,不允许临时覆盖词库 tag。没有清单的历史版本不能用新流水线宣称可复现重建。
发布节奏
发版由把 develop 提升到 main 触发;提升之后是全自动的,main 上每一次带 fix: / feat: 的合并都会走完构建、签名、发布。
签名额度不是约束。 证书允许每月 3000 次签名,而这个项目一个月的发布次数远低于这个量级。此前把「省签名次数」当作攒版本理由的说法不成立,不要据此调整合并策略。
真正花掉的是发布节奏本身。 自动链条一旦起跑就没有人工落点,「把若干修复攒成一个版本」和「这次不值得发」这类判断必须发生在它起跑之前。develop 就是这个落点:日常修复合进 develop 不产生任何版本,攒够一批再一次性提升到 main,那批提交会落在同一次 push 上、合成同一个版本。要让某次提升完全不递增版本,把提交类型写成 chore: / docs: / refactor: / test: 即可。
这一段原本记录的是相反的处境:默认分支曾经就是 main,消耗版本号的决定权因此落在「有没有人合了一个 fix:」上,用户会看到一串只差一两个提交的版本,release notes 也跟着碎。develop 模型把那个决定权收了回来。
换的是什么。 从合并到安装包可下载之间不再有人工环节,也就不会出现改动躺在 draft 里没人发的情况。
排队。 Release runs 按 concurrency: release-${{ github.ref }} 串行且不取消。一轮完整的自动发布 = 等 release PR 的 CI(十几分钟)+ 构建签名打包(二十分钟上下)。向 main 提升的频率高于这个节奏时队列只会越排越长,每个排队的 run 到头来都发一个版本。真出现这种情况,该调的是提升节奏或提交类型,不是把 cancel-in-progress 打开——那会在发布中途砍掉 run,留下挂着半个产物的 draft。
发布前 product_lock.py verify-published 要求清单里每个提交都能从各自仓库的默认分支到达,词库的 source_commit 一并检查——它是清单里仅剩的仓外输入,而摘要与词库清单只能证明这批数据出自锁文件写着的那个提交,证明不了那个提交有人合过。唯一的例外是已退休的词库仓库:它的历史 Release 是不可变的既有产物,仓库归档或删除后无从追溯默认分支,此时只记录不拦截。这一条只在发布路径上执行,不进 ci.yml:一个 PR 里出现尚未进默认分支的词库发布是正常的中间状态,在 PR CI 上强制会把它本该保护的那次落地直接锁死。到了发布这一刻判断相反——没进默认分支的输入就是没人合过的输入,锁文件为它背书等于宣称了一次并不存在的评审。各代码仓的默认分支现在是 develop,这条判据本身没有变:进了 develop 就是有人评审合过,发布提升与它无关。
安装包把 product-manifest.json 放进数据目录(默认 %LOCALAPPDATA%/metasequoiaime/,安装时用户可改到别的盘),Release 同时附加同名文件。其中包含本仓的发布提交、全部锁定输入和清单文件摘要。签名时间、工具链与 runner 更新仍可能改变二进制字节;此机制保证产品源码与数据组合确定,不承诺安装包逐字节一致。
本地检查:
python -m unittest discover -s tests -p 'test_product_lock.py'
python scripts/product_lock.py fetch-dictionaries --staging-root /tmp/msime-product-data
发布元数据令牌
版本 PR 的创建和更新使用 RELEASE_PLEASE_TOKEN,使正常 pull_request CI 自动运行并进入 PR 检查汇总。land-release-pr.sh 等待精确 head 的 PR CI 通过,再用 GITHUB_TOKEN 合并。维护 PR 的 release-please 调用设置 skip-github-release: true。它前后各有一次 skip-github-pull-request: true 的元数据调用,使用组织已有的 RELEASE_PLEASE_TOKEN:先补建已经手动合并的版本,再处理本次自动合并的版本,保持 release-please 原有的先发布、后计算下一版本的顺序。
后者需要仓库内容和 workflow 写权限(经典 PAT 的 repo + workflow,或等价的细粒度权限)。主分支先前包含 workflow 变更时,GITHUB_TOKEN 创建历史提交的 tag 可能被 Git refs API 拒绝。该令牌只更新版本分支,不用于合并;主分支仍由 GITHUB_TOKEN 写入,因此不会额外触发一次 Release。缺少令牌时提前报出配置错误。