揭秘 Electron 的 Linux 打包过程:你知道背后发生了什么吗?

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

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

前言

electron-builder 是一个强大的工具,用于将 Electron 应用程序打包成可分发的格式。它支持多种平台,包括 Windows、macOS 和 Linux。在 Linux 平台上,electron-builder 支持多种打包格式,如 AppImage、Flatpak、Snap 等。本文将详细介绍 electron-builder 在 Linux 上的打包原理及各格式是如何打包的。

涉及的核心源码路径

  • linuxPackager.ts:Linux 平台 打包核心文件

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/linuxPackager.ts

  • AppImageTarget.ts:构建 AppImage 包

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/AppImageTarget.ts

  • FlatpakTarget.ts:构建 Flatpak 包

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/FlatpakTarget.ts

  • FpmTarget.ts:构建 deb, rpm, sh, freebsd, pacman, apk, p5p

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/FpmTarget.ts

  • snap.ts:构建 Snap 包

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/snap.ts

Linux 平台打包核心流程

初始化 LinuxPackager

export class LinuxPackager extends PlatformPackager<LinuxConfiguration> {
  readonly executableName: string

  constructor(info: Packager) {
    super(info, Platform.LINUX)

    const executableName = this.platformSpecificBuildOptions.executableName ?? info.config.executableName
    this.executableName = executableName == null ? this.appInfo.sanitizedName.toLowerCase() : sanitizeFileName(executableName)
  }
  // ...
}

首先,创建 LinuxPackager 类的实例。这个类继承自 PlatformPackager,专门用于处理 Linux 平台的打包。在构造函数中,它会设置可执行文件名称和其他 Linux 特定的配置。

确定打包目标

get defaultTarget(): Array<string> {
  return ["snap", "appimage"]
}

LinuxPackager 的 defaultTarget 属性定义了默认的打包目标,通常是 [“snap”, “appimage”]。但实际使用的目标可能会根据用户的配置而有所不同。

创建打包目标

createTargets(targets: Array<string>, mapper: (name: string, factory: (outDir: string) => Target) => void): void {
  let helper: LinuxTargetHelper | null
  const getHelper = () => {
    if (helper == null) {
      helper = new LinuxTargetHelper(this)
    }
    return helper
  }

  for (const name of targets) {
    if (name === DIR_TARGET) {
      continue
    }

    const targetClass: typeof AppImageTarget | typeof SnapTarget | typeof FlatpakTarget | typeof FpmTarget | null = (() => {
      switch (name) {
        case "appimage":
          return require("./targets/AppImageTarget").default
        case "snap":
          return require("./targets/snap").default
        case "flatpak":
          return require("./targets/FlatpakTarget").default
        case "deb":
        case "rpm":
        case "sh":
        case "freebsd":
        case "pacman":
        case "apk":
        case "p5p":
          return require("./targets/FpmTarget").default
        default:
          return null
      }
    })()

    mapper(name, outDir => {
      if (targetClass === null) {
        return createCommonTarget(name, outDir, this)
      }

      return new targetClass(name, this, getHelper(), outDir)
    })
  }
}

核心方法是 createTargets,它会遍历所有指定的目标格式,并为每个格式创建相应的 Target 实例:

  • AppImage: 使用 AppImageTarget
  • Snap: 使用 SnapTarget
  • Flatpak: 使用 FlatpakTarget
  • deb, rpm, sh, freebsd, pacman, apk, p5p: 使用 FpmTarget
  • 其他格式: 使用 createCommonTarget

执行打包过程

对于每个目标,大致流程如下:

a. 准备工作:

  • 创建输出目录
  • 复制应用程序文件
  • 生成必要的元数据文件(如 .desktop 文件)

b. 格式特定的打包步骤:

  • AppImage: 使用 appimage-builder 创建 AppImage 文件
  • Snap: 生成 snapcraft.yaml 并使用 snapcraft 构建 snap 包
  • Flatpak: 创建必要的 manifest 文件并使用 flatpak-builder 构建 Flatpak 包
  • Fpm (deb, rpm 等): 使用 fpm 工具构建相应的包格式

c. 后处理:

  • 签名(如果配置了的话)
  • 移动生成的文件到最终输出目录

架构适配

export function toAppImageOrSnapArch(arch: Arch): string {
  switch (arch) {
    case Arch.x64:
      return "x86_64"
    case Arch.ia32:
      return "i386"
    case Arch.armv7l:
      return "arm"
    case Arch.arm64:
      return "arm_aarch64"
    default:
      throw new Error(`Unsupported arch ${arch}`)
  }
}

使用 toAppImageOrSnapArch 函数将 Electron 的架构名称转换为 AppImage 或 Snap 使用的架构名称。

清理

完成所有目标的打包后,清理临时文件和目录。

这就是 electron-builder 在 Linux 平台上打包的核心流程。每种特定的打包格式(如 AppImage、Snap、Flatpak 等)都有其独特的实现细节,但它们都遵循这个总体流程,下面我们来具体看看各种格式打包的具体实现。

创建 AppImage 包

初始化

export default class AppImageTarget extends Target {
  readonly options: AppImageOptions = { ...this.packager.platformSpecificBuildOptions, ...(this.packager.config as any)[this.name] }
  private readonly desktopEntry: Lazy<string>

  constructor(
    ignored: string,
    private readonly packager: LinuxPackager,
    private readonly helper: LinuxTargetHelper,
    readonly outDir: string
  ) {
    super("appImage")

    this.desktopEntry = new Lazy<string>(() => {
      const args = this.options.executableArgs?.join(" ") || "--no-sandbox"
      return helper.computeDesktopEntry(this.options, `AppRun ${args} %U`, {
        "X-AppImage-Version": `${packager.appInfo.buildVersion}`,
      })
    })
  }
  // ...
}
  • AppImageTarget 类继承自 Target,专门用于处理 AppImage 格式的打包。
  • 构造函数接收 LinuxPackager 和 LinuxTargetHelper 实例,这些提供了打包过程中的必要工具和方法。
  • 初始化配置选项,合并平台特定选项和通用选项。
  • 使用 Lazy 延迟计算桌面入口文件内容,优化性能。

构建过程(build 方法)

async build(appOutDir: string, arch: Arch): Promise<any> {
  // 准备工作
  const artifactName = packager.expandArtifactNamePattern(options, "AppImage", arch)
  const artifactPath = path.join(this.outDir, artifactName)
  await packager.info.callArtifactBuildStarted({
    targetPresentableName: "AppImage",
    file: artifactPath,
    arch,
  })

  // 并行处理多个准备任务
  const c = await Promise.all([
    this.desktopEntry.value,
    this.helper.icons,
    getAppUpdatePublishConfiguration(packager, arch, false),
    getNotLocalizedLicenseFile(options.license, this.packager, ["txt", "html"]),
    createStageDir(this, packager, arch),
  ])

  // 处理发布配置
  const publishConfig = c[2]
  if (publishConfig != null) {
    await outputFile(path.join(packager.getResourcesDir(stageDir.dir), "app-update.yml"), serializeToYaml(publishConfig))
  }

  // 构建 AppImage
  const args = [
    "appimage",
    "--stage", stageDir.dir,
    "--arch", Arch[arch],
    "--output", artifactPath,
    "--app", appOutDir,
    "--configuration", JSON.stringify({
      productName: this.packager.appInfo.productName,
      productFilename: this.packager.appInfo.productFilename,
      desktopEntry: c[0],
      executableName: this.packager.executableName,
      icons: c[1],
      fileAssociations: this.packager.fileAssociations,
      ...options,
    }),
  ]

  // 执行构建
  await packager.info.callArtifactBuildCompleted({
    file: artifactPath,
    safeArtifactName: packager.computeSafeArtifactName(artifactName, "AppImage", arch, false),
    target: this,
    arch,
    packager,
    isWriteUpdateInfo: true,
    updateInfo: await executeAppBuilderAsJson(args),
  })
}

a. 准备工作

b. 并行处理多个准备任务

c. 处理发布配置

d. 构建 AppImage

e. 执行构建

  • 确定输出文件名和路径。
  • 调用 artifactBuildStarted 回调,通知构建开始。
  • 获取桌面入口文件内容
  • 获取应用图标
  • 获取应用更新发布配置
  • 获取许可证文件
  • 创建临时工作目录(stage dir)
  • 如果存在发布配置,生成 app-update.yml 文件并保存到资源目录。
  • 准备 appimage 命令的参数,包括:
    * 临时目录路径
    * 目标架构
    * 输出路径
    * 应用目录
    * JSON 格式的配置信息(包含产品名称、文件名、桌面入口、可执行文件名、图标、文件关联等)
  • 如果指定了最大压缩级别,添加 xz 压缩参数。
  • 使用 executeAppBuilderAsJson 执行 appimage 命令。
  • 构建完成后调用 artifactBuildCompleted 回调,传递构建结果和更新信息。

错误处理和日志

await packager.info.callArtifactBuildStarted({
  targetPresentableName: "AppImage",
  file: artifactPath,
  arch,
})

// ... 构建过程 ...

await packager.info.callArtifactBuildCompleted({
  file: artifactPath,
  safeArtifactName: packager.computeSafeArtifactName(artifactName, "AppImage", arch, false),
  target: this,
  arch,
  packager,
  isWriteUpdateInfo: true,
  updateInfo: await executeAppBuilderAsJson(args),
})
  • 使用 Promise 和 async/await 处理异步操作。
  • 通过回调函数(artifactBuildStarted 和 artifactBuildCompleted)通知构建状态,便于外部监控和日志记录。

特殊功能支持

const args = [
  // ...
  "--configuration", JSON.stringify({
    productName: this.packager.appInfo.productName,
    productFilename: this.packager.appInfo.productFilename,
    desktopEntry: c[0],
    executableName: this.packager.executableName,
    icons: c[1],
    fileAssociations: this.packager.fileAssociations,
    ...options,
  }),
]
  • 支持自定义图标
  • 处理文件关联
  • 集成许可证文件
  • 支持应用自动更新配置

配置灵活性

readonly options: AppImageOptions = { ...this.packager.platformSpecificBuildOptions, ...(this.packager.config as any)[this.name] }
  • 通过 options 对象支持多种自定义选项
  • 支持从 package.json 或构建配置中读取选项

性能优化

this.desktopEntry = new Lazy<string>(() => {
  // ...
})

const c = await Promise.all([
  // ...
])
  • 使用 Lazy 延迟计算桌面入口文件内容
  • 利用 Promise.all 并行处理多个准备任务

总的来说,AppImageTarget 类封装了创建 AppImage 的完整流程,从准备必要的文件和配置,到执行构建命令,再到处理输出结果。它提供了高度的灵活性和可定制性,同时也优化了性能和资源使用。

创建 Flatpak 包

初始化 FlatpakTarget

constructor(
  name: string,
  private readonly packager: LinuxPackager,
  private helper: LinuxTargetHelper,
  readonly outDir: string
) {
  super(name)
}

FlatpakTarget 类继承自 Target,构造函数接收 LinuxPackager 和 LinuxTargetHelper 实例,这些提供了打包过程中的必要工具和方法。

构建过程

async build(appOutDir: string, arch: Arch): Promise<any> {
  // 准备工作
  const artifactName = packager.expandArtifactNamePattern(options, "flatpak", arch, undefined, false)
  const artifactPath = path.join(this.outDir, artifactName)
  
  // 通知构建开始
  await packager.info.callArtifactBuildStarted({
    targetPresentableName: "flatpak",
    file: artifactPath,
    arch,
  })

  // 准备临时目录
  const stageDir = await this.prepareStageDir(arch)

  // 获取 Flatpak 构建选项并执行构建
  const { manifest, buildOptions } = this.getFlatpakBuilderOptions(appOutDir, stageDir.dir, artifactName, arch)
  await bundleFlatpak(manifest, buildOptions)

  // 清理临时目录
  await stageDir.cleanup()

  // 通知构建完成
  await packager.info.callArtifactBuildCompleted({
    file: artifactPath,
    safeArtifactName: packager.computeSafeArtifactName(artifactName, "flatpak", arch, false),
    target: this,
    arch,
    packager,
    isWriteUpdateInfo: false,
  })
}

这个方法展示了 Flatpak 打包的整个流程,包括准备工作、构建和清理。

准备临时目录

private async prepareStageDir(arch: Arch): Promise<StageDir> {
  const stageDir = await createStageDir(this, this.packager, arch)

  await Promise.all([
    this.createSandboxBinWrapper(stageDir),
    this.createDesktopFile(stageDir),
    this.copyLicenseFile(stageDir),
    this.copyIcons(stageDir)
  ])

  return stageDir
}

这个方法准备了 Flatpak 打包所需的临时目录,包括创建沙箱包装器、桌面文件、复制许可证和图标等。

获取 Flatpak 构建选项

private getFlatpakBuilderOptions(appOutDir: string, stageDir: string, artifactName: string, arch: Arch): { manifest: FlatpakManifest; buildOptions: FlatpakBundlerBuildOptions } {
  // ... 省略部分代码 ...

  const manifest: FlatpakManifest = {
    id: appIdentifier,
    command: "electron-wrapper",
    runtime: this.options.runtime || flatpakBuilderDefaults.runtime,
    // ... 其他配置项 ...
  }

  const buildOptions: FlatpakBundlerBuildOptions = {
    baseFlatpakref: `app/${manifest.base}/${flatpakArch}/${manifest.baseVersion}`,
    // ... 其他构建选项 ...
  }

  return { manifest, buildOptions }
}

这个方法生成了 Flatpak 构建所需的配置和选项。

特殊功能支持

  • 沙箱包装器 (createSandboxBinWrapper 方法)
  • 桌面文件生成 (createDesktopFile 方法)
  • 许可证文件复制 (copyLicenseFile 方法)
  • 图标复制 (copyIcons 方法)

这些方法处理了 Flatpak 特有的一些需求,如沙箱环境、桌面集成等。

辅助函数

  • getElectronWrapperScript: 生成 Electron 包装器脚本
  • filterFlatpakAppIdentifier: 过滤应用标识符,确保符合 Flatpak 规范

整个 FlatpakTarget 类封装了创建 Flatpak 包的完整流程。它利用了 @malept/flatpak-bundler 库来执行实际的构建过程,同时处理了 Flatpak 特有的需求,如沙箱环境、桌面集成等。整个过程包括初始化、准备临时目录、生成必要的文件和配置、执行构建、清理等步骤。这个实现展示了如何将 Electron 应用适配到 Flatpak 格式,并集成到 electron-builder 的整体构建流程中。

创建 Fpm 包

初始化 FpmTarget

constructor(
  name: string,
  private readonly packager: LinuxPackager,
  private readonly helper: LinuxTargetHelper,
  readonly outDir: string
) {
  super(name, false)
  this.scriptFiles = this.createScripts()
}

FpmTarget 类继承自 Target,构造函数接收 LinuxPackager 和 LinuxTargetHelper 实例,并初始化脚本文件。

创建脚本文件

private async createScripts(): Promise<Array<string>> {
  // ... 省略部分代码 ...
  return await Promise.all<string>([
    writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterInstall, "after-install.tpl"), templateOptions),
    writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterRemove, "after-remove.tpl"), templateOptions),
  ])
}

这个方法创建了安装后和卸载后的脚本文件。

构建过程

async build(appOutDir: string, arch: Arch): Promise<any> {
  // ... 准备工作 ...

  const args = [
    "--architecture", toLinuxArchString(arch, target),
    "--after-install", scripts[0],
    "--after-remove", scripts[1],
    // ... 其他参数 ...
  ]

  // ... 设置其他选项 ...

  await executeAppBuilder(["fpm", "--configuration", JSON.stringify(fpmConfiguration)], undefined, { env })

  // ... 处理构建结果 ...
}

build 方法是打包的核心,它准备了 fpm 命令所需的参数,然后执行打包过程。

计算 fpm 元信息选项

private async computeFpmMetaInfoOptions(): Promise<FpmOptions> {
  // ... 省略部分代码 ...
  return {
    name: options.packageName ?? this.packager.appInfo.linuxPackageName,
    maintainer: author!,
    url: projectUrl!,
    vendor: options.vendor || author!,
  }
}

这个方法计算了 fpm 所需的元信息,如包名、维护者、URL 等。

自动更新支持

const publishConfig = this.supportsAutoUpdate(target)
  ? await getAppUpdatePublishConfiguration(packager, arch, false)
  : null
if (publishConfig != null) {
  // ... 添加自动更新文件 ...
}

对于支持自动更新的目标格式(如 deb、rpm、pacman),会添加相应的更新配置文件。

特殊处理

  • 针对不同目标格式(deb、rpm)的特殊处理
  • 处理依赖项、推荐包等
  • 处理图标、桌面文件、MIME 类型文件等

环境变量设置

const env = {
  ...process.env,
  SZA_PATH: await getPath7za(),
  SZA_COMPRESSION_LEVEL: packager.compression === "store" ? "0" : "9",
}

设置了特定的环境变量,用于控制压缩等行为。

执行 fpm 命令

await executeAppBuilder(["fpm", "--configuration", JSON.stringify(fpmConfiguration)], undefined, { env })

通过 executeAppBuilder 执行实际的 fpm 命令来创建包。

处理构建结果

await packager.info.callArtifactBuildCompleted(info)

构建完成后,调用回调函数通知构建结果。
FpmTarget 类封装了使用 fpm 工具创建各种 Linux 包格式(如 deb、rpm 等)的完整流程。它处理了从准备脚本文件、设置构建参数、执行构建命令到处理构建结果的整个过程。这个实现展示了如何将 Electron 应用适配到各种 Linux 包格式,并集成到 electron-builder 的整体构建流程中。特别值得注意的是,它还包含了对自动更新的支持,以及针对不同目标格式的特殊处理。

创建 snap 包

初始化 SnapTarget 类

export default class SnapTarget extends Target {
  readonly options: SnapOptions = { ...this.packager.platformSpecificBuildOptions, ...(this.packager.config as any)[this.name] }

  public isUseTemplateApp = false

  constructor(
    name: string,
    private readonly packager: LinuxPackager,
    private readonly helper: LinuxTargetHelper,
    readonly outDir: string
  ) {
    super(name)
  }

这里定义了 SnapTarget 类,继承自 Target。构造函数接收打包器、帮助器和输出目录等参数。options 属性合并了平台特定的构建选项和配置。

创建 Snap 包的描述符

private async createDescriptor(arch: Arch): Promise<any> {
  // 版本检查
  if (!this.isElectronVersionGreaterOrEqualThan("4.0.0")) {
    // ...版本警告和错误处理
  }

  // 设置基本信息
  const appInfo = this.packager.appInfo
  const snapName = this.packager.executableName.toLowerCase()
  
  // 处理插件和槽位
  const plugs = normalizePlugConfiguration(this.options.plugs)
  const plugNames = this.replaceDefault(plugs == null ? null : Object.getOwnPropertyNames(plugs), defaultPlugs)
  const slots = normalizePlugConfiguration(this.options.slots)

  // 处理包配置
  const buildPackages = asArray(options.buildPackages)
  const defaultStagePackages = getDefaultStagePackages()
  const stagePackages = this.replaceDefault(options.stagePackages, defaultStagePackages)

  // 创建应用描述符
  const appDescriptor: any = {
    command: "command.sh",
    plugs: plugNames,
    adapter: "none",
  }

  // 加载和修改 snap 配置
  const snap: any = load(await readFile(path.join(getTemplatePath("snap"), "snapcraft.yaml"), "utf-8"))
  // ... 更多配置处理

  // 返回完整的 snap 描述符
  return snap
}

这个方法负责创建 Snap 包的描述符,包括版本检查、基本信息设置、插件和槽位处理、包配置等。

构建

async build(appOutDir: string, arch: Arch): Promise<any> {
  // 准备构建参数
  const artifactName = packager.expandArtifactNamePattern(this.options, "snap", arch, "${name}_${version}_${arch}.${ext}", false)
  const artifactPath = path.join(this.outDir, artifactName)

  // 创建描述符
  const snap = await this.createDescriptor(arch)

  // 准备构建目录和参数
  const stageDir = await createStageDirPath(this, packager, arch)
  const snapArch = toLinuxArchString(arch, "snap")
  const args = ["snap", "--app", appOutDir, "--stage", stageDir, "--arch", snapArch, "--output", artifactPath, "--executable", this.packager.executableName]

  // 处理图标
  // ... 图标处理逻辑

  // 写入桌面入口文件
  await this.helper.writeDesktopEntry(/* ... */)

  // 处理额外的应用参数
  // ... 参数处理逻辑

  // 写入 snap 配置文件
  await outputFile(path.join(snapMetaDir, this.isUseTemplateApp ? "snap.yaml" : "snapcraft.yaml"), serializeToYaml(snap))

  // 执行构建
  await executeAppBuilder(args)

  // 处理发布配置
  const publishConfig = findSnapPublishConfig(this.packager.config)

  // 通知构建完成
  await packager.info.callArtifactBuildCompleted(/* ... */)
}

build 方法orchestrates整个构建过程,包括准备构建参数、创建描述符、处理图标和桌面入口、执行实际构建,以及处理构建完成后的操作。

辅助方法和函数

private replaceDefault(inList: Array<string> | null | undefined, defaultList: Array<string>) {
  const result = _replaceDefault(inList, defaultList)
  if (result !== defaultList) {
    this.isUseTemplateApp = false
  }
  return result
}

private isElectronVersionGreaterOrEqualThan(version: string) {
  return semver.gte(this.packager.config.electronVersion || "7.0.0", version)
}

function normalizePlugConfiguration(raw: Array<string | PlugDescriptor> | PlugDescriptor | null | undefined): { [key: string]: { [name: string]: any } | null } | null {
  // ... 插件配置规范化逻辑
}

function findSnapPublishConfig(config?: Configuration): SnapStoreOptions | null {
  // ... 查找 Snap 发布配置的逻辑
}

这些方法和函数提供了各种辅助功能,如替换默认值、版本比较、配置规范化等。

SnapTarget 类提供了一个全面的解决方案来构建 Snap 包。它处理了从配置解析到实际构建的所有方面,包括版本兼容性检查、配置合并、描述符生成、文件生成等。代码结构清晰,通过方法分离实现了关注点分离,使得整个构建过程更易于管理和扩展。

结语

通过以上的分析,我们深入了解了 electron-builder 在 Linux 平台上的打包流程。无论是 AppImage、Flatpak 还是 FPM 格式,每种包的创建都有其独特的步骤和注意事项。在实际应用中,开发者可以根据需要选择适合的打包格式,并利用 electron-builder 提供的灵活配置选项来定制打包过程。

希望这篇文章能够帮助你更好地理解和使用 electron-builder 进行 Linux 应用的打包。如果你有任何问题或想法,欢迎在评论区分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值