Inno Setup 实战指南:从零构建专业 Windows 安装包
1. 项目概述:为什么我们需要Inno Setup?
如果你是一名Windows平台的软件开发者,或者是一名需要将脚本、工具打包分发给同事或用户的IT人员,那么你一定遇到过这个头疼的问题:如何把一个文件夹里的程序、库文件、配置文件,变成一个用户双击就能安装、卸载的“专业”安装包?你可能试过直接把文件压缩成ZIP包发出去,结果用户反馈说“找不到主程序在哪”、“不知道怎么运行”,或者更糟的是,用户把文件解压得到处都是,最后卸载时只能手动删除,留下一堆垃圾文件。这就是安装程序制作工具存在的意义,而Inno Setup,无疑是这个领域里最经典、最可靠的选择之一。
简单来说,Inno Setup是一个免费的、用于创建Windows安装程序的脚本驱动工具。它不像某些大型商业安装工具那样有华丽的图形界面,但它通过一个清晰、强大的脚本语言(.iss文件),让你能精确控制安装过程的每一个细节。从创建开始菜单快捷方式和桌面图标,到写入注册表、安装服务、执行自定义脚本,它几乎无所不能。更重要的是,它生成的安装程序非常小巧,运行高效,并且完全支持Windows的所有现代版本,包括最新的Windows 11。对于个人开发者、小团队或是企业内部工具分发,Inno Setup提供了一个在专业性、灵活性和成本(免费)之间近乎完美的平衡点。
2. 核心需求解析:你的安装包到底需要什么?
在动手写第一行脚本之前,我们得先想清楚,一个合格的安装程序应该具备哪些基本功能?这决定了我们使用Inno Setup时需要配置哪些核心模块。
2.1 基础文件部署与目录结构管理
这是安装程序最根本的任务。你需要告诉Inno Setup:源文件在哪里?应该安装到用户电脑的哪个目录?是固定的C:\Program Files\YourApp,还是让用户自定义?安装过程是否要创建子目录来分类存放可执行文件、库、文档和资源?Inno Setup的[Files]段和[Dirs]段就是为此而生。你可以精确指定每个文件的来源、目标路径,以及安装条件(例如,仅当操作系统是64位时才安装某个DLL)。
2.2 用户交互与配置界面
一个友好的安装向导能极大提升用户体验。Inno Setup提供了标准的向导页面序列:欢迎页面、许可协议、安装路径选择、选择组件、选择开始菜单文件夹、准备安装、安装进度、完成页面。你可以在这些页面中嵌入自己的图片、修改文字,甚至通过插件扩展出更复杂的界面(如数据库配置页)。关键在于,你需要决定哪些选项对用户开放。比如,是否允许用户自定义安装路径?是否提供“典型安装”、“最小安装”、“完全安装”等组件选择?
2.3 系统集成与环境配置
软件安装后,如何让用户方便地启动它?这就涉及到系统集成。Inno Setup可以帮你:
- 创建快捷方式:在开始菜单、桌面、快速启动栏创建指向主程序的快捷方式。
- 注册文件关联:让你的软件成为某种特定文件格式(如
.myapp)的默认打开程序。 - 写入注册表:存储软件的安装路径、版本号、用户设置等。这对于需要被其他程序调用的COM组件或服务尤其重要。
- 安装Windows服务:如果你的程序是一个后台服务,Inno Setup可以直接调用系统的SC命令来安装和配置它。
- 修改系统环境变量:例如,将软件的
bin目录添加到PATH中,方便用户在命令行中直接调用。
2.4 安装与卸载的完整生命周期管理
安装不是一锤子买卖。Inno Setup生成的安装程序同时也是一个卸载程序。你需要考虑:
- 安装前检查:检查目标系统是否安装了必要的运行环境(如.NET Framework, Visual C++ Redistributable)。如果缺少,可以自动下载并安装。
- 自定义操作:在安装前、安装中、安装后执行特定的程序或脚本。例如,在安装前停止并卸载旧版本的服务,在安装后自动启动主程序进行初始配置。
- 静默安装:对于企业批量部署或自动化脚本,支持无界面的静默安装(使用
/SILENT或/VERYSILENT参数)是必须的。 - 干净的卸载:卸载程序不仅要删除所有安装的文件和目录,还要清理之前创建的注册表项、快捷方式,并停止相关服务。Inno Setup会记录安装期间的所有操作,以便在卸载时进行精确回滚。
3. Inno Setup脚本结构与核心语法详解
Inno Setup的脚本文件以.iss为扩展名,其结构非常清晰,像一本说明书,从上到下定义了整个安装程序的行为。让我们深入拆解每个核心段(Section)的用法和细节。
3.1 全局设置段[Setup]
这是脚本的心脏,定义了安装程序的基本属性和行为。其中有许多关键参数需要仔细配置:
- AppId:这是你应用程序的唯一标识符,通常使用类似
{GUID}的格式。至关重要:它决定了已安装程序在“控制面板-程序和功能”列表中的标识。如果更新版本时AppId不变,Inno Setup会执行升级安装;如果改变,则会被视为一个全新的软件。建议在项目初期就确定一个固定的GUID并永不更改。 - AppName和AppVersion:显示给用户的软件名称和版本号。版本号常用于判断升级。
- DefaultDirName:默认的安装目录。通常使用
{pf}\My Company\My Program这样的常量,{pf}代表Program Files目录(32位或64位系统会自动选择)。 - OutputDir:编译后安装包(.exe)的输出目录。我习惯设置为
Output,与源码分离。 - OutputBaseFilename:输出安装包的文件名(不含扩展名)。
- Compression:压缩设置。
lzma2/ultra64是目前压缩比和速度平衡得最好的选择,能显著减小安装包体积。 - PrivilegesRequired:权限要求。如果你的软件需要向
Program Files或系统目录写文件,或需要安装驱动、服务,必须设置为admin(需要管理员权限)。对于仅安装在用户个人目录下的软件,可以设为lowest以提升用户体验(无需UAC弹窗)。
注意:
[Setup]段中还有一个容易忽略但很重要的参数是ArchitecturesAllowed和ArchitecturesInstallIn64BitMode。如果你的软件是纯32位的,在64位系统上也能运行,可以只允许x86。如果是64位软件,必须明确允许x64,并设置ArchitecturesInstallIn64BitMode=x64,以确保安装到正确的Program Files目录。
3.2 文件与目录段[Files]&[Dirs]
这两个段定义了要安装的具体内容。
[Files]段示例:
[Files] ; 将源文件复制到目标目录,并标记为共享文件(如果被其他程序使用) Source: “MyApp.exe”; DestDir: “{app}”; Flags: ignoreversion Source: “libs\*.dll”; DestDir: “{app}\libs”; Flags: ignoreversion recursesubdirs createallsubdirs ; 安装一个VC++运行库,如果系统没有则安装 Source: “vcredist_x64.exe”; DestDir: “{tmp}”; Flags: deleteafterinstall ; 安装一个配置文件模板,如果目标文件已存在则不覆盖 Source: “config.ini.template”; DestDir: “{app}”; DestName: “config.ini”; Flags: onlyifdoesntexist- Flags详解:
ignoreversion:除非源文件版本比目标文件新,否则不覆盖。这是最常用的标志,避免用旧文件意外覆盖用户修改过的新文件。recursesubdirs和createallsubdirs:配合*.dll这样的通配符,可以复制整个目录树。deleteafterinstall:安装完成后删除文件。常用于临时解压的运行时安装包。onlyifdoesntexist:仅当目标文件不存在时才复制。适用于用户可自定义的配置文件模板。sharedfile:标记为共享文件。当卸载时,如果其他程序也在使用该文件,则不会删除,直到所有引用都被移除。常用于公共DLL。
[Dirs]段通常用于创建空目录。[Files]段在复制文件时会自动创建所需目录,所以[Dirs]主要用于创建那些不包含任何安装文件,但程序运行需要的目录(如日志目录{app}\logs)。
3.3 快捷方式段[Icons]
这个段用于在开始菜单、桌面等位置创建快捷方式。
[Icons] Name: “{group}\My Application”; Filename: “{app}\MyApp.exe”; WorkingDir: “{app}” Name: “{group}\Uninstall”; Filename: “{uninstallexe}” Name: “{commondesktop}\My Application”; Filename: “{app}\MyApp.exe”; Tasks: desktopicon{group}:代表用户在安装时选择的开始菜单程序组路径。{commondesktop}:所有用户的公共桌面。使用{userdesktop}则只创建在当前用户的桌面。Tasks: desktopicon:将此快捷方式的创建与一个“任务”(Task)关联。在安装向导的“选择附加任务”页面,用户可以选择是否勾选“创建桌面图标”。
3.4 注册表段[Registry]与运行段[Run]
[Registry]用于在安装和卸载时操作Windows注册表。这是软件进行系统集成(如文件关联、协议注册)和保存配置的关键。
[Registry] ; 在卸载时删除整个注册表项 Root: HKLM; Subkey: “SOFTWARE\MyCompany\MyApp”; Flags: uninsdeletekey ; 写入安装路径 Root: HKLM; Subkey: “SOFTWARE\MyCompany\MyApp”; ValueType: string; ValueName: “InstallPath”; ValueData: “{app}”; Flags: uninsdeletevalue ; 注册文件关联 Root: HKLM; Subkey: “SOFTWARE\Classes\.myapp”; ValueType: string; ValueData: “MyApp.Document”; Flags: uninsdeletekey Root: HKLM; Subkey: “SOFTWARE\Classes\MyApp.Document\shell\open\command”; ValueType: string; ValueData: “””{app}\MyApp.exe”” “”%1″””[Run]段用于在安装成功后执行程序。常用于启动应用程序进行首次配置,或运行运行时安装包。
[Run] Filename: “{tmp}\vcredist_x64.exe”; Parameters: “/install /quiet /norestart”; StatusMsg: “正在安装 Microsoft VC++ 运行库…”; Check: VCRedistNeedsInstall Filename: “{app}\MyApp.exe”; Description: “启动 My Application”; Flags: nowait postinstall skipifsilentCheck参数可以指定一个Pascal脚本函数,用于判断是否需要执行该操作。例如,VCRedistNeedsInstall函数会检查系统是否已安装指定版本的VC++运行库。Flags: nowait postinstall skipifsilent:nowait表示不等待程序结束;postinstall使其出现在安装完成的“附加任务”复选框里;skipifsilent表示在静默安装时跳过此项(避免自动弹出程序界面)。
3.5 组件与任务段[Components]&[Tasks]
这两个段用于构建安装向导中的可选项目。
[Components]定义软件的可选组件。例如,主程序、示例文件、开发SDK。用户可以在“选择组件”页面勾选或取消。[Tasks]定义安装时的附加任务。例如,“创建桌面图标”、“创建快速启动栏图标”、“将安装目录添加到PATH环境变量”。
它们通过Components和Tasks参数与其他段(如[Files],[Icons],[Registry])关联,实现条件化安装。
4. 高级功能与Pascal脚本编程
Inno Setup真正的威力在于其内嵌的Pascal脚本引擎。通过编写[Code]段,你可以实现几乎任何复杂的安装逻辑。
4.1 事件函数:掌控安装生命周期
Inno Setup提供了一系列事件函数,你可以在其中插入自定义代码。
InitializeSetup: 在安装向导显示之前执行。这里可以检查前置条件(如操作系统版本、磁盘空间),如果条件不满足,可以调用Abort终止安装。InitializeWizard: 在安装向导初始化后、显示前执行。这里可以动态修改向导页面的外观,比如更换图片、添加自定义控件。CurStepChanged: 在安装步骤改变时触发。参数CurStep表示当前步骤,如ssInstall(开始安装)、ssPostInstall(安装完成)。这是执行自定义操作最常用的地方。procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssPostInstall then begin // 安装完成后,执行数据库初始化脚本 Exec(ExpandConstant(‘{app}\init_db.exe’), ”, ”, SW_SHOW, ewWaitUntilTerminated, ResultCode); end; end;DeinitializeSetup: 在整个安装过程结束时执行(无论成功或失败),适合做最后的清理工作。
4.2 自定义页面与用户输入
虽然Inno Setup自带标准页面,但有时你需要收集更复杂的信息(如数据库连接字符串、服务器地址)。这时可以创建自定义页面。
- 在
InitializeWizard中创建页面:使用CreateInputQueryPage,CreateInputOptionPage,CreateInputDirPage等函数创建页面对象。 - 添加控件:向页面对象添加编辑框、复选框、下拉列表等。
- 读取用户输入:在后续的事件函数(如
CurStepChanged(ssInstall))中,从页面控件的属性中读取用户输入的值,并用于配置你的软件。
这个过程稍显繁琐,但网上有大量成熟的示例代码可以参考。一个常见的技巧是,将用户输入的值保存到ExpandConstant(‘{param:KEY|DEFAULT}’)可以访问的临时位置,或者直接写入一个临时的配置文件,供安装后的[Run]段程序读取。
4.3 环境检测与条件安装
通过Pascal脚本函数,你可以实现精细的条件判断。
- 检查操作系统版本:
GetWindowsVersionEx函数返回详细版本信息。 - 检查.NET Framework版本:可以读取注册表
HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full下的Release值来判断。 - 检查是否已安装旧版本:通过
RegKeyExists或GetUninstallString函数查找旧的AppId。 - 检查磁盘空间:
GetSpaceOnDisk函数。
这些检查通常放在InitializeSetup或组件、文件的Check参数中,用于决定是否显示错误、跳过某些安装步骤,或自动选择安装组件。
5. 实战:从零构建一个专业的安装包
让我们以一个虚构的“数据转换工具”为例,它包含一个主程序(Converter.exe)、一个依赖的DLL库文件夹(libs\)、一份用户手册(manual.pdf),并且需要安装Visual C++ 2019 Redistributable。
5.1 环境准备与项目初始化
首先,从官网下载并安装Inno Setup。安装时务必勾选“安装预处理器”和“安装Inno Setup Pascal Script (ISPP)”,这对高级功能至关重要。
打开Inno Setup编译器,选择“文件”->“新建”,会生成一个包含大量注释的示例脚本。我建议先将其另存为一个干净的模板。我的项目结构通常如下:
MyConverterApp/ ├── Source/ # 源码目录(非安装包所需) ├── Build/ # 编译后的程序文件 │ ├── Converter.exe │ ├── libs/ │ │ ├── helper.dll │ │ └── converter_core.dll │ └── manual.pdf ├── Dependencies/ # 第三方依赖 │ └── vcredist_x64.exe ├── Resources/ # 安装程序资源(图片、许可文件) │ ├── setup.bmp │ └── license.txt └── InstallerScript.iss # Inno Setup脚本5.2 编写核心脚本
以下是一个精简但功能完整的脚本框架:
; 脚本由 Inno Setup 脚本向导生成! ; 请务必仔细阅读此脚本中的注释,以了解如何修改它! #define MyAppName “数据转换大师” #define MyAppVersion “1.2.0” #define MyAppPublisher “我的公司” #define MyAppURL “https://www.example.com/” #define MyAppExeName “Converter.exe” [Setup] AppId={{你的唯一GUID,可以用工具生成} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppPublisher}\{#MyAppName} DefaultGroupName={#MyAppName} ; 卸载程序也显示在“控制面板”中 UninstallDisplayIcon={app}\{#MyAppExeName} ; 输出目录和文件名 OutputDir=..\Output OutputBaseFilename=数据转换大师_Setup_v{#MyAppVersion} ; 安装包压缩设置 Compression=lzma2/ultra64 SolidCompression=yes ; 需要管理员权限,因为要安装到Program Files PrivilegesRequired=admin ; 支持64位系统 ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 ; 安装向导界面设置 WizardStyle=modern SetupIconFile=..\Resources\app.ico WizardImageFile=..\Resources\setup_banner.bmp [Languages] Name: “chinesesimplified”; MessagesFile: “compiler:Languages\ChineseSimplified.isl” [Tasks] ; 可选任务:创建桌面图标 Name: “desktopicon”; Description: “创建桌面图标 (&D)”; GroupDescription: “附加图标:” [Files] ; 主程序 Source: “..\Build\{#MyAppExeName}”; DestDir: “{app}”; Flags: ignoreversion ; 依赖库目录 Source: “..\Build\libs\*”; DestDir: “{app}\libs”; Flags: ignoreversion recursesubdirs createallsubdirs ; 用户手册 Source: “..\Build\manual.pdf”; DestDir: “{app}”; Flags: ignoreversion ; VC++运行库(放在临时目录,安装后删除) Source: “..\Dependencies\vcredist_x64.exe”; DestDir: “{tmp}”; Flags: deleteafterinstall [Icons] Name: “{group}\{#MyAppName}”; Filename: “{app}\{#MyAppExeName}” Name: “{group}\卸载 {#MyAppName}”; Filename: “{uninstallexe}” Name: “{commondesktop}\{#MyAppName}”; Filename: “{app}\{#MyAppExeName}”; Tasks: desktopicon [Run] ; 静默安装VC++运行库(如果需要) Filename: “{tmp}\vcredist_x64.exe”; Parameters: “/install /quiet /norestart”; StatusMsg: “正在安装系统组件…”; Check: VCRedistNeedsInstall ; 安装完成后,可选启动程序 Filename: “{app}\{#MyAppExeName}”; Description: “启动 {#MyAppName}”; Flags: nowait postinstall skipifsilent [Code] // 函数:检查是否需要安装VC++ 2019 Redistributable (x64) function VCRedistNeedsInstall: Boolean; var Version: String; begin // 检查注册表中VC++ 2019 (x64)的版本 if RegQueryStringValue(HKLM, ‘SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64’, ‘Version’, Version) then begin // 如果已安装版本 >= 14.28.29914,则不需要再安装 Result := (CompareStr(Version, ‘v14.28.29914.0’) < 0); end else begin // 未找到注册表项,需要安装 Result := True; end; end;5.3 编译、测试与调试
- 编译:在Inno Setup编译器(ISCC)中打开脚本,按F9或点击“编译”按钮。如果没有错误,会在
Output目录生成.exe安装包。 - 基础测试:
- 在干净的虚拟机或测试机上运行安装包。
- 测试所有安装路径(默认路径、自定义路径)。
- 测试所有可选组件和任务(勾选/不勾选桌面图标)。
- 完整走一遍安装流程,检查文件是否安装到正确位置,快捷方式是否创建。
- 从控制面板卸载,检查是否所有文件、注册表项、快捷方式都被干净移除。
- 静默安装测试:在命令行中运行
生成的安装包.exe /SILENT或/VERYSILENT。安装完成后,检查程序是否被正确安装。这是自动化部署的关键。 - 调试脚本:如果Pascal脚本逻辑复杂,可以使用
MsgBox函数弹出信息框来显示变量值,这是最直接的调试方式。也可以使用Log函数将信息写入安装日志(安装时加/LOG参数)。
6. 进阶技巧与避坑指南
在实际项目中,你会遇到各种边界情况。以下是我积累的一些经验:
6.1 处理旧版本升级与数据迁移
这是最容易出问题的地方。策略如下:
- 保持
AppId不变:这是实现升级检测的基础。 - 在
[InstallDelete]段删除废弃文件:新版本不再需要的旧文件,可以在这里指定删除。但要极其小心,避免误删用户数据。 - 用户数据迁移:永远不要把用户数据(配置文件、数据库)放在
{app}安装目录下。应该放在{localappdata}\YourApp或{userappdata}\YourApp。这样在卸载重装时,用户数据得以保留。安装程序可以在CurStepChanged(ssInstall)中检查旧版数据位置,并将其迁移到新位置。
6.2 解决“文件正在使用,无法覆盖”问题
在升级安装时,如果旧版本的程序正在运行,其主程序文件会被系统锁定,导致新文件无法覆盖。解决方案:
- 使用
RestartReplace标志:在[Files]段为可能被锁定的文件添加Flags: restartreplace。这会让Inno Setup在无法替换文件时,计划在系统重启后替换。 - 在安装前关闭旧程序:在
[Code]段的InitializeSetup或PrepareToInstall中,尝试结束旧进程。function InitializeSetup(): Boolean; begin Exec(‘taskkill.exe’, ‘/f /im OldApp.exe’, ”, SW_HIDE, ewWaitUntilTerminated, ResultCode); Result := True; end;注意:强制结束进程(
/f)可能引起用户数据丢失,需谨慎使用并给出提示。
6.3 为安装包添加数字签名
未经签名的安装包在Windows SmartScreen等安全机制下会被标记为“未知发布者”,影响用户信任度。你需要购买一个有效的代码签名证书(如DigiCert, Sectigo)。
- 在编译脚本后,使用
signtool.exe(Windows SDK的一部分)对生成的.exe进行签名。 - 可以编写一个批处理脚本,将编译和签名过程自动化:
iscc “MyInstaller.iss” signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /a “Output\MyInstaller.exe”
6.4 多语言支持
如果你的软件面向国际用户,安装程序也需要多语言。Inno Setup内置了多种语言文件(.isl)。
- 在
[Languages]段添加你需要的语言,例如Name: “english”; MessagesFile: “compiler:Default.isl”。 - 在脚本中所有显示给用户的字符串,都改用
{cm:…}常量,并在[Messages]段或额外的.isl文件中定义这些常量的多语言翻译。 - 安装程序会根据用户系统的语言设置自动选择界面语言。
7. 常见问题排查与解决方案实录
即使按照指南操作,也难免会遇到问题。这里记录了几个我踩过的“坑”及其解决方法。
问题1:安装程序在Windows 10/11上运行时,界面字体模糊或DPI缩放异常。
- 现象:在4K等高DPI屏幕上,安装向导的文字和控件显得很小或模糊。
- 原因:Inno Setup的默认界面在高DPI下支持不佳。
- 解决方案:在
[Setup]段添加WizardStyle=modern。这是Inno Setup 6引入的现代风格向导,对DPI缩放支持更好。同时,确保你使用的位图资源(WizardImageFile)有足够高的分辨率。
问题2:静默安装(/SILENT)时,如何自动接受许可协议?
- 现象:使用
/SILENT参数安装时,如果安装包有许可协议页面,安装会暂停,等待用户点击“同意”。 - 解决方案:使用
/SUPPRESSMSGBOXES和/NORESTART参数组合,并结合/LOADINF从应答文件加载设置。但更简单的方法是在脚本中处理:在[Code]段,检查WizardSilent函数,如果为True(静默模式),则在相关事件中自动“同意”。function ShouldSkipPage(PageID: Integer): Boolean; begin // 在静默安装时跳过许可协议页面 if (PageID = wpLicense) and WizardSilent() then Result := True else Result := False; end;
问题3:卸载程序无法完全删除所有文件,提示“文件正在使用”。
- 现象:卸载时,某些DLL或日志文件被锁定,导致删除失败。
- 原因:可能是你的主程序或相关服务在卸载时仍在运行。
- 解决方案:在
[UninstallRun]段,在删除文件之前,先执行命令停止相关进程或服务。
同时,确保你的应用程序在退出时释放所有文件句柄。[UninstallRun] ; 卸载前,先停止并删除可能创建的服务 Filename: “{sys}\sc.exe”; Parameters: “stop MyService”; Flags: runhidden Filename: “{sys}\sc.exe”; Parameters: “delete MyService”; Flags: runhidden
问题4:如何让安装包在安装完成后自动添加防火墙例外?
- 需求:如果你的软件是一个服务器程序,可能需要开放防火墙端口。
- 解决方案:在
[Run]段或[Code]段的CurStepChanged(ssPostInstall)中,使用netsh命令。[Run] Filename: “{sys}\netsh.exe”; Parameters: “advfirewall firewall add rule name=””MyApp Server”” dir=in action=allow program=””{app}\MyServer.exe”” enable=yes”; Flags: runhidden重要:这需要管理员权限。务必在
[Setup]段设置PrivilegesRequired=admin。
问题5:编译脚本时出现“找不到文件”错误。
- 现象:
Source指定的文件路径找不到。 - 排查:
- 检查路径是否正确。Inno Setup脚本中的相对路径是相对于脚本文件(.iss)本身的。
- 检查文件名和扩展名是否拼写正确,包括大小写(在区分大小写的文件系统上)。
- 如果使用预处理器常量(如
{#MyAppExeName}),确保它已正确定义。
- 技巧:我习惯使用完整的相对路径,如
Source: “..\..\Build\Release\app.exe”;,并在项目根目录下运行编译器,以保持路径基准一致。使用#define定义基础路径常量也是一个好习惯。