一次 Tauri 应用的真实发布问题:开发机上运行正常,用户从浏览器下载后,macOS 却提示“App 已损坏,无法打开”。
原始 Issue:macOS 无法打开应用的反馈 #1
这类问题很适合用来理解桌面软件发布。写完程序只是第一步,真正交付给用户时,还要处理构建、打包、签名、系统信任、安装测试和后续更新。
1. 先搞清楚:能运行,不代表能发布
开发时,我们通常只关心:
写代码 → 编译 → 运行发布给别人后,链路会变长:
写代码 ↓Release 构建 ↓打包 ↓签名 ↓公证(正式 macOS 分发) ↓上传 ↓用户下载 ↓系统检查 ↓安装与首次启动所以“我电脑上能跑”只能说明程序本身基本可用,不能说明它已经适合分发。
这次反馈把排查重点指向了 分发与系统信任。Issue 中提到了 ad-hoc 签名,但仅凭报错和这条建议,还不能确认具体发行包的根因;仍需结合签名检查与下载测试判断。
本文以 Tauri 2 为例,整理这次反馈引出的发布知识。以下 macOS 命令需在 Mac 的终端中执行;构建命令在 Tauri 项目根目录执行,应用路径按实际名称替换。官方资料核对日期为 2026 年 9 月 15 日。
2. 为什么下载回来以后,macOS 的态度变了
同一个 .app,直接从开发目录启动和从浏览器下载回来,系统看到的并不完全一样。
浏览器下载的文件通常会带一个扩展属性:
com.apple.quarantine它可以理解为一个来源标签:
这个文件来自互联网,首次运行时需要额外检查。
查看一个 App 的扩展属性:
xattr -l "/Applications/Codex Session Manager.app"如果看到 com.apple.quarantine,说明它带有隔离标记。
有时网上会看到这条命令:
xattr -dr com.apple.quarantine "/Applications/Codex Session Manager.app"它的含义只是:删除 quarantine 扩展属性。
xattr:操作 macOS 扩展属性-d:删除-r:递归处理整个.app包com.apple.quarantine:要删除的属性
它可能绕过由隔离标记触发的部分检查,但不会修复损坏的文件、补齐签名或完成公证。仅在已确认来源与完整性的排查场景中使用,不应把它作为正常安装流程。
3. macOS 到底在检查什么
可以先记住四个概念:
Quarantine → 它从哪里来?Code Signing → 谁发布的?内容有没有被改?Notarization → Apple 是否检查过这个发行版本?Gatekeeper → 综合以上信息,决定是否允许运行3.1 Quarantine:记录来源
它主要告诉系统“这个文件来自外部”。它本身不等于病毒判断。
3.2 Code Signing:身份与完整性
代码签名可以近似理解成给发行包加一层可验证的封条。
系统能够检查:
- 谁签了这个程序;
- 签名后程序有没有被修改。
.app 其实是一个目录,里面还有可执行文件、资源、 Framework 等内容,例如:
MyApp.app/└── Contents/ ├── Info.plist ├── MacOS/ ├── Resources/ ├── Frameworks/ └── _CodeSignature/因此,签名完成后再修改包内文件,很可能让签名失效。
3.3 Notarization:Apple 对发行版本的检查
正式在 Mac App Store 外分发应用时,常见流程是:
Developer ID 签名 ↓提交 Apple Notary Service ↓检查通过 ↓Staple 公证凭据Developer ID 主要建立开发者身份;Notarization 针对具体发行版本做自动化安全检查。
3.4 Gatekeeper:最后的入口检查
Gatekeeper 会综合来源、签名、公证等信息,决定是否允许软件运行。
因此用户看到“已损坏”,可能涉及文件损坏、签名失效或系统安全检查失败。不能直接把它等同于“缺少公证”,也不能承诺删除隔离标记就能解决。不同提示及手动允许条件见 Apple 的安全打开应用说明。
4. 没有 Apple Developer 账号时,Tauri 可以怎么做
没有付费开发者账号时,可以先使用 ad-hoc signing。
在 src-tauri/tauri.conf.json 中配置:
{ "bundle": { "macOS": { "signingIdentity": "-" } }}或者构建时指定(此处假设项目的 package.json 已定义 tauri:build 脚本,并调用 Tauri CLI 的 build 命令):
APPLE_SIGNING_IDENTITY="-" npm run tauri:build这里的 - 表示 ad-hoc 签名。
ad-hoc 到底解决了什么
可以把两种签名简单区分为:
| 方式 | 完整性签名 | Apple 可验证开发者身份 | 适合场景 |
|---|---|---|---|
| ad-hoc | 有 | 无 | 开发、测试、早期开源项目 |
| Developer ID | 有 | 有 | 面向普通用户正式分发 |
ad-hoc 能让应用具备基本的签名结构,对 Tauri/macOS 构建很有价值,尤其是 Apple Silicon;但它不能替代 Developer ID 和 notarization。
Tauri 官方签名文档明确说明,ad-hoc 不会免除 macOS 的手动允许运行要求。系统是否提供“仍要打开”,还取决于具体提示和安全策略,不能把它视为所有“已损坏”报错的通用修复。
当前阶段比较合理的做法
Tauri Release Build ↓ad-hoc signing ↓生成 .app / .dmg ↓实际通过浏览器下载测试 ↓在 README 写清楚首次启动方式xattr -dr com.apple.quarantine ... 可以作为高级故障排查方案,但不建议把它写成普通用户的第一安装步骤。
5. 将来正式发布时,应该升级成什么流程
当项目用户变多,希望用户下载后直接获得更正常的 macOS 安装体验,就需要完整发布链路:
Release Build 与 App Bundle 准备 ↓Developer ID Application 签名 ↓按工具链准备提交包(如 ZIP / DMG) ↓Notarization ↓对支持的 App / DMG 执行 Staple ↓验证最终发行物并发布这里有一个很重要的顺序原则:
准备 App 内容 ↓完成签名 ↓按实际打包流程提交公证并 Staple ↓验证用户最终拿到的发行物 ↓发布签名之后再改二进制、Framework、资源或配置,都可能破坏签名。Staple 是附加公证票据的标准步骤,不等于任意修改包内内容。
公证提交包和最终下载包的关系取决于分发格式:不能把“生成 DMG”固定放在所有公证步骤之后。使用 Tauri 的自动签名、公证流程时,应按工具链处理打包顺序,再验证实际发布产物。参见 Apple 公证流程说明。
6. 遇到“已损坏”时,怎么查,而不是靠猜
用户界面的报错通常为了易懂,会隐藏很多底层细节。开发者应该直接检查发行物。
看 quarantine
xattr -l "/Applications/Codex Session Manager.app"看签名信息
codesign -dv --verbose=4 "/Applications/Codex Session Manager.app"验证签名
codesign --verify --deep --strict --verbose=4 \ "/Applications/Codex Session Manager.app"让 macOS 安全策略评估
spctl --assess --type execute --verbose=4 \ "/Applications/Codex Session Manager.app"可以这样理解:
codesign → 重点看签名本身是否有效spctl → 重点看 macOS 是否愿意放行它ad-hoc 签名的应用可能通过 codesign 完整性验证,却被 spctl 拒绝;这两项结果并不矛盾。策略评估也不能代替真实的下载与首次启动测试。
排查时记录 macOS 版本、CPU 架构、应用版本及命令输出,不要只盯着“已损坏”三个字,要继续追到系统真正拒绝的原因。
7. 发布测试:测用户拿到的版本
最容易犯的错误是:
npm run tauri:build ↓直接双击 target 目录里的 .app ↓能打开 ↓发布这里没有模拟用户真正经历的过程。
更可靠的测试方式是:
生成最终发行物 ↓上传到 Release ↓用浏览器重新下载 ↓解压 / 挂载 DMG ↓拖入 Applications ↓首次启动 ↓测试核心功能最好再用另一台 Mac、另一个 macOS 用户或干净环境测试一次。
关键原则只有一句:
测试用户最终拿到的发行物,不要只测试构建目录中的产物。
8. 软件作者还需要考虑什么
桌面软件做到“功能能用”之后,还需要继续考虑这些问题。
构建与兼容性
- Release 是否可以稳定复现;
- Apple Silicon / Intel 是否都支持;
.app、.dmg是否正确;- 依赖是否完整。
安全与信任
- 是否签名;
- 签名使用 ad-hoc 还是 Developer ID;
- 正式版本是否 notarized;
- 签名密钥和 CI Secret 是否安全;
- 发布后是否还能验证文件完整性。
安装体验
用户不应该为了运行普通软件,被迫先学习一堆终端命令。
好的安装体验通常意味着:
下载 → 安装 → 打开 → 使用如果必须有额外步骤,应在 README 和 Release Notes 中明确解释原因。
数据与升级
需要提前想清楚:
- 配置和用户数据存在哪里;
- 升级是否会丢数据;
- 卸载会留下什么;
- 自动更新如何验证更新包;
- 新版本失败时能不能回退。
日志与诊断
开发机出错时可以看终端,用户通常只会说:
“打不开。”
因此软件最好能提供版本号、日志位置、诊断信息和清晰的 Issue 模板。
9. 一份够用的发布 Checklist
构建前
- 版本号正确
- Release 配置正确
- Bundle Identifier 正确
- 图标、名称正确
- 确认目标 CPU 架构
构建与签名
- 使用 Release Build
- 没账号时至少配置 ad-hoc signing
- 正式发布时使用 Developer ID
- 正式发布完成 notarization / staple
- 签名完成后不再修改包内代码与资源,仅执行标准公证 / staple 步骤
验证
-
codesign --verify通过 -
spctl --assess结果符合预期 - 检查 quarantine 场景
- 浏览器重新下载后测试
- Apple Silicon / Intel 按支持范围测试
发布后
- Release Notes
- 安装说明
- 已知问题
- Issue 模板
- 日志 / 诊断方法
- 更新策略
10. 从这次问题真正学到的东西
这次经历可以浓缩成几条:
- 编译成功只是开发完成,不等于发布完成。
- 代码签名解决的是身份和完整性问题。
- Quarantine、Code Signing、Notarization、Gatekeeper 是不同层。
- ad-hoc signing 适合没有 Developer ID 时的过渡阶段,但不等于正式信任链。
- 发布测试必须覆盖“上传 → 下载 → 安装 → 首次启动”。
- 软件作者要为用户拿到软件之后的整个生命周期负责:安装、升级、数据、安全、诊断和维护。
把它们串起来,就是一条完整的软件交付链:
源码 ↓构建 ↓打包 ↓签名 ↓平台信任 ↓分发 ↓安装 ↓更新与维护当开始关心这条链路时,就已经从“把程序写出来”进入了“把软件可靠地交付给别人”的阶段。