揭秘 electron-builder:macOS 应用打包背后到底发生了什么?

简介: 本文详细介绍了 Electron 应用在 macOS 平台上的打包流程,涵盖配置文件、打包步骤、签名及 notarization 等关键环节。通过剖析 `electron-builder` 的源码,展示了如何处理多架构应用、执行签名,并解决常见问题。适合希望深入了解 macOS 打包细节的开发者。

本文首发微信公众号:前端徐徐。

大家好,我是徐徐。今天我们聊聊 electron-builder 中 macOS 如何打包的。

前言

我在后台收到留言说上一篇关于 electron-builder 解析的文章不够有深度,我就想了一下,要再细致一点就只能把每个平台的打包源码讲一下啰。 这促使我重新审视了这个话题,决定深入源码,为大家揭示 macOS 平台上 Electron 应用打包的全过程。 Electron 应用的 macOS 平台打包是一个涉及多个步骤的复杂过程,包括准备打包配置、执行打包、应用签名、notarization 等。

为什么 macOS 打包如此重要?

在我们开始技术探讨之前,先问问自己:为什么 macOS 平台的打包如此重要?随着 Apple 对安全性要求的不断提高,特别是从 macOS Catalina 开始,如果不正确处理打包和签名,你的应用可能根本无法在用户的 Mac 上运行。想象一下,你辛苦开发的应用被系统拒之门外的沮丧!这就是为什么我们需要深入了解 electron-builder 的工作原理。  

准备阶段

在打包之前,需要确保 Electron 应用的 package.json 文件中包含了必要的字段,如 nameversionmain(入口脚本)等。此外,macOS 应用需要一个 build 配置,指定打包选项和签名信息。

配置文件

开发者可以在项目的 package.json 文件中定义 electron-builder 的配置,包括:

  • 图标 (icon):macOS 应用的图标路径。
  • 证书和签名 (mac):包含签名证书的路径和密码。
  • 应用信息 (appId):用于唯一标识应用的字符串。
  • 目标格式 (target):指定打包目标,如 dmgzippkg 等。

打包流程

electron-builder 通过以下步骤完成打包:

创建打包器实例

首先,创建一个 MacPackager 实例,它继承自 PlatformPackager,用于处理 macOS 平台特有的打包逻辑。

export class MacPackager extends PlatformPackager<MacConfiguration> {
  constructor(info: Packager) {
    super(info, Platform.MAC)
  }
  // ...
}

定义默认目标

定义 macOS 平台的默认打包目标,如 dmgzippkg 等。

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):如果 archuniversal,则需要创建一个支持多种架构(如 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 会生成指定格式的打包文件,如 dmgzippkg,并根据配置进行后续的发布操作。

错误处理和日志

在整个打包和签名过程中,electron-builder 提供了详细的错误处理和日志记录,帮助开发者诊断和解决问题。

其他高级特性

electron-builder 还支持许多高级特性,如:

  • 代码压缩:减少应用的大小。
  • 自动更新:集成自动更新支持。
  • 配置脚本:在打包前后执行自定义脚本。

总结

Electron 应用在 macOS 平台上的打包过程是一个多层面、复杂的任务,涉及诸多关键步骤,包括准备打包配置、执行打包、应用签名和 notarization 等。electron-builder 的出现大大简化了这一过程,它通过自动化这些繁琐的步骤,使开发者能够将精力更多地集中在应用本身的开发上,而无需深究打包的技术细节。

然而,在实际操作中,开发者仍可能遇到各种打包相关的错误和问题。在这种情况下,对打包原理的深入理解以及对相关源码的熟悉就显得尤为重要。这些知识不仅能帮助开发者更快速地定位问题,还能提供更清晰的解决思路。

我理解很多小伙伴可能更关心如何在实战中解决具体的打包问题。请大家放心,这正是我们接下来要深入探讨的内容。本系列文章的编排是经过精心规划的,我们会先奠定必要的理论基础,然后逐步过渡到实际应用的教程中。

事实上,Mac Electron 打包确实存在许多棘手的问题,比如以下几个方面:

1. 应用签名和验证流程

2. 特定 PKG 格式的制作

3. 应用的升级更新

4. 原生包的资源处理

这些都是极其细节且关键的问题,每一个都值得我们深入探讨。在接下来的文章中,我们将逐一解析这些问题,并提供实用的解决方案。

通过这个系列,我希望能够帮助大家不仅理解 Electron 应用打包的原理,更能够在实际项目中熟练应用这些知识,解决各种复杂的打包问题。

相关文章
|
4月前
|
资源调度 JavaScript 前端开发
IM跨平台技术学习(十一):环信基于Electron打包Web IM桌面端的技术实践
这次借着论证 Web IM端 SDK 是否可以在 Electron 生成的桌面端正常稳定使用,我决定把官方新推出的 webim-vue3-demo,打包到桌面端,并记录了这次验证的过程以及所遇到的问题和解决方法。
81 2
|
1天前
|
监控 前端开发 安全
谈谈我做 Electron 应用的这一两年
本文首发于微信公众号“前端徐徐”,作者徐徐分享了过去一两年间开发Electron桌面应用的经验与心得。文章详细介绍了从项目启动、技术选型到具体实施的过程,并探讨了桌面端开发面临的挑战及解决方案,如软件更新、任务队列设计、性能优化等。此外,还列举了一些特殊需求的实现方法,如静默安装、进程禁用等。作者认为,尽管桌面端开发有其独特性,但通过不断探索与实践,仍能显著提升用户体验和技术水平。
22 0
谈谈我做 Electron 应用的这一两年
|
2月前
|
容器 iOS开发 Linux
震惊!Uno Platform 响应式 UI 构建秘籍大公开!从布局容器到自适应设计,带你轻松打造跨平台完美界面
【8月更文挑战第31天】Uno Platform 是一款强大的跨平台应用开发框架,支持 Web、桌面(Windows、macOS、Linux)及移动(iOS、Android)等平台,仅需单一代码库。本文分享了四个构建响应式用户界面的最佳实践:利用布局容器(如 Grid)适配不同屏幕尺寸;采用自适应布局调整 UI;使用媒体查询定制样式;遵循响应式设计原则确保 UI 元素自适应调整。通过这些方法,开发者可以为用户提供一致且优秀的多设备体验。
63 0
|
2月前
|
前端开发 JavaScript API
强强联手打造桌面应用新标杆:Angular与Electron的完美融合——从环境搭建到通信机制,全面解析构建跨平台应用的最佳实践与技巧
【8月更文挑战第31天】随着Web技术的进步,开发者们越来越多地采用Web技术来构建桌面应用程序。通过结合使用开源框架Electron及前沿的前端框架Angular,开发者能充分利用JavaScript、HTML和CSS打造出高性能且易维护的跨平台桌面应用。本文将详细介绍如何搭建基于Angular与Electron的开发环境,包括创建Angular项目、安装Electron及相关依赖、配置Electron主进程以及实现Angular应用与Electron间的通信等关键步骤,并最终将应用打包成多平台可执行文件,为读者提供了一套完整的解决方案以快速入门并实践这一强大技术组合。
36 0
|
2月前
|
iOS开发 MacOS Python
Electron Mac 打包报 Error: Exit code: ENOENT. spawn /usr/bin/python ENOENT 解决方法
Electron Mac 打包报 Error: Exit code: ENOENT. spawn /usr/bin/python ENOENT 解决方法
|
3月前
|
JavaScript 网络安全 iOS开发
如何用 Electron 打包chatgpt-plus.top并生成mac客户端
如何用 Electron 打包chatgpt-plus.top并生成mac客户端
37 0
|
3月前
|
JavaScript 区块链
从零开始:如何用Electron将chatgpt-plus.top 打包成EXE文件
从零开始:如何用Electron将chatgpt-plus.top 打包成EXE文件
38 0
|
5月前
|
关系型数据库 MySQL iOS开发
macOS Catalina(10.15)如何访问iPhone(Ipad)的应用文档文件
macOS Catalina(10.15)如何访问iPhone(Ipad)的应用文档文件
142 0
|
5月前
|
Linux Windows
教你在Linux上安装Node并用Electron打包deb和rpm包
教你在Linux上安装Node并用Electron打包deb和rpm包
301 9
|
5月前
|
前端开发 数据可视化 iOS开发
基于electron快速将任意网站打包成跨平台的桌面端软件
基于electron快速将任意网站打包成跨平台的桌面端软件
151 1