本文首发同名微信公众号:前端徐徐
大家好,我是徐徐。今天我们聊聊 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 应用的打包。如果你有任何问题或想法,欢迎在评论区分享!