launch-platform-host
GitHub指导构建、启动及验证水杉输入法多端原生宿主,涵盖依赖检查、防丢失构建技巧及真机自动化测试隔离配置。
触发场景
安装
npx skills add metasequoiaime/msime --skill launch-platform-host -g -y
SKILL.md
Frontmatter
{
"name": "launch-platform-host",
"description": "Build, launch and verify a 水杉输入法 platform host — macOS, iOS, Android, HarmonyOS, Linux or Windows. Use whenever the task is to run the product, see a change in the real app, screenshot a settings page, type on a real device with a script, or close an \"untested on device\" gap. Records the commands that work and the traps that do not announce themselves."
}
启动平台宿主
产品本体是 platforms/<os> 下的原生宿主。 AGENTS.md 开篇就写着这一条:不要把 Tauri 生成的工程当成某个平台的产品去启动、调试或验收;apps/desktop 的目录名和 Tauri 工程自带的 bundle id 都不构成例外。
所以「跑一下看看」永远从 platforms/<os> 开始,pnpm tauri dev 不是答案。
共享 React 设置页(打字统计、词库、背单词……)是原生宿主按需拉起的公共组件,而且只在三个平台上是设置界面:macOS 与 Linux 由宿主拉起 Tauri 设置应用,HarmonyOS 由 pages/Settings.ets 用 ArkWeb 加载、不经过 Tauri。Windows、iOS 与 Android 发布包的设置是原生界面,在那里看不到这些页面(归属见 ARCHITECTURE.md)。要看某个共享设置页,先把这三个平台之一的宿主编出来,再让宿主打开它。
先确认前置条件,不要假设没有
每条都有固定版本或固定来源。动手前先查本机有没有,查过再说「本机不具备」——凭印象断言缺依赖,代价是整块验证被跳过而没人知道。
| 依赖 | 用途 | 怎么找 |
|---|---|---|
| Sparkle 2.9.6 | macOS 更新控制器,不可省略 | 按 platforms/macos/README.md 下载校验后解压到独立目录,MSIME_SPARKLE_ROOT 指向含 Sparkle.framework 的那一层 |
| Android SDK + NDK 28.2.13676358 | Android 原生构建 | ANDROID_SDK_ROOT;NDK 在 $ANDROID_SDK_ROOT/ndk/28.2.13676358,版本由 build-native.sh 钉死 |
| vcpkg(锁定 commit) | Android 依赖 | MSIME_VCPKG_ROOT,或 target/tooling/vcpkg;commit 不符会直接拒绝 |
DevEco 命令行 hvigorw / ohpm |
HarmonyOS 打包 | DevEco Studio 自带,把其 command-line-tools/bin 加进 PATH |
| Docker | Linux 容器构建 | docker info |
MinGW x86_64-w64-mingw32-g++ |
Windows 交叉编译 | platforms/windows/build-cross.sh 会引导 vcpkg |
产物会消失,把构建和使用串成一条命令
target/ 是被忽略的构建输出。并发 worktree 多、磁盘接近满时,它会被清理掉——包括刚编好的 .app、静态库和 staged 资源。分两步跑,第二步很可能发现第一步的产物已经不在。
# 反例:两条命令,中间产物可能蒸发
bash platforms/ios/build-native.sh simulator
xcodebuild ... test # ld: library 'msime_host_api' not found
# 正例
bash platforms/ios/build-native.sh simulator && xcodebuild ... test
空间不够时先清自己的:target/{android-cargo,android-deps,macos-cargo,ios-cargo,resources} 都能重新生成。不要清别的 worktree。
macOS
产出 水杉输入法.app(IMK 输入法 bundle)。
CFLAGS="-mmacosx-version-min=13.0" CXXFLAGS="-mmacosx-version-min=13.0" \
CMAKE_OSX_DEPLOYMENT_TARGET=13.0 CMAKE_PREFIX_PATH="$(brew --prefix)" \
CARGO_TARGET_DIR=target/macos-cargo cargo build -p msime-host-api --locked \
&& cmake -S platforms/macos -B target/macos-isolated \
-DMSIME_HOST_LIBRARY="$PWD/target/macos-cargo/debug/libmsime_host_api.a" \
-DMSIME_SPARKLE_ROOT="<Sparkle-2.9.6 所在目录>" \
&& cmake --build target/macos-isolated --parallel
- 不要把最低版本写成全局
MACOSX_DEPLOYMENT_TARGET:rustc 会把它一并应用到为宿主编译的 proc-macro 动态库上,冷缓存构建以can't find crate for zerofrom_derive失败;cargo 不把该变量算进指纹,坏掉的产物会被后续构建继续复用,失败因此看起来时有时无。 - bundle 名字是中文。
cp -R target/macos-isolated/水杉输入法.app …会因 APFS 的 NFC/NFD 归一化报No such file or directory——用find target/macos-isolated -maxdepth 1 -name "*.app" -exec cp -R {} <目标> \;。 - 安装与输入源注册走
platforms/macos/scripts/install.sh。注册后有分钟级不稳定窗口:check_input_source.swift要隔几秒多查几次再下结论,只查一次两个方向都可能误判。 - 装完还打不出中文是正常的:
install.sh不准备词库。 输入会话要~/Library/Application Support/app.msime.macos/runtime-options.json,开发构建里没有(发布包由设置应用首次启动时写)。没有会话的控制器把按键原样交给应用,看起来就是「选了中文却打出英文」。补齐的三条命令在platforms/macos/README.md的《安装与输入源注册》开头;资源别留在target/resources,那里会被清掉。查现象用log show --predicate 'process == "水杉输入法"'找MSIME has no input session。 - 产品是
src/input/InputController.mm和src/core/ClientDictionaryRuntime.mm,改动要落在这两处;scripts/test-macos-orphan-sources.py守着不让不参与构建的源文件留在树里。 - 换新 bundle identifier 需要重新登录一次,这是 macOS 本身的限制,与签名和 plist 无关。
check_input_source.swift报某个模式disabled(典型是英文模式.Roman)而中文模式 enabled 时,安装是成功的,别去重新登录或反复重装:macOS 27 上进程启用不了键盘输入模式,TISEnableInputSource返回 noErr 而状态不变,苹果自己的模式一样如此。该脚本为此退 2(可用但有源未启用),退 1 才是不可用。测量见docs/macos-parity.md。- 钥匙串里有两张同名 Developer ID Application 证书时,
codesign会因名字歧义拒签。install.sh已改为按 SHA-1 取身份,要指定就把MSIME_SIGNING_IDENTITY设成哈希而不是名字。
看共享设置页时,Tauri 设置壳把 target/macos/水杉输入法.app 和 target/macos/EngineResources 列为 bundle 资源,两者必须先就位:
cargo run --quiet -p msime-client-core --example install_resources --locked -- target/resources
bash platforms/macos/stage-resources.sh target/resources/<上一步返回的目录>
mkdir -p target/macos && find target/macos-isolated -maxdepth 1 -name "*.app" -exec cp -R {} target/macos/ \;
开发构建照旧暂存全部资源(含日文词典,和 target/language-dictionaries 里有的粤拼、注音、笔画词库),它们是按需下载之外的内置兜底,所以开发包里这几个方案装好就能用。发布包只带核心词库,日文词典、五笔码表(水杉五笔除外)、粤拼/注音/笔画词库和手写模型由设置应用下载到 ~/Library/Application Support/app.msime.macos/resource-packs/<id>/;resources/voice-runtime-macos.lock.json 固定了重签的语音运行库之后,签名的发布包也不带 sherpa-onnx 运行库,改由资源包 voice-runtime-macos 下载。开发构建默认照旧内置运行库,要看省略之后的样子就在 CMake 配置时加 -DMSIME_BUNDLE_VOICE_RUNTIME=OFF,此时只有本机装着由同一 Team ID 签名的 voice-runtime-macos 资源包,本地识别才加载得了。要在本机测试下载流程,暂存时改用 MSIME_MACOS_OMIT_ON_DEMAND=1 bash platforms/macos/stage-resources.sh <目录>(不准备 target/language-dictionaries),并先删掉 resource-packs/ 下已装的资源包;cargo run --quiet --locked -p msime-client-providers --example install_resource_pack -- <绝对路径的 state_root> [japanese|language-dictionaries|handwriting] 用 App 同一个安装器直接装资源包。
iOS
bash platforms/ios/build-native.sh simulator
跑模拟器测试要静态库、staged 资源、生成好的工程三样同时在场,串成一条:
cargo run --quiet -p msime-client-core --example install_resources --locked -- target/resources > /tmp/ir.log \
&& RES=$(tail -1 /tmp/ir.log) && bash platforms/ios/stage-resources.sh "$RES" \
&& bash platforms/ios/build-native.sh simulator \
&& (cd platforms/ios && xcodebuild -project MSIMEClient.xcodeproj -scheme MSIMEClientTests \
-destination 'platform=iOS Simulator,name=<模拟器名>' test)
- scheme 是
MSIMEClientTests,MSIMEKeyboardTests是 target 名,直接用它报 "does not contain a scheme"。xcodebuild -list查全部。 xcodegen generate会做 spec 校验,缺target/ios/EngineResources及其中的msime-dictionary-manifest.json直接失败——先 stage 再生成工程。- 改了
App/Sources、SharedUI、KeyboardTests下的文件要xcodegen generate并提交project.pbxproj:它逐个列出源文件,不重新生成,新文件不会被编译。 - Xcode 27 的模拟器界面是
DeviceHub.app,Simulator.app已不存在;xcrun simctl一切照常,设备状态以xcrun simctl list devices为准。 - 验收系统键盘与主应用的共享设置时,模拟器构建使用
CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=YES。CODE_SIGNING_ALLOWED=NO的产物能启动,但没有 Xcode 链入的*-Simulated.xcent权限,主应用添加的方案可能传不到扩展;事后仅用codesign --entitlements重签也不能补回这段链接输入。安装并打开主应用后,用xcrun simctl get_app_container <设备> app.msime.ios groups确认有group.app.msime.ios,再开始跨进程验收。进程内构造键盘控制器的测试不能代替这一步。
Android
ANDROID_SDK_ROOT=<SDK> MSIME_VCPKG_ROOT=<vcpkg> bash platforms/android/build-native.sh arm64-v8a
导出与 ABI 核验(verify-native.sh 要三个参数):
READELF=$(find "$ANDROID_SDK_ROOT/ndk/28.2.13676358" -name llvm-readelf | head -1)
bash platforms/android/verify-native.sh "$READELF" target/android/jniLibs/arm64-v8a arm64-v8a
- vcpkg 树跨 worktree 共享,并发构建会撞
Failed to take the filesystem lock / Resource busy,等一会儿重试。 - 两个 APK 同包名同输出路径:
build-apk.sh是无 WebView 的原生壳,build-client-apk.sh是 Tauri 合包。哪一个都不是「那个 Android 应用」。 - 不需要 NDK 的纯 Java 门禁:
ANDROID_SDK_ROOT=… bash platforms/android/check-host.sh。它同一次运行里还会用 NDK 对client_jni.cpp做-fsyntax-only——那是唯一能抓出 Javanative声明与 C++ 导出不匹配的地方,没有 NDK 时它跳过并打印提示。
HarmonyOS
export PATH="<DevEco command-line-tools>/bin:$PATH"
bash platforms/harmony/stage-settings.sh # settings page from packages/ui; hvigor refuses to package without it
cd platforms/harmony && ohpm install && hvigorw assembleHap --no-daemon
产出 entry/build/default/outputs/default/entry-default-unsigned.hap。
- 复用缓存也必须带齐包内容。 Hvigor 不会把顶层符号链接形式的
entry/libs/当作原生库输入;另一检出的resources/resfile/缺失时也能构建和签名成功,但键盘缺词库就起不来。复用既有产物时把这两个暂存目录作为真实目录交给打包,安装前打开 HAP 核对原生库、核心词库、音效与许可证;仅改 ArkTS 时可与已验证的包逐项比较,除代码和源码映射外的内容应相同。签名校验与安装成功都不能代替这个检查。 ohpm install不能省:它建立entry/oh_modules/里的链接,import client from 'libmsimeclient.so'才能解析到.d.ts。不跑它,ArkTS 把整个 NAPI 边界当成无类型,构建照样成功——于是「构建通过」可能意味着一个原生调用都没被检查。hvigorw assembleHap是唯一编译 ArkTS 的地方。tsc全过、单测全绿、verify-local.sh --quick通过,都不代表 HAP 打得出来:曾有 13 个 ArkTS 错误进入 develop 而没有任何门禁发现。改过.ets就跑一次。- 设置页
rawfile/settings/index.html不提交,由stage-settings.sh在打包前从packages/ui构建;改了packages/ui重跑一次它再打包,设备上才是新界面。不要把这个文件加回版本库。
Linux
bash platforms/linux/build-container.sh # 固定容器内构建 IBus/Fcitx5 并跑 ctest
Tauri 外壳的 Linux 编译是唯一能看见 #[cfg(target_os = "linux")] 分支的地方——macOS 上 cargo check --workspace 根本走不到 msime-desktop,这个盲区曾攒下 36 个编译错误:
image="msime-linux-desktop-check:$(printf %s "$PWD" | shasum | cut -c1-12)"
docker build -q -t "$image" -f platforms/linux/tests/tools/Dockerfile.desktop-check platforms/linux/tests
docker run --rm -v "$PWD":/source \
-v "$PWD/target/linux-desktop-check":/ctarget -w /source \
-e CARGO_TARGET_DIR=/ctarget \
"$image" cargo check -p msime-desktop --locked --all-targets --message-format short
镜像与 verify-local.sh 用的是同一个 tag,apt 只在 Dockerfile 变化后装一次。
zsh 下不要照抄 verify-local.sh 里的 ${var:+-v "$x":/path} 写法——它会把一个前导空格带进参数,docker 报 includes invalid characters for a local volume name。把挂载路径写死。
Windows
bash platforms/windows/build-cross.sh x64 # MinGW 交叉编译,不需要 Windows 机器
跑过一次后 verify-local.sh 会在任何 worktree 里自动接管这条路径。
在别人的真机上自动打字
脚本打出来的字会变成用户的个人数据。 在用户自己的手机上跑测速或回归时,首选上屏的每个词都会被「记忆新词」记住,打字时自动造的词进了「我的词条」,云同步再把它们推到同一账号的其他设备,候选翻译把每页候选发给服务端。2026-10-29 在用户的 Pixel 上跑测速时这几项都开着:约一千次空格上屏全被学进了用户的词库,其中四到六成是错的首选。
所以动手打字之前:
- 先关掉三项、记下原值、结束后按原值恢复: 记忆新词、云同步、用水杉账号翻译候选。记录要落到文件里(读界面就存 uiautomator dump 或截图),恢复放在
trap … EXIT INT TERM里,脚本中途失败或被打断也要还原;恢复后冷启动设置应用再读一遍,确认真的回到原值。 - 恢复之前先停掉设备端的输入注入,确认停了再动设置。 Android 的
monkey、HarmonyOS 的uitest跑在设备上,杀掉本机的 adb / hdc 不会结束它们(脚本没有终端时 adb 不分配 PTY)。不先停,剩下的点击会落到正在恢复的设置页上误切别的开关,键盘重新起来后打进去的字又会被学进词库。Android 用adb shell pidof com.android.commands.monkey找到进程逐个kill,再查一次确认没有了;还在就别动设置,记录留着待恢复。 - 记录写明是谁持有的。 记录里写持有它的进程号;另一个脚本遇到持有者还活着的记录,不接管也不恢复,否则它结束时会把别人还在用的隔离提前撤掉。
- 先关云同步,最后再开。 记忆新词随账号同步(Android 的
input.learning,iOS 的platform.ios.dictionary_learning),云同步开着时先关学习,会把「关」传到用户的其他设备上。用水杉账号翻译候选(translation_account)不在同步字段里,关它不受同步影响。重新打开云同步会把全部分类标成待上传并马上比对一次;如果弹出「合并 / 使用云端」,停下来交给用户选,不要替用户点。这个对话框可能晚几秒才出来:每次读屏后都查一遍,恢复期间冷启动设置应用之前也先看一眼,否则 force-stop 会把它关掉,用户看不到这个选择。 - 同样会外发或学习输入的开关一并看一眼: 云候选(
cloud_candidates,把当前拼写发给所选的云候选服务;旧偏好文档缺这个键时按开启处理,用户手机上很可能开着)、AI 候选(ai_assistant,把拼音和前文发给用户配置的 AI 服务)、AI 辅助翻译(把候选交给ai_assistant里配置的服务翻译;各端的开关不同:桌面与 HarmonyOS 走 host-api 的selected_translation_services,看的是translation_ai、不看ai_assistant.enabled,只关 AI 候选挡不住它;Android 的CandidateTranslationPolicy.route与 iOS 的TranslationProviderPreference在没选别的翻译服务时按ai_assistant.enabled走 AI 翻译、不看translation_ai,只关translation_ai挡不住它。所以两项都关)、用小牛 / 腾讯 / 自定义服务的候选翻译(candidate_translations加niutrans/tencent_tmt/custom_translation)、「我的词条」的 WebDAV 词库同步。测试不是在测它们,就同样记录、关闭、恢复。匿名使用统计(usage_reporting)只发当天是否使用、会话是否正常结束和崩溃摘要,打字统计的「上传到云端」只传每日计数,都不含输入内容,不用为隔离去动。但测试打的字会计入用户本机的打字统计(字数、活跃时长、候选位次),开着「上传到云端」时还会合进账号的统计,显示在 QQ 群助手的/stats里:测试期间关掉上传并按原值恢复,或者在报告里说明哪几天的统计被测试污染了。 - 能用测试账号就用测试账号,最好是没登录账号的专用设备或模拟器;用户的手机只在必须真机时借用。
- 不要把测试文本写进用户的个人数据。 词库、常用语、剪贴板、皮肤都不往里加东西;已经学进去的,报告给用户并说明范围,不要自己去删用户的词。
三端的位置与偏好键(键都在共享偏好里,crates/client-core/src/preferences.rs;云同步不是偏好,开关在 Rust 的 cloud-sync/state.json,经 msime_client_cloud_sync 的 set_enabled 改):
记忆新词(learning,默认开) |
云同步 | 水杉账号翻译(translation_account,默认关) |
|
|---|---|---|---|
| Android | 词库 › 学习 › 记忆新词 | 我的 › 同步 › 云同步:词库、自造词和设置 | 我的 › 隐私 › 联网功能 › 用水杉账号翻译候选 |
| iOS | 词库 › 学习 › 学习常用词 | 我的 › 同步 › 云同步:只同步设置和无编码常用语,个人词库不经它 | 输入 › 翻译 › 翻译服务 选「水杉账号」;关掉是改选「不使用在线翻译」 |
| HarmonyOS | 词库 › 学习 › 记忆新词(共享设置页) | 没有自动云同步;账号页的「设置同步」是手动上传下载。自动外发「我的词条」的是词库页的 WebDAV「词库同步」,设置应用开着时按间隔运行 | 键盘不走水杉账号翻译(TranslationPolicy.provider 没有这一支);在线翻译经小牛、腾讯、自定义服务,或者 AI 辅助(ai_assistant 开着且 AI 总闸 AiGenerationGate 开时),关输入 › 翻译 › 候选词翻译(candidate_translations) |
Android 的发布 APK 不可调试,run-as 读不到偏好文件,只能经界面读写:开关行在 uiautomator dump 里是 checkable="true" 的 Switch 节点,checked 就是当前值,点行的中心切换。
证据分级
ARCHITECTURE.md 的五级,报告时不能跨级:
- 源码与单元测试
- 跨目标编译或容器构建
- 模拟器运行
- 真机或系统入口
- 安装、签名与真实编辑器验收
交叉编译通过不等于系统入口可用,模拟器跑通不等于真机验收完成。说到第几级就写第几级。
起了进程就要收尾
pnpm dev 的 Vite、直接跑起来的宿主二进制、Playwright / CDP 起的浏览器,都要在同一次会话里关掉。macOS 上无头 Chrome 会以同 bundle id 注册成前台实例,导致点 Dock 图标毫无反应;异常退出后按临时 profile 特征兜底清理:pkill -f playwright_chromiumdev_profile。
版本历史
-
c53e6d3
当前 2026-11-02 09:08
新增在他人真机上自动打字前的环境隔离说明(关闭云同步、学习及翻译),修正相关开关描述,并补充设备持有者规则与测试污染提示。
- e57a087 2026-10-18 15:29


