本文首发微信公众号:前端徐徐。
大家好,我是徐徐。今天我们聊聊 electron-builder 中 windows 是如何打包的。
前言
electron-builder 中 windows 的打包其实也是很复杂的,因为光是使用这个工具去打包就会遇到很多问题,更别说去探究里面的源码了解其原理了。但是之前已经写了 electron-builder 中 macOS 的源码和原理解读,然后为了彻底了解所有平台的打包逻辑,我又开始阅读 windows 的构建源码,这里跟大家分享一下我的一些理解。
涉及的核心源码路径
- winPackager.ts:Windows 平台打包的核心文件
- AppxTarget.ts:用于创建 AppX/MSIX 包
- MsiTarget.ts:用于创建 MSI 安装包
- NsisTarget.ts:创建 NSIS 安装程序
Windows 平台打包的核心流程
我们可以通过 Windows 平台打包的核心文件来分析一下核心的流程。WinPackager
类是 electron-builder 中处理 Windows 平台打包的核心类。它继承自 PlatformPackager
,专门处理 Windows 相关的打包逻辑。
export class WinPackager extends PlatformPackager<WindowsConfiguration> { // ... }
主要有以下几个核心的功能流程:
目标创建
createTargets
方法根据用户配置创建不同的打包目标:
createTargets(targets: Array<string>,mapper: (name: string, factory: (outDir: string) => Target) => void): void { // ... for (const name of targets) { if (name === "nsis" || name === "portable") { mapper(name, outDir => new NsisTarget(this, outDir, name, getHelper())) } else if (name === "nsis-web") { mapper(name, outDir => new WebInstallerTarget(this, path.join(outDir, name), name, new AppPackageHelper(getCopyElevateHelper()))) } // ... 其他目标类型 } }
这里支持多种打包格式,如 NSIS、AppX、MSI 等,每种格式对应一个特定的 Target 类,这一部分是非常关键和核心的逻辑,在后面会对每种包的生成做一个详细的讲解。
图标处理
图标处理使用懒加载方式:
_iconPath = new Lazy(() => this.getOrConvertIcon("ico"))
这确保了只在需要时才进行图标转换,优化了性能。
代码签名
签名过程主要通过 sign
和 doSign
方法实现:
async sign(file: string): Promise<boolean> { const signOptions: WindowsSignOptions = { path: file, options: this.platformSpecificBuildOptions, } const didSignSuccessfully = await this.doSign(signOptions) // ... } private async doSign(options: WindowsSignOptions) { return retry( () => signWindows(options, this), 3, 500, 500, 0, // ... 错误处理逻辑 ) }
这里使用了重试机制,提高了签名的可靠性。
资源编辑
signAndEditResources
方法处理可执行文件的资源编辑:
async signAndEditResources(file: string, arch: Arch, outDir: string, internalName?: string | null, requestedExecutionLevel?: RequestedExecutionLevel | null) { // ... 准备参数 if (process.platform === "win32" || process.platform === "darwin") { await executeAppBuilder(["rcedit", "--args", JSON.stringify(args)]) } else if (this.info.framework.name === "electron") { // 使用 Wine 在非 Windows 平台上运行 rcedit await execWine(path.join(vendorPath, "rcedit-ia32.exe"), path.join(vendorPath, "rcedit-x64.exe"), args) } // ... 签名和缓存处理 }
这段代码展示了如何跨平台编辑 Windows 可执行文件的资源。
应用签名
signApp
方法处理整个应用的签名过程:
protected async signApp(packContext: AfterPackContext, isAsar: boolean): Promise<boolean> { // ... 签名主可执行文件 if (!isAsar) { return true } // 处理 asar 打包情况下的文件签名 const filesToSign = await Promise.all([filesPromise(["resources", "app.asar.unpacked"]), filesPromise(["swiftshader"])]) await BluebirdPromise.map(filesToSign.flat(1), file => this.sign(file), { concurrency: 4 }) // ... }
这里特别处理了 asar 打包的情况,确保所有需要的文件都被正确签名。
性能优化
代码中使用了几种性能优化技术:
- 懒加载: 如
_iconPath
和vm
的处理 - 缓存: 使用
BuildCacheManager
缓存构建结果 - 并发: 使用
BluebirdPromise.map
并行处理文件签名
创建 AppX/MSIX 包
AppX/MSIX 是 Windows 应用程序的现代打包格式,主要用于 Windows Store 分发和企业部署。AppxTarget 类负责将 Electron 应用打包成 AppX/MSIX 格式。以下是这个过程的主要步骤:
初始化和验证
constructor(private readonly packager: WinPackager, readonly outDir: string) { super("appx") if (process.platform !== "darwin" && (process.platform !== "win32" || isOldWin6())) { throw new Error("AppX is supported only on Windows 10 or Windows Server 2012 R2 (version number 6.3+)") } }
这段代码确保 AppX 打包只在支持的系统上进行。
构建过程 (build 方法):
async build(appOutDir: string, arch: Arch): Promise<any> { // ... (设置输出路径等) const stageDir = await createStageDir(this, packager, arch) // ... (准备映射文件) }
build 方法是整个打包过程的核心,它协调了所有必要的步骤。
资源处理
const assetInfo = await AppXTarget.computeUserAssets(vm, vendorPath, userAssetDir)
这部分处理应用程序的资源文件,包括图标和其他视觉元素。
生成 AppX 清单
await this.writeManifest(manifestFile, arch, await this.computePublisherName(), userAssets)
writeManifest 方法生成 AppxManifest.xml 文件,这是 AppX 包的核心配置文件,定义了应用的身份、能力和资源。
创建资源索引 (resources.pri)
if (isScaledAssetsProvided(userAssets)) { // ... (使用 makepri.exe 创建 resources.pri) }
如果提供了缩放资源,会使用 makepri.exe 工具创建资源索引文件。
打包 AppX
await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makeappx.exe")), makeAppXArgs)
使用 makeappx.exe 工具将所有文件打包成 .appx 或 .msix 文件。
签名
await packager.sign(artifactPath)
对生成的 AppX 包进行数字签名,这是分发和安装 AppX 包的必要步骤。
清理和完成
await stageDir.cleanup() await packager.info.callArtifactBuildCompleted({ // ... })
清理临时文件并通知打包过程完成。
关键点:
- AppX 清单 (AppxManifest.xml) 是整个包的核心,定义了应用的元数据、能力和资源。
- 资源处理非常重要,包括图标、启动画面等。
- makeappx.exe 工具用于实际创建 AppX 包。
- 数字签名是 AppX 分发的必要步骤。
- 整个过程支持自定义和扩展,如自定义扩展、文件关联等。
AppX/MSIX 包的优势:
- 安全性更高,所有内容都经过签名验证。
- 支持自动更新。
- 可以利用 Windows 现代特性,如实时磁贴。
- 更好的应用隔离和清理卸载。
创建 MSI 安装程序
MSI(Microsoft Installer)是 Windows 平台上常用的安装包格式,它提供了一种标准化的方式来安装、维护和删除软件。MSI 文件本质上是一个包含安装信息和文件的数据库。
在这个 MsiTarget
类中,MSI 生成的主要步骤如下:
准备阶段
async build(appOutDir: string, arch: Arch) { const stageDir = await createStageDir(this, packager, arch) // ... }
这里创建了一个临时目录来存放生成过程中的文件。
生成 WiX 项目文件
const projectFile = stageDir.getTempFile("project.wxs") await writeFile(projectFile, await this.writeManifest(appOutDir, wixArch, commonOptions))
WiX(Windows Installer XML)是用来创建 MSI 安装程序的工具集。这一步生成了 WiX 需要的 XML 格式的项目文件。
编译 WiX 项目
await vm.exec(vm.toVmFile(path.join(vendorPath, "candle.exe")), candleArgs, { cwd: stageDir.dir, })
使用 WiX 的 candle.exe
工具编译项目文件。
链接并生成 MSI
await this.light(objectFiles, vm, artifactPath, appOutDir, vendorPath, stageDir.dir)
使用 WiX 的 light.exe
工具链接编译后的对象文件,生成最终的 MSI 文件。
签名(可选)
await packager.sign(artifactPath)
对生成的 MSI 文件进行数字签名。
生成原理的核心在于 writeManifest
方法,它定义了 MSI 的结构和内容:
protected async writeManifest(appOutDir: string, wixArch: Arch, commonOptions: FinalCommonWindowsInstallerOptions) { const { files, dirs } = await this.computeFileDeclaration(appOutDir) // ... return (await this.projectTemplate.value)({ // ... 各种选项 dirs, files, }) }
这个方法生成了 WiX 项目文件,定义了安装程序的各个方面,包括:
- 文件和目录结构
- 快捷方式
- 文件关联
- 注册表项
- 安装和卸载逻辑
computeFileDeclaration
方法详细定义了文件和目录结构,包括如何处理主可执行文件、创建快捷方式等。
总结一下MSI 程序的优势,大概有如下几个:
- 标准化:使用统一的安装、更新和卸载流程。
- 回滚能力:安装失败时可以回滚到之前的状态。
- 特权分离:支持普通用户安装和管理员安装。
- 广泛支持:Windows 原生支持,兼容性好。
总的来说,这个类通过生成 WiX 项目文件,然后使用 WiX 工具集编译和链接,最终创建出一个完整的 MSI 安装程序。这个过程封装了许多复杂的细节,使得创建专业级的 Windows 安装程序变得相对简单,因为 MSI 文件的创作其实里面包含了非常多的东西,不是一篇两篇就能讲完的,更多可参考下面的文档。
原生参考文档:https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal
创建 NSIS 安装程序
NSIS(Nullsoft Scriptable Install System)是一个用于创建 Windows 安装程序的开源系统。它最初由 Nullsoft 公司开发,该公司以创建 Winamp 播放器而闻名。 在 electron-builder 项目中,NSIS 被用来为 Windows 平台生成安装程序。它允许开发者通过配置选项来自定义安装程序,而无需直接编写 NSIS 脚本,大大简化了创建专业安装程序的过程。
在 NsisTarget 中有一下几个核心的关键步骤:
应用程序打包
首先,NsisTarget 类会将应用程序文件打包成一个压缩文件:
async buildAppPackage(appOutDir: string, arch: Arch): Promise<PackageFileInfo> { const format = !isBuildDifferentialAware && options.useZip ? "zip" : "7z" const archiveFile = path.join(this.outDir, `${packager.appInfo.sanitizedName}-${packager.appInfo.version}-${Arch[arch]}.nsis.${format}`) await archive(format, archiveFile, appOutDir, archiveOptions) // ... }
这里将应用程序文件打包成 7z 或 zip 格式,这是后续 NSIS 脚本将要使用的主要内容。
生成 NSIS 脚本
接下来,生成 NSIS 脚本。这个过程主要通过 computeFinalScript
方法完成:
private async computeFinalScript(originalScript: string, isInstaller: boolean, archs: Map<Arch, string>): Promise<string> { const scriptGenerator = new NsisScriptGenerator() // ...生成各种脚本内容 return scriptGenerator.build() + originalScript }
这个方法生成了完整的 NSIS 脚本,包括文件关联、预压缩文件处理等。
配置 NSIS 定义和命令
通过 configureDefines
和 configureDefinesForAllTypeOfInstaller
方法,设置各种 NSIS 定义:
protected configureDefines(oneClick: boolean, defines: Defines): Promise<any> { // ...设置各种定义 } private configureDefinesForAllTypeOfInstaller(defines: Defines): void { // ...设置通用定义 }
这些定义控制了 NSIS 脚本的行为,如安装目录、快捷方式创建等。
执行 NSIS 编译
最后,使用 executeMakensis
方法调用 NSIS 编译器:
private async executeMakensis(defines: Defines, commands: Commands, script: string): Promise<void> { const args: Array<string> = [] // ...准备参数 await spawnAndWrite(command, args, script, { env: { ...process.env, NSISDIR: nsisPath }, cwd: nsisTemplatesDir, }) }
这个方法将生成的脚本、定义和命令传递给 NSIS 编译器 (makensis),编译器然后生成最终的安装程序。
特殊文件处理
对于预压缩的文件,有特殊处理:
async function generateForPreCompressed(preCompressedFileExtensions: Array<string>, dir: string, arch: Arch, scriptGenerator: NsisScriptGenerator): Promise<void> { // ...处理预压缩文件 }
这确保了某些已压缩的文件(如 .asar 文件)能够正确地包含在安装程序中。
多架构支持
源码支持为不同架构生成安装程序:
for (const archs of doBuildArchs) { await this.buildInstaller(archs) }
这允许生成适用于不同 CPU 架构的安装程序。
NSIS 打包这个过程高度可配置,允许定制安装程序的各个方面,如文件关联、快捷方式、多语言支持等。通过抽象和模块化,这个实现使得生成复杂的 Windows 安装程序变得相对简单和灵活。
结语
通过深入探讨 electron-builder 的 Windows 打包过程,我们看到了其背后的复杂性和精巧设计。从 winPackager.ts 的整体协调,到 AppxTarget.ts、MsiTarget.ts 和 NsisTarget.ts 等不同目标的具体实现,每一部分都扮演着重要角色。理解这些打包机制不仅有助于解决在使用 electron-builder 时可能遇到的问题,还能让我们在设计跨平台应用时做出更明智的决策,希望这篇文章可以帮助到你