本文首发微信公众号:前端徐徐。
大家好,我是徐徐。今天我们聊聊 electron-builder 中 macOS 如何打包的。
前言
我在后台收到留言说上一篇关于 electron-builder 解析的文章不够有深度,我就想了一下,要再细致一点就只能把每个平台的打包源码讲一下啰。 这促使我重新审视了这个话题,决定深入源码,为大家揭示 macOS 平台上 Electron 应用打包的全过程。 Electron 应用的 macOS 平台打包是一个涉及多个步骤的复杂过程,包括准备打包配置、执行打包、应用签名、notarization 等。
为什么 macOS 打包如此重要?
在我们开始技术探讨之前,先问问自己:为什么 macOS 平台的打包如此重要?随着 Apple 对安全性要求的不断提高,特别是从 macOS Catalina 开始,如果不正确处理打包和签名,你的应用可能根本无法在用户的 Mac 上运行。想象一下,你辛苦开发的应用被系统拒之门外的沮丧!这就是为什么我们需要深入了解 electron-builder 的工作原理。
准备阶段
在打包之前,需要确保 Electron 应用的 package.json
文件中包含了必要的字段,如 name
、version
、main
(入口脚本)等。此外,macOS 应用需要一个 build
配置,指定打包选项和签名信息。
配置文件
开发者可以在项目的 package.json
文件中定义 electron-builder
的配置,包括:
- 图标 (
icon
):macOS 应用的图标路径。 - 证书和签名 (
mac
):包含签名证书的路径和密码。 - 应用信息 (
appId
):用于唯一标识应用的字符串。 - 目标格式 (
target
):指定打包目标,如dmg
、zip
、pkg
等。
打包流程
electron-builder
通过以下步骤完成打包:
创建打包器实例
首先,创建一个 MacPackager
实例,它继承自 PlatformPackager
,用于处理 macOS 平台特有的打包逻辑。
export class MacPackager extends PlatformPackager<MacConfiguration> { constructor(info: Packager) { super(info, Platform.MAC) } // ... }
定义默认目标
定义 macOS 平台的默认打包目标,如 dmg
、zip
、pkg
等。
get defaultTarget(): Array<string> { return this.info.framework.macOsDefaultTargets }
准备应用信息
在打包之前,需要准备应用信息,包括应用的名称、版本、版权信息等,并将其规范化以满足代码签名的要求。
protected prepareAppInfo(appInfo: AppInfo): AppInfo { return new AppInfo(this.info, this.platformSpecificBuildOptions.bundleVersion, this.platformSpecificBuildOptions, true) }
创建打包目标
根据配置中指定的目标格式,创建相应的打包目标。
createTargets(targets: Array<string>, mapper: (name: string, factory: (outDir: string) => Target) => void): void { for (const name of targets) { // ... } }
执行打包
对应用进行实际的打包操作,包括复制文件、合并资源,处理不同架构(如 x64 和 arm64)的打包等逻辑。
protected async doPack( outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: MacConfiguration, targets: Array<Target> ): Promise<any> { switch (arch) { default: { return super.doPack(outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets) } case Arch.universal: { const outDirName = (arch: Arch) => `${appOutDir}-${Arch[arch]}-temp` const x64Arch = Arch.x64 const x64AppOutDir = outDirName(x64Arch) await super.doPack(outDir, x64AppOutDir, platformName, x64Arch, platformSpecificBuildOptions, targets, false, true) if (this.info.cancellationToken.cancelled) { return } const arm64Arch = Arch.arm64 const arm64AppOutPath = outDirName(arm64Arch) await super.doPack(outDir, arm64AppOutPath, platformName, arm64Arch, platformSpecificBuildOptions, targets, false, true) if (this.info.cancellationToken.cancelled) { return } const framework = this.info.framework log.info( { platform: platformName, arch: Arch[arch], [`${framework.name}`]: framework.version, appOutDir: log.filePath(appOutDir), }, `packaging` ) const appFile = `${this.appInfo.productFilename}.app` const { makeUniversalApp } = require("@electron/universal") await makeUniversalApp({ x64AppPath: path.join(x64AppOutDir, appFile), arm64AppPath: path.join(arm64AppOutPath, appFile), outAppPath: path.join(appOutDir, appFile), force: true, mergeASARs: platformSpecificBuildOptions.mergeASARs ?? true, singleArchFiles: platformSpecificBuildOptions.singleArchFiles, x64ArchFiles: platformSpecificBuildOptions.x64ArchFiles, }) await fs.rm(x64AppOutDir, { recursive: true, force: true }) await fs.rm(arm64AppOutPath, { recursive: true, force: true }) // Give users a final opportunity to perform things on the combined universal package before signing const packContext: AfterPackContext = { appOutDir, outDir, arch, targets, packager: this, electronPlatformName: platformName, } await this.info.afterPack(packContext) if (this.info.cancellationToken.cancelled) { return } await this.doSignAfterPack(outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets) break } } }
这个地方需要核心讲解一下,因为它完整的将打包资源变成的一个可执行的文件应用。
入参解析
outDir
: 最终打包产物的输出目录。appOutDir
: 应用输出目录,即应用的临时打包目录。platformName
: 目标平台名称,例如ElectronPlatformName.macOS
。arch
: 目标架构,例如Arch.x64
或Arch.arm64
。platformSpecificBuildOptions
: 平台特定的构建选项。targets
: 打包目标数组。
打包流程
- 架构判断:使用
switch
语句根据arch
参数来判断当前的架构类型。 - 默认情况:如果
arch
不是universal
,则调用父类的doPack
方法进行打包。 - 通用应用(Universal):如果
arch
是universal
,则需要创建一个支持多种架构(如 x64 和 arm64)的应用包。
通用应用打包步骤
- 定义临时目录:定义一个函数
outDirName
,用于生成基于架构的临时输出目录名称。 - 打包 x64 架构:
- 调用父类的
doPack
方法,为 x64 架构打包应用。 - 使用
x64AppOutDir
作为临时输出目录。
- 检查取消状态:如果构建过程被取消,则退出方法。
- 打包 arm64 架构:
- 调用父类的
doPack
方法,为 arm64 架构打包应用。 - 使用
arm64AppOutPath
作为临时输出目录。
- 记录日志:记录打包信息,包括平台、架构和应用输出目录。
- 合并应用:调用
makeUniversalApp
方法,合并 x64 和 arm64 架构的应用包为一个通用应用包,makeUniversalApp
这个方法源码在这里:https://github1s.com/electron/universal/blob/main/src/index.ts,主要是做一个通用应用包。 - 清理临时目录:删除临时构建目录。
- 执行用户自定义的打包后操作:如果提供了
afterPack
钩子,则执行它。 - 检查取消状态:再次检查构建过程是否被取消。
- 签名应用:调用
doSignAfterPack
方法对合并后的应用进行签名
关键点
- 合并 ASAR 文件:如果
mergeASARs
选项为true
,则合并架构相关的 ASAR 文件。 - 单架构文件:
singleArchFiles
选项允许指定仅包含在一个架构中的应用文件。 - x64 架构文件:
x64ArchFiles
选项允许指定 x64 架构特有的文件。
上面这个方法展示了如何为 macOS 平台打包一个 Electron 应用,特别是如何创建一个支持多架构的通用应用包。它涵盖了从打包、合并到签名的整个流程,并提供了对构建过程的细粒度控制。
签名流程
macOS 应用需要进行代码签名,以确保应用的安全性和在 macOS Catalina 及更高版本上的运行。
准备签名信息
使用开发者的证书和密钥,准备签名所需的信息。
readonly codeSigningInfo = new MemoLazy<CreateKeychainOptions | null, CodeSigningInfo>( // ... )
执行签名
调用签名函数,对应用进行签名,sign
方法包含了应用签名的逻辑,包括查找合适的身份(identity)、设置签名选项、调用签名函数等。
private async sign(appPath: string, outDir: string | null, masOptions: MasConfiguration | null, arch: Arch | null): Promise<boolean> { // ... }
signApp
方法用于签名应用的各个部分,包括主应用和可能的 ASAR 解包文件。
protected async signApp(packContext: AfterPackContext, isAsar: boolean): Promise<boolean> { // ... }
Notarization
从 macOS Catalina 开始,苹果要求所有应用在分发前必须通过苹果的 notarization (苹果的官方认证过程,俗称公证) 流程。详情可看这里:https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution
private async notarizeIfProvided(appPath: string, buildOptions: MacConfiguration) { // ... }
打包完成
完成打包和签名后,electron-builder
会生成指定格式的打包文件,如 dmg
、zip
或 pkg
,并根据配置进行后续的发布操作。
错误处理和日志
在整个打包和签名过程中,electron-builder
提供了详细的错误处理和日志记录,帮助开发者诊断和解决问题。
其他高级特性
electron-builder
还支持许多高级特性,如:
- 代码压缩:减少应用的大小。
- 自动更新:集成自动更新支持。
- 配置脚本:在打包前后执行自定义脚本。
总结
Electron 应用在 macOS 平台上的打包过程是一个多层面、复杂的任务,涉及诸多关键步骤,包括准备打包配置、执行打包、应用签名和 notarization 等。electron-builder 的出现大大简化了这一过程,它通过自动化这些繁琐的步骤,使开发者能够将精力更多地集中在应用本身的开发上,而无需深究打包的技术细节。
然而,在实际操作中,开发者仍可能遇到各种打包相关的错误和问题。在这种情况下,对打包原理的深入理解以及对相关源码的熟悉就显得尤为重要。这些知识不仅能帮助开发者更快速地定位问题,还能提供更清晰的解决思路。
我理解很多小伙伴可能更关心如何在实战中解决具体的打包问题。请大家放心,这正是我们接下来要深入探讨的内容。本系列文章的编排是经过精心规划的,我们会先奠定必要的理论基础,然后逐步过渡到实际应用的教程中。
事实上,Mac Electron 打包确实存在许多棘手的问题,比如以下几个方面:
1. 应用签名和验证流程
2. 特定 PKG 格式的制作
3. 应用的升级更新
4. 原生包的资源处理
这些都是极其细节且关键的问题,每一个都值得我们深入探讨。在接下来的文章中,我们将逐一解析这些问题,并提供实用的解决方案。
通过这个系列,我希望能够帮助大家不仅理解 Electron 应用打包的原理,更能够在实际项目中熟练应用这些知识,解决各种复杂的打包问题。