Electron 程序支持 M1 芯片

原文地址:https://webfe.kujiale.com/electron-support-m1
酷家乐客户端:下载地址https://www.kujiale.com/activity/136

概述

目前存在两种架构芯片的 macbook,分别是 x86 和 arm 架构,对应了 Intel 芯片和 Apple M1 芯片。

旧版本酷家乐客户端,是通过 Rosetta 2 运行在 M1 芯片的 macbook 设备上的。新版本12.2.0之后的酷家乐客户端原生支持了 M1 芯片,本文主要说明酷家乐客户端是如何原生支持 M1 芯片的。

名词解释

酷家乐客户端:酷家乐官方基于Electron开发的桌面客户端,支持 windows 和 macos。

Rosetta:苹果提供的用于转译应用的工具。Rosetta 最初是用于将 PowerPC 的应用转译成 Intel 的应用,这次苹果发布 M1 芯片设备的同时发布了 Rosetta 2,用于将 Intel 的应用转译为可以在 arm 架构的 M1 芯片上运行的应用(相关文档:https://zh.wikipedia.org/wiki/Rosettahttps://support.apple.com/zh-cn/HT211861

为什么需要支持 M1 芯片

回顾苹果上一次的芯片架构替换,也就是从 PowerPC 换成 intel x86,在刚开始的2005年系统默认安装了 Rosetta 用于转译,而2009年系统不在默认安装 Rosetta,到2011年系统彻底不在支持 Rosetta。

也就是说目前提供的 Rosetta 2 也应该会在3、4年后不在默认提供,5、6年后彻底不在支持,所以支持 M1 芯片必须是要完成的。

x64 的安装包

酷家乐客户端之前发布的 macos 安装包是针对 x64 构建的,通过 Rosetta 是可以运行的。

但是需要你注意的是: electron 从 2020.7.22 - 2020.11.19 之间发布的版本由于chromium的崩溃问题(相关issue),彻底禁用了 electron 在 Rosetta 上运行(相关pr)。由于酷家乐客户端 v8.2.3 不在这个时间内,所以可以正常使用。

当然你可能会思考那些没有禁用 Rosetta 运行的 electron 版本上,没有 chromium 崩溃问题吗?根据我的实际测试,酷家乐客户端的 v8.2.3 是可以正常通过 Rosetta 上运行的。其实仔细查看那个关于 chromium 崩溃问题的 issue 中的讨论,有人提到在最终量产的 Mac 上的 Rosetta 应该没有这个崩溃问题(相关评论)。

arm64 的安装包

根据electron博客文章,从 v11.0.0 开始,electron 已经支持构建 darwin-arm64 的应用,可以不通过 Rosetta 转译,直接运行在 M1 芯片的机器上。

electron-packager

酷家乐客户端使用的构建工具 electron-packager,也支持构建 darwin-arm64 安装包了。只需要将打包参数里的 platform 值设置为 darwinarch 值设置为 arm64 即可。
electron-packager文档

electron-builder

另外一个社区中使用较多的打包工具electron-builder,可以使用 arch 值设置为 arm64
electron-builder文档

支持 Intel 和 Apple M1 的通用应用

现在已经可以分别打包 x64 和 arm64 的 macos 安装包,用于分发给用户。但是分成两个包,对用户来说,是有一定的理解成本的,用户可能根本不清楚自己应该安装哪一个。并且对于推送给用户的更新,也需要处理两种包的安装下载逻辑,所以就有了制作通用应用的需求。

通用应用,顾名思义就是可以同时原生运行在 Intel 和 Apple M1 芯片上的应用。这样用户只需要安装通用应用,更新推送也只需要推送通用应用,可以无缝替换之前安装的非通用应用。当然也有缺点,因为是合并了两个应用,所以安装包的体积也接近翻倍了。

构建通用应用

electron-builder

如果你使用的是 electron-builder,可以使用 arch 值设置为 universal 即可。

electron-packager

如果是和酷家乐客户端,以及 vscode 一样使用的是 electron-packager,则需要先使用 electron-packager 分别构建出 x64 和 arm64 应用,然后使用 electron 官方提供的构建工具 @electron/universal 将 x64 和 arm64 应用合并成通用应用。

import { makeUniversalApp } from '@electron/universal';
 
await makeUniversalApp({
  x64AppPath: 'path/to/App_x64.app',
  arm64AppPath: 'path/to/App_arm64.app',
  outAppPath: 'path/to/App_universal.app',
});

实际使用时的踩坑

hash检测报错

实际使用 @electron/universal 时会出现hash检测报错:

(node:10983) UnhandledPromiseRejectionWarning: Error: Expected all non-binary files to have identical SHAs when creating a universal build but "Contents/CodeResources" did not

可以看到,这里对所有的非二进制文件都进行了 hash 检测。

这是因为对于二进制文件,可以通过 lipo命令行工具 生成通用二进制文件。而非二进制文件,就必须只保留一份放入到最终的通用应用中,所以需要保证 hash 一致,这样就可以保证通用应用中的文件是和构建前的 x64 和 arm64 应用中的都是一致的。

但是从报错信息可以看到 CodeResources 有问题,而 CodeResources 是代码签名生成的,当然 hash 是不一致的。这时的解决方法就只能在生成 x64 和 arm64 的过程中,不进行签名,而是在生成通用应用之后,对通用应用进行签名即可,这个方式也是 electron-builder 的策略。

除了跳过 x64 & arm64 的签名以外,我在 vscode 的代码中找到来另外的策略:修改 @electron/universal 代码,支持对某些文件进行忽略检测 hash。

import { makeUniversalApp } from 'vscode-universal';
 
await makeUniversalApp({
  x64AppPath,
  arm64AppPath,
  x64AsarPath,
  arm64AsarPath,
  filesToSkip: [
    'product.json',
    'Credits.rtf',
    'CodeResources',
    'fsevents.node',
    'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds
    '.npmrc',
  ],
  outAppPath,
  force: true,
});

可以看到 vscode 自己 fork 了一个 vscode-universal 包,添加了 filesToSkip 参数,用于忽略某些文件对 hash 检测,其中就有 CodeResources

当然你可能会问,忽略了对 CodeResources 的检测,会不会有问题,其实答案很显然,通用应用的代码签名文件其实会覆盖掉这个 CodeResources,所以不会有问题。

添加 LSRequiresNativeExecution

根据苹果官方文档的说明,macos 系统在运行通用应用时,会自动根据当前设备所属的架构,选择运行正确的代码。但是在网上有看到用户的 M1 芯片的设备上,运行通用应用时未正确识别,仍然是通过 Rosetta 转译来运行应用。

所以这里可以在调用 makeUniversalApp 成功生成通用应用后,修改应用的 Info.plist 文件,添加上 LSRequiresNativeExecution,这个可以保证不使用 Rosetta 转译,当然前提是你必须已经确认应用可以在 Apple M1 芯片和 Intel 芯片的 macos 上正常运行。其实查看 electron 禁用在 Rosetta 上运行的 commit,可以发现也是通过添加 LSRequiresNativeExecution 来实现的。

仔细查看 vscode 的代码,针对通用包也添加了 LSRequiresNativeExecution

import * as plist from 'plist';
let infoPlistString = await fs.readFile(infoPlistPath, 'utf8');
let infoPlistJson = plist.parse(infoPlistString);
Object.assign(infoPlistJson, {
  LSRequiresNativeExecution: true,
});
await fs.writeFile(infoPlistPath, plist.build(infoPlistJson), 'utf8');

最后

欢迎大家在评论区讨论,技术交流。

对酷家乐前端团队感兴趣的同学可以把简历发到我的邮箱 xinming@qunhemail.com

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
comments powered by Disqus