Repository navigation
fix(macos): 应用包内自带 interface.json 时以可执行文件目录为项目目录 - #432
Conversation
There was a problem hiding this comment.
嗨——我发现了 3 个问题
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src-tauri/src/commands/utils.rs" line_range="306-310" />
<code_context>
- if let Some(project_dir) = bundle.parent() {
- return Ok(project_dir.to_path_buf());
+ if !exe_dir.join("interface.json").exists() {
+ if let Some(project_dir) = macos_app_bundle(exe_path)
+ .as_deref()
+ .and_then(std::path::Path::parent)
+ {
+ return Some(project_dir.to_path_buf());
}
}
</code_context>
<issue_to_address>
**Bundled resources resolve from wrong root**
When a macOS bundle contains `interface.json` in `Contents/MacOS` while `maafw` or project resources remain beside the `.app`, or the bundle sits beside its own project, `project_dir_for_executable` returns `Contents/MacOS` as the project root, so `get_maafw_dir` and `AppConfigState` resolve libraries, configuration, and project resources from the wrong directory; users see missing resources or the wrong project.
Resolve the project root from the neighboring project/resource layout instead of treating `Contents/MacOS` as the project root solely because it contains `interface.json`.
</issue_to_address>
### Comment 2
<location path="src-tauri/src/commands/utils.rs" line_range="306-310" />
<code_context>
- if let Some(project_dir) = bundle.parent() {
- return Ok(project_dir.to_path_buf());
+ if !exe_dir.join("interface.json").exists() {
+ if let Some(project_dir) = macos_app_bundle(exe_path)
+ .as_deref()
+ .and_then(std::path::Path::parent)
+ {
+ return Some(project_dir.to_path_buf());
}
}
</code_context>
<issue_to_address>
**Bundled interface is not found**
When a macOS bundle contains `interface.json` only in `Contents/Resources`, without a corresponding entry in `Contents/MacOS`, `project_dir_for_executable` checks only `Contents/MacOS/interface.json` and returns the directory outside the `.app`, so startup looks for the interface in the wrong place and reports it missing.
Check `Contents/Resources` for `interface.json` when resolving the project directory.
</issue_to_address>
### Comment 3
<location path="src-tauri/src/commands/utils.rs" line_range="301" />
<code_context>
+ project_dir_for_executable(&exe_path).ok_or_else(|| "无法获取 exe 所在目录".to_string())
+}
+
+fn project_dir_for_executable(exe_path: &std::path::Path) -> Option<PathBuf> {
+ let exe_dir = exe_path.parent()?;
</code_context>
<issue_to_address>
**Corrupt bundled interface blocks fallback**
When a macOS bundle contains an incomplete, unreadable, or malformed `Contents/MacOS/interface.json` while a valid interface exists beside the bundle, `get_exe_directory` selects the executable directory based only on the file’s existence. `AppConfigState::load_interface` then returns on a read or parse error without trying the valid neighboring project interface, leaving the interface unavailable to callers.
Validate that the bundled interface can be read and parsed before selecting that directory, and fall back to the neighboring project directory if loading it fails.
</issue_to_address>Original comment in English
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src-tauri/src/commands/utils.rs" line_range="306-310" />
<code_context>
- if let Some(project_dir) = bundle.parent() {
- return Ok(project_dir.to_path_buf());
+ if !exe_dir.join("interface.json").exists() {
+ if let Some(project_dir) = macos_app_bundle(exe_path)
+ .as_deref()
+ .and_then(std::path::Path::parent)
+ {
+ return Some(project_dir.to_path_buf());
}
}
</code_context>
<issue_to_address>
**Bundled resources resolve from wrong root**
When a macOS bundle contains `interface.json` in `Contents/MacOS` while `maafw` or project resources remain beside the `.app`, or the bundle sits beside its own project, `project_dir_for_executable` returns `Contents/MacOS` as the project root, so `get_maafw_dir` and `AppConfigState` resolve libraries, configuration, and project resources from the wrong directory; users see missing resources or the wrong project.
Resolve the project root from the neighboring project/resource layout instead of treating `Contents/MacOS` as the project root solely because it contains `interface.json`.
</issue_to_address>
### Comment 2
<location path="src-tauri/src/commands/utils.rs" line_range="306-310" />
<code_context>
- if let Some(project_dir) = bundle.parent() {
- return Ok(project_dir.to_path_buf());
+ if !exe_dir.join("interface.json").exists() {
+ if let Some(project_dir) = macos_app_bundle(exe_path)
+ .as_deref()
+ .and_then(std::path::Path::parent)
+ {
+ return Some(project_dir.to_path_buf());
}
}
</code_context>
<issue_to_address>
**Bundled interface is not found**
When a macOS bundle contains `interface.json` only in `Contents/Resources`, without a corresponding entry in `Contents/MacOS`, `project_dir_for_executable` checks only `Contents/MacOS/interface.json` and returns the directory outside the `.app`, so startup looks for the interface in the wrong place and reports it missing.
Check `Contents/Resources` for `interface.json` when resolving the project directory.
</issue_to_address>
### Comment 3
<location path="src-tauri/src/commands/utils.rs" line_range="301" />
<code_context>
+ project_dir_for_executable(&exe_path).ok_or_else(|| "无法获取 exe 所在目录".to_string())
+}
+
+fn project_dir_for_executable(exe_path: &std::path::Path) -> Option<PathBuf> {
+ let exe_dir = exe_path.parent()?;
</code_context>
<issue_to_address>
**Corrupt bundled interface blocks fallback**
When a macOS bundle contains an incomplete, unreadable, or malformed `Contents/MacOS/interface.json` while a valid interface exists beside the bundle, `get_exe_directory` selects the executable directory based only on the file’s existence. `AppConfigState::load_interface` then returns on a read or parse error without trying the valid neighboring project interface, leaving the interface unavailable to callers.
Validate that the bundled interface can be read and parsed before selecting that directory, and fall back to the neighboring project directory if loading it fails.
</issue_to_address>
Windsland52
left a comment
There was a problem hiding this comment.
LGTM with two nits.
我核对了代码和 MaaEnd v3.0.0 官方 DMG 的实际布局:install.yml 会把 install/* 全部拷到 MaaEnd.app/Contents/MacOS/,所以 interface.json、locales/、maafw/、resource/ 都在 Contents/MacOS(Contents/Resources 只有 maaend.icns)。docs/macos-deployment.md 里的 --repair-layout 则把非 Mach-O 文件移到 Contents/Resources/MXU-portable,并在 Contents/MacOS 原位置留相对 symlink。两种布局下 exe_dir/interface.json 的 .exists() 都能命中,修复路径成立。
两个非阻塞建议:
-
exists()!= 可用(同 Sourcery 的 Medium)。is_file()可以挡掉“目录冒充 interface.json”,但它不解决 unreadable/corrupt 文件;如果要把 Sourcery #3 一并解决,需要 read/parse 失败时显式回退,而不只是is_file()。至少建议is_file()+ 一条log::info!记录最终选中的 project root,便于排查 bundled-vs-external 的歧义。 -
隐式约定:bundle 内有
interface.json就表示整个项目根是Contents/MacOS——maafw/、dist/、import、locales都会在 bundle 内解析。建议在docs/macos-deployment.md补一句:bundle 内自带 interface 意味着项目应完全自包含,不要再与包外资源混用。
CI/测试备注:当前 build.yml 只有 pnpm tauri build,没有 cargo test;新增的 macOS-only 测试不会被 CI 执行,这次 CI 绿只代表编译通过。合并前在 macOS 本地跑一次 cargo test macos_project_dir_follows_interface_location 会更稳。
另外,Sourcery 的两条 High 我认为基本是 overstatement:--repair-layout 的常规迁移路径会给移动过的文件留 symlink,Contents/Resources/interface.json 无 MacOS 入口的场景不会出现;mixed layout 也不在现有文档契约内。补充一点:planBundleLayout() 只扫描 Contents/MacOS,如果 Contents/Resources/MXU-portable 里已有 interface 而 MacOS 没有 symlink,--repair-layout 也不会补;所以不要按“绝不可能”处理,但也不是本 PR 的阻塞项。
macOS 的 MXU 包改成和 MaaEnd 一样的 `.app` + dmg 分发形态。 > 基于 [#1012](https://github.com/MAA1999/M9A/pull/1012)(入口解析修复):没有它,macOS 包在 > `build-release.mjs` 阶段就构建失败。本 PR 的 base 指向 `fix/mxu-macos-app-layout`,#1012 合并后 > 会自动回落到 `main`。 ## 产物形态 | | 之前 | 现在 | | --- | --- | --- | | macOS MXU | `M9A-macos-aarch64-vXXX-MXU.tar.gz`(裸 `m9a`) | `M9A-macos-aarch64-vXXX-MXU.dmg`(内含 `M9A.app`) | | macOS MFAA | `.tar.gz` | 不变 | ## 包内布局 新增 [tools/macos-app-bundle.mjs](tools/macos-app-bundle.mjs),`dist/package-mxu` → `M9A.app`。 CI 里 `ls -l dist/M9A.app/Contents/MacOS` 的实际输出(macos-aarch64): ``` total 79592 -rwxr-xr-x 1 runner staff 40748176 Nov 7 10:33 m9a lrwxr-xr-x 1 runner staff 31 Nov 7 10:33 agent -> ../Resources/MXU-portable/agent lrwxr-xr-x 1 runner staff 38 Nov 7 10:33 announcement -> ../Resources/MXU-portable/announcement lrwxr-xr-x 1 runner staff 33 Nov 7 10:33 CONTACT -> ../Resources/MXU-portable/CONTACT lrwxr-xr-x 1 runner staff 30 Nov 7 10:33 data -> ../Resources/MXU-portable/data lrwxr-xr-x 1 runner staff 40 Nov 7 10:33 interface.json -> ../Resources/MXU-portable/interface.json lrwxr-xr-x 1 runner staff 33 Nov 7 10:33 LICENSE -> ../Resources/MXU-portable/LICENSE lrwxr-xr-x 1 runner staff 33 Nov 7 10:33 locales -> ../Resources/MXU-portable/locales lrwxr-xr-x 1 runner staff 34 Nov 7 10:33 logo.ico -> ../Resources/MXU-portable/logo.ico lrwxr-xr-x 1 runner staff 31 Nov 7 10:33 maafw -> ../Resources/MXU-portable/maafw lrwxr-xr-x 1 runner staff 32 Nov 7 10:33 python -> ../Resources/MXU-portable/python lrwxr-xr-x 1 runner staff 35 Nov 7 10:33 README.md -> ../Resources/MXU-portable/README.md lrwxr-xr-x 1 runner staff 34 Nov 7 10:33 resource -> ../Resources/MXU-portable/resource lrwxr-xr-x 1 runner staff 31 Nov 7 10:33 tasks -> ../Resources/MXU-portable/tasks ``` `Contents/MacOS` 只有 `m9a` 一个真实文件,其余都是指向 `Contents/Resources/MXU-portable` 的包内相对 符号链接:MXU 仍能在可执行文件旁找到 `interface.json`,而数据不落在代码目录——MXU 的 [docs/macos-deployment.md](https://github.com/MistEO/MXU/blob/v2.10.0-beta.1/docs/macos-deployment.md) 写明数据放进 `Contents/MacOS` 会让 `codesign` 报 `code object is not signed at all / In subcomponent: ...json`,整个 `.app` 并未签名成功,macOS 通知随之失效。 **为什么是顶层链接、而不是逐文件链接**:第一版按文档做了逐文件双向链接并用 `--deep` 签名,在真实 macOS runner 上直接失败: ``` M9A.app: bundle format unrecognized, invalid, or unsuitable In subcomponent: .../Contents/MacOS/python/include/python3.13 ``` `--deep` 会自己去找子组件,把「目录里只有符号链接」的依赖树当成异常 bundle。改成现在的形态:每个 顶层条目一条链接,嵌套的 75 个 Mach-O 先逐个 `codesign --force --sign -`,最后封整个 bundle、**不带 `--deep`**,`codesign --verify --strict` 一次通过。 ## Agent 数据目录 `Contents/MacOS` 和应用包放在 `/Applications` 时,项目根目录对普通用户只读,而 Agent 原来把 `config/`、`debug/`、`data/manifest_cache.json` 写在工作目录下([runtime_paths.py](agent/utils/runtime_paths.py)、 [logger.py](agent/utils/logger.py) 导入时就会 `makedirs`)——放进 .app 后会写进应用包、甚至直接 `PermissionError` 起不来。 按 PI 协议 v2.11.0 补上 `PI_USER_DATA_DIR` 支持:Client(MXU v2.10 起)本来就会把这个可写目录传给 子进程,M9A 之前没用。现在 `default_work_root()` 优先取该变量,未设置时(MFAAvalonia)行为不变; `Tasker.set_log_dir` 也改走解析后的 `debug/agent`。同一个修复顺带纠正了版本检查: `version_checker` 之前按 work_root 找 `interface.json`,现在按 project_root。 ## 其他改动 - [release.yml](.github/workflows/release.yml#L192-L247):macOS 的 MXU 资产改 `.dmg`,Release 上传补 `*.dmg`。 - [package-smoke.yml](.github/workflows/package-smoke.yml):macOS job 每次构建 `M9A.app`、生成 icns、 逐层签名、验签并打一个 dmg;该步骤放在扁平包断言之后(组装会搬走包内容)。 - 文档(中/英 newbie):下载表、架构说明、macOS 使用步骤(dmg → 拖进「应用程序」)、Gatekeeper 说明。 ## 验证 CI(`Package Smoke` 42370955711,6/6 绿): ``` [OK] signed 75 nested Mach-O file(s) M9A.app: valid on disk M9A.app: satisfies its Designated Requirement created: dist/M9A-package-smoke.dmg ``` 另外新增了两步运行时验证(不再只是静态打包): - `Verify macOS bundle runs`:把 `M9A.app` 复制成只读副本(模拟 `/Applications`),在包内用 嵌入式 Python 导入 MaaFW(解析到 `Resources/MXU-portable/maafw/libMaaFramework.dylib` ✓), 并以 `PI_USER_DATA_DIR` 真跑一次 `agent/main.py`,断言日志落在数据目录、包内零写入 ✓ - `Probe the MXU GUI`:直接启动包里的 `m9a`,读 `~/Library/Application Support/MXU/debug/` 的 日志——**这一步暴露了一个上游 bug**,见下节。 本地: - `uv run --frozen pytest -q` → 611 passed(新增 `test_macos_app_bundle.py`、`test_runtime_paths.py`) - `pnpm check` → 通过 ## 上游依赖:MaaXYZ/MXU#432(已解决) v2.10.0-beta.1 的 `get_exe_directory()` 在 `.app` 内运行时把项目目录解析为 `.app` 的父目录, 探针日志曾打出 `interface.json not found at ".../dist/interface.json"`。 [MaaXYZ/MXU#432](MaaXYZ/MXU#432) 已合并并随 **v2.10.0-beta.2** 发布:`.app` 内存在 `interface.json`(符号链接也算)时回退到可执行文件目录。Package Smoke (run 42382951270)在 beta.2 下的实测输出: ``` [OK] MXU stayed alive for 20s [OK] MXU found interface.json inside the bundle (MaaXYZ/MXU#432 or later) ``` 探针步骤继续保留,每次构建都会打印 `[OK]`/`[WARN]` 判定,防止上游再次回归。 ## 还没验证的 没有在真 Mac 上端到端跑过任务(需要一台 Mac 双击验收)。CI 已覆盖:组装、签名、验签、dmg、 包内导入 MaaFW、只读包下 Agent 数据目录外置、GUI 启动与 interface 查找(beta.2 实测通过)。
找不到 interface 爆炸了

Sourcery 摘要
修正 macOS 应用包的项目目录定位,确保能够正确发现内置的接口配置。
错误修复:
interface.json时项目目录定位错误的问题,避免运行时找不到接口配置。功能增强:
interface.json的位置,在应用包目录与可执行文件目录之间进行选择。测试:
interface.json的场景。Original summary in English
Summary by Sourcery
修正 macOS 应用包的项目目录定位,确保内置 interface 配置能够被正确发现。
Bug Fixes:
interface.json时项目目录定位错误,避免运行时找不到 interface 配置。Enhancements:
interface.json的位置在应用包目录与可执行文件目录之间选择。Tests:
interface.json的场景。