11 月 9 号晚上十点半,上海市金山区。君陌工具箱的 Mac 版终于打出来了,我把包拷到另一台 Mac 上,双击那个图标,准备看它跑起来的样子。

屏幕上弹出来的是一行字:

「君陌工具箱」已损坏,无法打开。你应该将它移到废纸篓。

点了「移到废纸篓」,它就真被扔进去了。再解压一次,再双击,还是一样。换了台机器还是一样。

那会儿我第一反应是包打坏了,重新打了两遍。没用。第二反应是杀毒软件,把包扫了一遍,干净。

真实原因跟病毒一点关系都没有:未签名的 .app 一旦带上隔离属性,macOS 会直接判它不合格。这篇就把这两个坑和对应两条命令写清楚。

一、先搞清楚 macOS 到底在拦什么

macOS 上有三道闸,名字很像,作用不一样:

机制干什么的没它会怎样
代码签名(codesign)证明这个包从哪来、有没有被改过签名缺失,系统认定「来源不明」
Gatekeeper首次打开时判断是否放行直接弹「无法验证开发者」或「已损坏」
quarantine 隔离属性标记文件是「从外部进来的」从浏览器 / 压缩包 / 网盘拿到的文件都会被打上

关键在最后一条。文件是不是「从外部进来」,系统靠一个扩展属性记录:

com.apple.quarantine

用 xattr 就能看见:

xattr -l /Applications/君陌工具箱.app

com.apple.quarantine: 0083;65e7a2b1;Safari;

这一行里 Safari 就是来源。只要这个属性在、签名又不可信,双击必然被拦。而且 macOS 的提示文案会骗人——它说「已损坏」,其实是「我不认识这个包」。

二、第一步:给 .app 做 ad-hoc 签名

我没有 Apple 开发者账号,做不了正式签名(那个要 99 美元一年,后面说)。但 macOS 允许一种「我自己签我自己」的方式,叫 ad-hoc 签名,签名标识写成一个减号:

codesign --force --deep --sign - /Applications/君陌工具箱.app

三个参数各管一件事:

签完立刻验一下,看签名到底落在没落上:

codesign -dv --verbose=4 /Applications/君陌工具箱.app

Identifier=com.junmo.toolbox
Format=app bundle with Mach-O universal (x86_64 arm64)
Signature=adhoc
TeamIdentifier=not set

看到 Signature=adhoc 就对了。这一步不做,后面清隔离也没用——系统照样不认。

三、第二步:把隔离属性清掉

签名做完,属性还在,还是会被拦。清掉它:

xattr -cr /Applications/君陌工具箱.app

-c 是清除,-r 是递归到目录里所有文件。整个 .app 里几百个文件,每个都可能带属性,必须递归清。只想精确清隔离这一个属性,也可以:

xattr -dr com.apple.quarantine /Applications/君陌工具箱.app

两个命令效果差不多,我更常用 -cr,因为从压缩包解出来的东西经常不止一个属性。

四、验证:这次是真的能开了

清完不要直接双击,先用 Gatekeeper 自己问一遍:

spctl --assess --type execute --verbose /Applications/君陌工具箱.app

/Applications/君陌工具箱.app: accepted
source=no usable signature

accepted 就是放行。这时候再双击,它会正常起来:主窗口出来,26 个工具都能点,剪贴板、文件、截图这些本地功能全在。

如果 spctl 报 rejected,别急着怀疑命令,先看 xattr -l 是不是还有残留,以及签名有没有真的盖上(重跑一次 codesign -dv)。

五、在 electron-builder 里怎么配

如果包是用 electron-builder 打的,签名这一步可以在配置里就定好,省得每次出包再手动救。我的配置里跟 macOS 相关的是这几行:

"mac": {
  "target": ["dmg", "zip"],
  "category": "public.app-category.productivity",
  "identity": null,
  "hardenedRuntime": false
}

identity: null 的意思是「不要尝试用证书签名」。这适合没有开发者账号、只做内测包的情况——不设它,electron-builder 会去找钥匙串里的证书,找不着就报签名失败,包都出不来。

但要注意:identity: null 打出来的包是完全未签名的,不是 ad-hoc。所以出包之后还得补一遍:

codesign --force --deep --sign - dist/mac/君陌工具箱.app
xattr -cr dist/mac/君陌工具箱.app

想省这一遍,也可以把 identity 设成字符串 "-",让 electron-builder 直接走 ad-hoc。两种都行,我习惯出包后手动补,因为要顺手把最终包再复制一份到桌面的备份文件夹——打包目录被清理过一次,成品差点没了,从那以后每次都留一份。

顺带说两个打包侧的细节,都是这轮踩过的:

架构。我手上一台 M4、一台 2014 款 Intel Mac mini,两边都要能跑,所以出的是 universal 包(x64 和 arm64 合一个):

electron-builder --mac --universal

代价是体积翻倍。小工具无所谓,带 Chromium 的大应用会多出几十兆。前提是项目里没有不支持双架构的原生模块,纯前端的小工具一般没问题。

dmg 还是 zip。dmg 双击挂载,用户把图标拖进 Applications 最直观;zip 解压出来是裸 .app,适合自己快速测。我两个都出,救现场的时候用 zip 更快。

六、这套做法能用到什么程度

必须说清楚:ad-hoc 签名 + 清隔离,只适合自己用、或者小范围内测。它解决的是「我这台机器能不能打开」,不是「别人能不能放心装」。

真要对外分发,正规路子是 Apple Developer Program(99 美元/年),拿到 Developer ID 证书,流程是三步:

  1. 用 Developer ID 正式签名(不再是减号)
  2. 开 hardenedRuntime,配 entitlements
  3. 提交公证(notarytool),等 Apple 那边验完会给你一个 ticket

走完这三步,用户双击才不会看到任何警告。我现在的工具箱还在内测阶段,暂时不对外提供,所以停在 ad-hoc 这一步——能用、能自己测,就不去花那 99 美元了。

七、三条经验

1. 「已损坏」十个有九个不是真损坏

这个提示最容易把人带偏,让人以为包传坏了、重新打十遍。真正该看的是 xattr -l 里有没有 quarantine、codesign -dv 里有没有签名。两条命令两秒钟,比重新打包省事得多。

2. Electron 应用必须 --deep

Electron 打出来的 .app 里面有一整个 Chromium 框架和几个 helper 进程。只签外层壳子,里面的 helper 还是不认,双击照样起不来。--deep 别省。

3. 打包目录不是保险箱

我这边打包流程跑完会自动清 dist,有一次 mac 目录被清掉,成品没了,只能重打。现在固定动作是:包一出来就复制一份到桌面单独的文件夹,命名带上日期。多花十秒,避免重打半小时。

八、最后

君陌工具箱目前 26 个工具,全部本地运行、断网可用,数据不出本机,v1.0 还在内测阶段,暂不对外提供。这篇写的是把它打包成 Mac 版时踩的坑,跟软件本身的功能没关系——真正要发布的时候,签名和公证这块还得再走一遍正式流程。

打包、签名、分发这块有想聊的,站内交流,微信:lige0552。