Innosetup源码拆解:手写实现安装包核心逻辑
发布时间:2026/9/22 3:00:52 锦皓数字建站

Innosetup源码拆解:手写实现安装包核心逻辑
配置环境就卡半天,是不是你也遇到过?明明照着教程敲了半小时,结果ISCC编译器直接报错,或者打包出来的安装包路径全错。别急,今天咱们不聊那些虚的,直接扒开 Inno Setup 的源码底裤,看看它到底是怎么把一堆文件塞进一个 .exe 的。咱们不整那些花里胡哨的封装,直接手写实现一个极简版的安装引擎,让你彻底搞懂它的底层逻辑。
入口定位:从 Setup.scr 到 ISCC.exe
很多人以为 Inno Setup 是个黑盒,其实它就是个纯 Pascal 脚本解释器加上一个编译引擎。核心入口在 ISCC.exe(Inno Setup Script Compiler)。当你双击那个 .iss 文件时,实际上触发的是 ISCC.exe 加载你的脚本,解析 AST(抽象语法树),然后生成最终的 Setup.exe。
在 Inno Setup 的源码目录里,最关键的文件是 Compiler/Compiler.pas。这个文件定义了编译器的状态机。它不是直接执行代码,而是把你的 .iss 文件读成字符串,然后用正则和自定义解析器切分成 Token。
这里有个坑:Inno Setup 不支持标准的 Delphi 对象模型。它有自己的 TFileSet、TRegEntry 等类。这些类在 Common/SetupDef.pas 里定义。如果你看源码,会发现这些类非常轻量,几乎全是数据容器,没有复杂的业务逻辑。业务逻辑全在 Compiler/ 目录下。
核心片段:文件收集与哈希计算
咱们先看最核心的部分:文件收集。在 Compiler/Compiler.pas 中,有一个关键函数 ProcessFile。这个函数负责把你要打包的文件读进内存,计算哈希,并标记是否压缩。
// 源码片段 1: 核心文件处理逻辑 (简化版)
// 来源: Inno Setup Compiler Source, Unit: Compiler
// 注意: 这是核心逻辑的伪代码还原,实际源码有更复杂的异常处理function TCompiler.ProcessFile(FileName: string; var FileSet: TFileSet): Boolean;
varF: TFileStream;FileData: TBytes;MD5Hash: TMD5Digest;CompressedSize: Integer;
beginResult := False;// 1. 检查文件是否存在if not FileExists(FileName) thenbeginWriteLn('Error: File not found: ', FileName);Exit;end;// 2. 读取文件到内存F := TFileStream.Create(FileName, fmOpenRead or fmShareDenyNone);trySetLength(FileData, F.Size);F.ReadBuffer(FileData[0], F.Size);finallyF.Free;end;// 3. 计算 MD5 哈希 (Inno Setup 使用 MD5 进行完整性校验)// 这里调用的是内嵌的 MD5 实现,而非系统 API,确保跨平台一致性MD5Hash := MD5Compute(FileData, Length(FileData));// 4. 判断是否需要压缩// 策略: 如果文件大于 1KB 且不是已压缩格式 (如 .jpg, .mp4),则进行 LZMA 压缩if (F.Size 1024) and (not IsAlreadyCompressed(FileName)) thenbegin// 调用 LZMA 压缩库 (Inno Setup 默认使用 LZMA)// 这里简化为直接调用压缩函数CompressedSize := LZMACompress(FileData, Length(FileData));// 实际源码中,压缩后的数据会写入一个临时的 .bin 文件endelsebeginCompressedSize := Length(FileData);end;// 5. 将文件信息加入 FileSet 列表FileSet.Add(FileName, MD5Hash, CompressedSize, F.Size);Result := True;
end;逐行注释解读:第 10 行:FileExists 检查是第一步。很多新手在这里踩坑,因为脚本里的路径是相对路径,而当前工作目录可能不是你以为的那个地方。
第 14-20 行:TFileStream 的使用。注意 fmShareDenyNone 标志,这允许其他进程同时读取该文件。这是 Inno Setup 能在安装时复制正在使用的文件(虽然有风险)的基础。
第 24 行:MD5 计算。官方文档明确指出,Inno Setup 使用 MD5 来检测文件损坏。虽然 MD5 现在被认为不安全,但在安装包校验场景中,它依然足够且高效。
第 28-32 行:压缩策略。这是 Inno Setup 体积小的关键。它不是盲目压缩,而是智能判断。如果你打包的是视频文件,压缩反而变大,所以它跳过。
第 38 行:FileSet.Add。这是关键数据结构。TFileSet 是一个链表,每个节点存储了文件路径、哈希、压缩后大小和原始大小。这个链表最终会被序列化到安装包的头部。设计思想:数据驱动与状态机
Inno Setup 的设计哲学非常清晰:数据驱动 + 有限状态机。数据驱动:你的 .iss 脚本本质上是一个数据描述文件。它不告诉编译器“怎么做”,而是告诉编译器“做什么”。比如 [Files] 段告诉编译器“我要打包这些文件”,[Run] 段告诉编译器“装完后我要执行这些命令”。编译器只负责解析这些数据,然后生成通用的安装逻辑。
有限状态机:安装过程被分解为几个明确的状态:stPrepare:准备阶段,检查依赖,显示欢迎页。
stInstall:安装阶段,解压文件,写入注册表。
stPostInstall:安装后阶段,执行自定义脚本,显示完成页。
stUninstall:卸载阶段,删除文件,清理注册表。这种设计的好处是解耦。你不需要关心文件是怎么解压的,你只需要提供文件列表。Inno Setup 的核心引擎(SetupLdr.exe 和生成的 Setup.exe)负责处理所有的底层 IO、解压、注册表操作。
为什么这么设计?
因为安装程序需要极高的稳定性和兼容性。如果每个脚本都自己写文件复制逻辑,那兼容性噩梦就来了。Inno Setup 把所有底层操作封装在引擎里,脚本只负责描述意图。这样,即使你的脚本逻辑复杂,底层行为也是可预测的。
手写简化版:实现一个迷你安装引擎
光看源码不够,咱们手写实现一个极简版的安装引擎。这个引擎只支持:读取 JSON 配置(模拟 .iss)。
解压文件到目标目录。
写入注册表(模拟)。
执行后置命令。我们用 Go 语言写,因为 Go 的并发和 IO 处理很适合这种场景。
// 源码片段 2: 手写简化版安装引擎
// 语言: Go
// 文件: mini_installer.gopackage mainimport (encoding/jsonfmtospath/filepath
)// InstallConfig 模拟 Inno Setup 的 .iss 配置结构
type InstallConfig struct {Name string `json:name` // 应用名称Version string `json:version` // 版本号Files []File `json:files` // 文件列表Regs []Reg `json:regs` // 注册表项PostCmd []string `json:post_cmd`// 安装后命令
}// File 模拟 [Files] 段
type File struct {Source string `json:source` // 源文件路径DestDir string `json:dest_dir` // 目标目录Name string `json:name` // 目标文件名Compressed bool `json:compressed` // 是否压缩
}// Reg 模拟 [Registry] 段
type Reg struct {RootKey string `json:root_key` // HKEY_LOCAL_MACHINEKey string `json:key` // 子键Name string `json:name` // 值名Data string `json:data` // 值数据Type string `json:type` // REG_SZ
}// MiniInstaller 迷你安装引擎
type MiniInstaller struct {Config InstallConfig
}// NewMiniInstaller 创建新的安装引擎实例
func NewMiniInstaller(config InstallConfig) *MiniInstaller {return MiniInstaller{Config: config}
}// Run 执行安装过程
func (m *MiniInstaller) Run() error {// 1. 状态: Preparefmt.Println([STATE] Prepare: Checking dependencies...)// 这里可以检查依赖,比如 .NET Framework 版本// 简化版跳过// 2. 状态: Installfmt.Println([STATE] Install: Copying files...)if err := m.installFiles(); err != nil {return fmt.Errorf(install files failed: %w, err)}fmt.Println([STATE] Install: Writing registry...)if err := m.writeRegistry(); err != nil {return fmt.Errorf(write registry failed: %w, err)}// 3. 状态: PostInstallfmt.Println([STATE] PostInstall: Running post-install commands...)if err := m.runPostCommands(); err != nil {return fmt.Errorf(post install failed: %w, err)}fmt.Println([STATE] Done: Installation completed successfully.)return nil
}// installFiles 处理文件复制逻辑
func (m *MiniInstaller) installFiles() error {for _, f := range m.Config.Files {// 计算目标路径destPath := filepath.Join(f.DestDir, f.Name)// 确保目标目录存在if err := os.MkdirAll(f.DestDir, 0755); err != nil {return fmt.Errorf(create dir %s: %w, f.DestDir, err)}// 读取源文件data, err := os.ReadFile(f.Source)if err != nil {return fmt.Errorf(read source %s: %w, f.Source, err)}// 模拟解压逻辑// 实际 Inno Setup 中,这里会调用 LZMA 解压// 简化版:如果标记为压缩,则假装解压(实际数据未压缩)if f.Compressed {fmt.Printf( [SIMULATED] Decompressing %s\n, f.Source)// 实际应调用 lzma.Decompress(data)}// 写入目标文件if err := os.WriteFile(destPath, data, 0644); err != nil {return fmt.Errorf(write dest %s: %w, destPath, err)}fmt.Printf( [OK] %s - %s\n, f.Source, destPath)}return nil
}// writeRegistry 处理注册表写入 (模拟)
func (m *MiniInstaller) writeRegistry() error {// 注意: 在 Linux/Mac 上,注册表不存在// 这里模拟 Inno Setup 的行为:在 Windows 上写入 HKLM// 在跨平台环境中,我们将其转换为写入配置文件for _, r := range m.Config.Regs {// 模拟注册表路径// Inno Setup 使用 HKEY_LOCAL_MACHINE\SOFTWARE\...// 简化版:写入到 ~/.app_config.jsonconfigPath := filepath.Join(os.Getenv(HOME), .mini_installer_config.json)// 读取现有配置var config map[string]interface{}data, _ := os.ReadFile(configPath)if len(data) 0 {json.Unmarshal(data, config)} else {config = make(map[string]interface{})}// 模拟注册表层级// 这里简化为扁平化键值key := fmt.Sprintf(%s\\%s\\%s, r.RootKey, r.Key, r.Name)config[key] = r.Data// 写回配置out, _ := json.MarshalIndent(config, , )if err := os.WriteFile(configPath, out, 0644); err != nil {return err}fmt.Printf( [REG] %s = %s\n, key, r.Data)}return nil
}// runPostCommands 执行安装后命令
func (m *MiniInstaller) runPostCommands() error {for _, cmd := range m.Config.PostCmd {fmt.Printf( [CMD] Executing: %s\n, cmd)// 实际应使用 exec.Command 执行// 简化版:仅打印}return nil
}func main() {// 模拟 .iss 配置config := InstallConfig{Name: MyApp,Version: 1.0.0,Files: []File{{Source: app.exe, DestDir: C:\\Program Files\\MyApp, Name: app.exe, Compressed: true},{Source: readme.txt, DestDir: C:\\Program Files\\MyApp, Name: readme.txt, Compressed: false},},Regs: []Reg{{RootKey: HKEY_LOCAL_MACHINE, Key: SOFTWARE\\MyApp, Name: InstallPath, Data: C:\\Program Files\\MyApp, Type: REG_SZ},},PostCmd: []string{regsvr32 /s myplugin.dll},}installer := NewMiniInstaller(config)if err := installer.Run(); err != nil {fmt.Println(Install Error:, err)os.Exit(1)}
}逐行注释解读:结构体定义:InstallConfig 和 File 结构体直接对应 Inno Setup 的 [Files] 和 [Registry] 段。这是数据驱动设计的体现。
状态机:Run 方法中的 [STATE] 打印模拟了 Inno Setup 的状态转换。每个状态都是独立的函数,便于测试和扩展。
文件处理:installFiles 方法简化了 Inno Setup 的复杂逻辑。没有 LZMA 解压,没有哈希校验,但核心流程(读取-转换-写入)是一致的。
注册表模拟:writeRegistry 方法展示了跨平台兼容性的挑战。Inno Setup 在 Windows 上直接写注册表,而在其他平台上,你需要用配置文件或数据库替代。
后置命令:runPostCommands 模拟了 [Run] 段。这是安装程序最容易出错的地方,因为命令失败可能导致安装状态不一致。应用场景与避坑指南
这个手写实现虽然简单,但足以说明 Inno Setup 的核心思想。在实际项目中,你会遇到以下场景:大型应用打包:如果你的应用超过 100MB,Inno Setup 的默认 LZMA 压缩可能不够快。你可以切换到 LZMA2 或 Zstd(Inno Setup 6.x 支持)。在 setup.iss 中修改 Compression 和 SolidCompression 参数。
依赖检查:如果你的应用依赖 .NET Framework 4.8,而用户机器没有,Inno Setup 可以在 [Run] 段之前插入依赖检查脚本。但更好的做法是在 PrepareToInstall 函数中用 Pascal 脚本检查。
静默安装:企业环境常用静默安装。Inno Setup 支持 /SILENT 参数。但注意,静默模式下,[Run] 段的 nowait 参数很重要,否则安装程序会卡在等待命令执行。避坑指南:路径问题:永远不要硬编码路径。使用 {app}、{localappdata} 等变量。
权限问题:安装到 C:\Program Files 需要管理员权限。确保你的安装程序以管理员身份运行,或者使用 PrivilegesRequired=lowest 并安装到用户目录。
版本冲突:如果用户已经安装了旧版本,Inno Setup 默认会覆盖。但如果你需要升级,建议在 PrepareToInstall 中检测旧版本并卸载。Inno Setup 的源码设计非常精妙,它把复杂的安装逻辑封装在引擎里,脚本只负责描述。这种设计思想不仅适用于安装包,也适用于很多其他工具链。
这个知识点你面试被问过吗?比如让你设计一个跨平台的软件分发系统,你会怎么借鉴 Inno Setup 的数据驱动和状态机思想?留言说说你的想法。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。