本文首发微信公众号:前端徐徐。
大家好,我是徐徐。今天我们聊聊 electron-builder 中 Linux 是如何打包的。
前言
electron-builder 是一个强大的工具,用于将 Electron 应用程序打包成可分发的格式。它支持多种平台,包括 Windows、macOS 和 Linux。在 Linux 平台上,electron-builder 支持多种打包格式,如 AppImage、Flatpak、Snap 等。本文将详细介绍 electron-builder 在 Linux 上的打包原理及各格式是如何打包的。
涉及的核心源码路径
- linuxPackager.ts:Linux 平台 打包核心文件
- AppImageTarget.ts:构建 AppImage 包
- FlatpakTarget.ts:构建 Flatpak 包
- FpmTarget.ts:构建 deb, rpm, sh, freebsd, pacman, apk, p5p
- snap.ts:构建 Snap 包
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 应用的打包。如果你有任何问题或想法,欢迎在评论区分享!