This page looks best with JavaScript enabled

Electron Mac Universal Package

 ·  ☕ 3 min read

目前我们新产品的安装包是 x86_64 架构的,在 mac m1/m2 芯片的电脑上运行时,性能、流畅性相比在 intel 芯片 mac 上运行都明显卡顿。所以迫切的需要针对 apple silicon 出包。

ffi 支持 arm64

在 m1 上直接运行 x86_64 的程序,ffi 加载动态库时报错,这个报错是 ffi 库加载本身失败了

load libsscronet.dylib failed ["Error","No native build was found for platform=darwin arch=arm64node=16.15.0 electron=20.1.0
    loaded from: node_modules/ref-napi
"]

原因是 ffi-napi 输出的 native 模块缺少 arm64 的版本

ffi-napi

ffi-napi 的发布原理是在 travis ci 中使用 prebuildify 构造 native 模块,prebuildify 根据当前运行的平台和架构打出对应架构的 Addons 文件,但是 ff-napi 已经很久没人维护了,ci 貌似也没维护运行。
找到一个 koffi,支持多平台、多架构,用来替换 ffi-napi

koffi

koffi 在 webpack 打包时会将它所支持的所有平台的 node 二进制文件都放到包里,每个平台接近 4m 的 node ,会导致安装包体积增加较多,所以需要修改 eletron-builder 的打包配置,只将需要的的平台打包进安装包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 所有平台的 node 文件夹名
const KoffiNodes = [
    'freebsd_arm64',
    'freebsd_ia32',
    'freebsd_x64',
    'linux_arm32hf',
    'linux_arm64',
    'linux_ia32',
    'linux_riscv64hf64',
    'linux_x64',
    'openbsd_ia32',
    'openbsd_x64',
    'win32_arm64',
    DarwinArm64FFI,
    DarwinX64FFI,
    Win32FFI,
    Win64FFI
]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function appendKoffiNode(nodes) {
    return `!node_modules/koffi/build/koffi/{${nodes.join(',')}}`
}

// windows 只保留 win32/win64 对应的文件
const files = [...commFiles].concat(['**/*.ico'])
const nodes = [...KoffiNodes]
nodes.splice(nodes.indexOf(arch == 'x64' ? Win64FFI : Win32FFI), 1)
files.push(appendKoffiNode(nodes))

// mac 只保留 x64/arm64 对应的文件
const files = [...commFiles]
const nodes = [...KoffiNodes]
nodes.splice(nodes.indexOf(arch == 'x64' ? DarwinX64FFI : DarwinArm64FFI), 1)
files.push(appendKoffiNode(nodes))

如果打 universal 包,则需要将 darwin_x64 和 darwin_arm64 都打到包里

动态库支持 arm64

ffi 支持了 arm64 之后,在 arm64 机器上加载动态库失败,动态库的架构和机器的架构不匹配

1
2
3
4
5
6
load libimcppsdk.dylib failed Error Failed to load shared library: dlopen(libimcppsdk.dylib, 0x0002): tried: 'libimcppsdk.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OSlibimcppsdk.dylib' (no such file), 'libimcppsdk.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')) Error: Failed to load shared library: dlopen(libimcppsdk.dylib, 0x0002): tried: 'libimcppsdk.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OSlibimcppsdk.dylib' (no such file), 'libimcppsdk.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))
    at rr (xxx.app/Contents/Resources/app.asar/index.js:1:455903)
    at async Object.load (xxx.app/Contents/Resources/app.asar/index.js:1:466442)
    at async wr.init (xxx.app/Contents/Resources/app.asar/index.js:1:468301)
    at async eo.initIMSDK (xxx.app/Contents/Resources/app.asar/index.js:1:496819)
    at async node:electron/js2c/browser_init:241:563

universal 打包

universal 打包会先打出 x64 和 arm64 的产物,然后使用 lipo 将两种产物打到一个安装包里。

lipo 在对我们用到的动态库进行合并操作时报错:

have the same architectures (x86_64) and can't be in the same fat output file

失败原因在于,我们打包时只将一个架构的动态库拷贝到了资源目录,没有分架构拷贝,导致 lipo 在最后的合并操作时,发现库1 和 库2 其实对应的是同一个文件,所以不能合并为一个新的文件。

electron-builder 提供了 afterPack hook,可以在打包之后、产生分发包、签名之前执行自定义操作。

1
2
3
4
5
6
7
8
9
// afterPack 函数可以拿到的 context
interface AfterPackContext {
  outDir: string
  appOutDir: string
  packager: PlatformPackager<any>
  electronPlatformName: string
  arch: Arch
  targets: Array<Target>
}

在 afterPack hook 中,根据 electron-builder 当前正在处理的架构,将对应的动态库拷贝到资源目录中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const builder = require('electron-builder');
const { Arch, Platform } = builder;

exports.default = async function (context) {
  const { appOutDir, arch, packager } = context
  const { platform } = packager
  if (platform.name == Platform.MAC.name) {
    const arm64 = arch == Arch.arm64
    const x64 = arch == Arch.x64
    if (!arm64 && !x64) {
      console.warn('Not arm64 and not x64')
      return
    }
    const dest = path.join(appOutDir, packager.appInfo.productFilename + '.app', 'Contents', 'Resources', 'lib', 'mac')
    const source = path.join(__dirname, '..', 'lib', 'mac', arm64 ? 'arm64' : 'x64')
    await copy(source, dest)
  }
};

包分发

采用分架构分发还是 universal 通用包分发

  • 分架构分发 x64 和 arm64 和安装包
    • 优点:安装包小,下载快一些
    • 缺点:需要用户自己选择对应架构下载
  • 通用包
    • 优点:用户下载时不用自己选择对应架构
    • 缺点:安装包文件较大

链接

欢迎体验新产品, 抖音聊天

Support the author with
alipay QR Code
wechat QR Code

Yang
WRITTEN BY
Yang
Developer