在M1上打包一个Electron项目

Typora开始收费了。作为五年前就开始使用这款软件的用户,感觉非常奇妙,就像一个大家都期待它发生但又觉得它不会发生的事情最后真的发生了。当然,三年前就没有用它了。因为我,真的,不喜欢,Electron.

此后,我偶然在评论区看到Marktext这个软件。点开主页看,跟Typora有几分相似,下载下来后方才发现,可执行文件是x86_64的,需要Rosetta 2才能在M1上运行。好在是自由软件,Apple Silicon上的Electron也很成熟了,我们可以自己动手。

不过,肯定有坑。

第一步:伟大的node_modules

没有在深夜痛苦地yarn install过的人,不足以谈人生。

项目的构建步骤还是挺清晰的,在仓库文档里。一个开源项目,如果没有文档指引其他人如何将其编译、运行起来,恐怕只能算半个开源。

首先我们clone以后,就开始yarn,等等等。然后打开,发现安装超时了。至于原因,懂的都懂。一个解决办法是用nrm切换安装源:

> npm install -g nrm

> nrm ls

  npm ---------- https://registry.npmjs.org/
  yarn --------- https://registry.yarnpkg.com/
  tencent ------ https://mirrors.cloud.tencent.com/npm/
  cnpm --------- https://r.cnpmjs.org/
  taobao ------- https://registry.npmmirror.com/
  npmMirror ---- https://skimdb.npmjs.com/registry/

> nrm use tencent

  Registry has been set to: https://mirrors.cloud.tencent.com/npm/

这里用了腾讯的源。切换之后,安装过程就不会超时了。

第二步:构建

没有人比我更懂C++!
——一位JavaScript程序员

然后我们又遇到了新的问题:

error marktext/node_modules/cld: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments: 
...
../src/cld.cc:9:12: error: no member named 'unexpected_handler' in namespace 'std'
using std::unexpected_handler;
      ~~~~~^
1 error generated.
make: *** [Release/obj.target/cld/src/cld.o] Error 1

看起来是某个C++代码编译出错了。std::unexpected_handler在C++17里被移除。我本想自己动手修改,不过还是先搜索一下这个包的GitHub仓库看有没有人提过这个问题。好在看最新的提交纪录,这个问题在2.7.1里已经修复。好吧,先手动改yarn.lock好了。

继续安装,又遇到奇怪的问题,类似:

name 'openssl_fips' is not defined while evaluating condition 'openssl_fips != ""' in binding.gyp while trying to load binding.gyp

查看Github issue意识到,是我v17的node太新,那就降级吧。非前端程序员是不配使用高贵的n/nvm/ndenv/nodenv/nodeenv的,只能用Homebrew:

brew install node@16
brew link --overwrite node@16 # 小朋友切勿随意模仿

舒服了……吗?

第三步:签名

然后就是惊险刺激的yarn run build release:mac了。

众所周知,所有运行在macOS上的App都要签名,Electron也不能例外。所以负责打包部分的electron-builder会帮你做这个事情。但是,签名它失败了!

Command failed: codesign --sign redacted --force --timestamp --options runtime --entitlements ...
Electron Framework: code object is not signed at all
In subcomponent: ...

然后我们发现,出现这个错误是因为App里还有其他可执行文件,我们需要递归签名,也就是给codesign命令多传一个--deep选项。但我们不好手动调用codesign,只能魔改node_modules,找到用来签名的代码(‌node_modules/app-builder-lib/electron-osx-sign/sign.js):

const args = [
  '--sign', opts.identity.hash || opts.identity.name,
  '--force',
  '--deep' # Add here
]

重新构建。Build目录里就会产生打包好的zip和dmg文件。

第四部:公证

Time and time again, I asked you.

为了测试这个软件被其他用户下载时能否正常打开,我将打包好的dmg上传到服务器再试着下载,悲剧地发现仍然有提示「无法打开『Mark Text』,因为Apple无法检查是否包含恶意软件」,然后只能手动在安全性设置里强制打开。这不科学。

查阅文档后发现,除了签名这一步,macOS上运行的App还需要Notarize(中文意为「公证」),也就是将App打包发送给Apple,运行一个自动化检查,通过公证后的App可以打上标记,这样用户在电脑上打开时就不会有安全性提示了。

自然,electron-builder也是可以帮你做这个事情的。在项目的electron-builder.yml中加入以下字段:

build:
  afterSign: "notarize.js"

如果你的Electron Builder配置文件是写在JSON里的,请把它改为对应的JSON记法。

然后创建notarize.js这个文件:

const { notarize } = require('electron-notarize')

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context
  if (electronPlatformName !== 'darwin') {
    return
  }
  const appName = context.packager.appInfo.productFilename
  return await notarize({
    # 请改为自己App的Bundle ID
    appBundleId: 'com.github.marktext.marktext',
    appPath: `${appOutDir}/${appName}.app`,
    # 临时打包用,所以直接填,真实项目里推荐使用dotenv
    appleId: 'MY_APPLE_ID_EMAIL',
    appleIdPassword: 'MY_APPLE_ID_SPECIAL_PASSWORD',
  })
}

注意。这里的Apple ID密码应该是你在Apple ID设置中生成的专用密码(如果你在其他平台客户端用过iCloud邮箱,你应该明白我在说什么),而不是你平时用来登录的那个密码。

好了吗?没有。重新build,的确如我们预期的一般,App被发送给Apple进行公证了。但很快看到错误通知和邮件,告知我公证失败。查看JSON报告,错误信息是这样的:

{
  "severity": "error",
  "code": null,
  "path": "Mark_Text.zip/Mark Text.app/Contents/MacOS/Mark Text",
  "message": "The binary is not signed with a valid Developer ID certificate.",
  "docUrl": null,
  "architecture": "arm64"
}

The binary is not signed with a valid Developer ID certificate.

什么!?我可是给库克交过钱的!你们怎么能这样对我?

冷静以后,我发现是用来签名的证书类型不对。通常来说,如果你之前只用过Xcode提交过App到Mac App Store的话,证书类型应该是Apple Distribution;而要在Mac App Store之外以dmg包发布App,签名的证书类型应该是Developer ID Application,所以需要找Apple重新签一个。

按照这篇文档的说明,用本地钥匙串生成一个证书颁发请求。然后登录到Apple Developer,进入Certificates选择创建新的Certificate,类型选择Developer ID Application,点击上传前面生成的证书请求文件。然后我们就得到了一份新的证书,点击导入钥匙串。

根据Electron Builder的文档,我们可以在环境变量里指定用来签名的证书ID:

# 在钥匙串里找到对应的证书名字
export CSC_NAME="YOUR_NAME (CERT_ID)"

大功告成。

顺便,因为Marktext的图标一直没有改为Big Sur的圆角正方形设计,我也自己改了一个。

你可以在这里下载。只有arm64的版本,因为它已经很大了,Universal会更大。图标在这里下载。

更多阅读

发表评论