20、CPack生成器详解(NSIS):Windows安装包制作、NSIS模板定制、创建桌面快捷方式与开始菜单项

说到Windows平台的安装包,NSIS(Nullsoft Scriptable Install System)绝对是个绕不开的名字。我最早接触它是在十年前,那时候还在用老版本的CMake,NSIS的集成远没有现在这么顺手。如今CPack对NSIS的支持已经相当成熟了,说白了,你只需要写几行CMake代码,就能生成一个像模像样的Windows安装程序。

这一章,我们就来聊聊怎么用CPack + NSIS,把我们的C++项目打包成一个专业的Windows安装包。包括桌面快捷方式、开始菜单项,还有模板定制这些实战中一定会遇到的需求。

CPack NSIS 生成器知识体系 CPack + NSIS 基础配置 CPACK_NSIS_* 变量体系 安装路径、公司信息、版本号 快捷方式 & 开始菜单 CPACK_CREATE_DESKTOP_LINKS CPACK_NSIS_CREATE_ICONS_EXTRA NSIS 模板定制 CPACK_NSIS_EXTRA_INSTALL_COMMANDS 自定义 .nsi 模板文件 目标:一键生成专业 Windows 安装包

20.1 NSIS生成器的基础配置

先说说怎么让CPack知道你要用NSIS。在CMakeLists.txt里,你只需要设置一个生成器名称:

set(CPACK_GENERATOR "NSIS")

嗯,就这么简单。但光有这一行还不够,你得告诉NSIS一些基本信息。我个人习惯把这些配置集中放在一个单独的 CPackConfig.cmake 文件里,这样主CMakeLists.txt不会太乱。

下面是一组最常用的NSIS相关变量:

变量名 说明 示例值
CPACK_NSIS_DISPLAY_NAME 安装程序中显示的应用名称 "MyApp 1.0"
CPACK_NSIS_INSTALL_ROOT 默认安装根目录(通常是 $PROGRAMFILES) "$PROGRAMFILES64"
CPACK_NSIS_MUI_ICON 安装程序的图标文件(.ico) "${CMAKE_SOURCE_DIR}/icon.ico"
CPACK_NSIS_MUI_UNIICON 卸载程序的图标文件 "${CMAKE_SOURCE_DIR}/uninstall.ico"
CPACK_NSIS_INSTALL_DIRECTORY 安装目录名称(在安装根目录下) "MyApp"
CPACK_NSIS_URL_INFO_ABOUT 应用主页URL(显示在控制面板中) "https://myapp.com"
CPACK_NSIS_CONTACT 开发者联系信息 "support@myapp.com"

举个例子,一个完整的配置大概长这样:

set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "MyApp")

set(CPACK_NSIS_DISPLAY_NAME "MyApp ${CPACK_PACKAGE_VERSION}")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/resources/install.ico")
set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/resources/uninstall.ico")
set(CPACK_NSIS_URL_INFO_ABOUT "https://mycompany.com/myapp")
set(CPACK_NSIS_CONTACT "support@mycompany.com")

include(CPack)
小提示:如果你用的是64位应用,记得把 CPACK_NSIS_INSTALL_ROOT 设为 $PROGRAMFILES64,否则默认会装到32位的Program Files目录下。我曾经因为这个被测试同事追着问了好几次,后来就长记性了。

20.2 创建桌面快捷方式与开始菜单项

这是打包过程中最常被问到的需求。用户装完软件,总得有个地方点开它吧?

CPack提供了一个非常方便的变量:CPACK_CREATE_DESKTOP_LINKS。你只需要列出那些需要创建桌面快捷方式的可执行文件名称:

set(CPACK_CREATE_DESKTOP_LINKS "myapp" "myapp_tool")

这里有个坑——myappmyapp_tool 必须是你在 install() 命令中安装过的目标名称,而且不能带路径。CPack会自动找到它们并生成对应的快捷方式。

但如果你想要更精细的控制,比如在开始菜单里创建文件夹、添加卸载快捷方式,那就得用 CPACK_NSIS_CREATE_ICONS_EXTRA 了。这个变量允许你直接写NSIS脚本片段:

set(CPACK_NSIS_CREATE_ICONS_EXTRA
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyApp.lnk' '$INSTDIR\\\\bin\\\\myapp.exe'"
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Uninstall.lnk' '$INSTDIR\\\\Uninstall.exe'"
)
注意:NSIS脚本中的反斜杠需要转义,所以路径里要写成 \\\\。另外,$SMPROGRAMS$STARTMENU_FOLDER 是NSIS内置变量,分别代表「所有用户的开始菜单目录」和「当前应用的开始菜单文件夹」。

我个人习惯的做法是:桌面只放主程序的快捷方式,开始菜单里放一个完整的文件夹,包含主程序、文档链接和卸载程序。这样用户找起来方便,卸载也直观。

20.3 NSIS模板定制

有时候,CPack提供的那些变量不够用。比如你想在安装过程中弹出一个自定义的许可协议页面,或者想在安装完成后自动启动程序。这时候就需要动NSIS模板了。

CPack在生成NSIS脚本时,会使用一个内置的模板文件。你可以通过 CPACK_NSIS_TEMPLATE 变量指定自己的模板:

set(CPACK_NSIS_TEMPLATE "${CMAKE_SOURCE_DIR}/cmake/MyNSISTemplate.nsi.in")

这个模板文件是一个 .nsi.in 文件,里面可以包含CMake变量替换。比如:

!define PRODUCT_NAME "@CPACK_NSIS_DISPLAY_NAME@"
!define PRODUCT_VERSION "@CPACK_PACKAGE_VERSION@"
!define INSTALL_DIR "@CPACK_NSIS_INSTALL_ROOT@\\@CPACK_NSIS_INSTALL_DIRECTORY@"

Section "MainSection" SEC01
  SetOutPath "$INSTDIR"
  File /r "${SOURCE_DIR}\\*.*"
SectionEnd

Section "StartMenu"
  CreateDirectory "$SMPROGRAMS\\${PRODUCT_NAME}"
  CreateShortCut "$SMPROGRAMS\\${PRODUCT_NAME}\\MyApp.lnk" "$INSTDIR\\bin\\myapp.exe"
  CreateShortCut "$SMPROGRAMS\\${PRODUCT_NAME}\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"
SectionEnd

如果你不想从头写模板,也可以用 CPACK_NSIS_EXTRA_INSTALL_COMMANDSCPACK_NSIS_EXTRA_UNINSTALL_COMMANDS 来追加自定义的NSIS命令。这两个变量会在安装和卸载脚本的末尾插入你指定的代码:

set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
  "ExecWait '$INSTDIR\\\\bin\\\\myapp.exe --first-run'"
)

上面这行代码会在安装完成后自动运行一次程序,并带上 --first-run 参数。我在做某个工具链打包时用过这个技巧,用来初始化用户配置文件,效果还不错。

核心思路:能用变量解决的,就别动模板。模板定制虽然灵活,但维护成本高。我一般只在需要修改安装流程结构时才用模板,比如添加自定义页面、修改安装类型选择等。

20.4 实战:一个完整的NSIS打包示例

说了这么多,我们来拼一个完整的例子。假设我有一个叫 SuperTool 的项目,包含一个主程序和一个命令行工具:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(SuperTool VERSION 2.0.0 LANGUAGES CXX)

add_executable(supertool main.cpp)
add_executable(supertool_cli cli.cpp)

install(TARGETS supertool supertool_cli
  RUNTIME DESTINATION bin
)

# CPack 配置
set(CPACK_GENERATOR "NSIS")
set(CPACK_PACKAGE_NAME "SuperTool")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "SuperCompany")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "SuperTool")

set(CPACK_NSIS_DISPLAY_NAME "SuperTool ${CPACK_PACKAGE_VERSION}")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/icon.ico")
set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/icon.ico")
set(CPACK_NSIS_URL_INFO_ABOUT "https://supercompany.com")
set(CPACK_NSIS_CONTACT "dev@supercompany.com")

# 桌面快捷方式:只放主程序
set(CPACK_CREATE_DESKTOP_LINKS "supertool")

# 开始菜单:主程序 + 命令行帮助 + 卸载
set(CPACK_NSIS_CREATE_ICONS_EXTRA
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\SuperTool.lnk' '$INSTDIR\\\\bin\\\\supertool.exe'"
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\CommandLineHelp.lnk' '$INSTDIR\\\\bin\\\\supertool_cli.exe --help'"
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Uninstall.lnk' '$INSTDIR\\\\Uninstall.exe'"
)

include(CPack)

然后执行:

mkdir build && cd build
cmake ..
cpack -G NSIS

你就会得到一个 SuperTool-2.0.0-win64.exe 安装包。双击运行,安装完成后桌面上会出现 SuperTool 的快捷方式,开始菜单里也有一个完整的 SuperTool 文件夹。

调试小技巧:如果生成的安装包行为不符合预期,可以在 build/_CPack_Packages/win64/NSIS/ 目录下找到 CPack 生成的 NSIS.nsi 脚本。直接看这个脚本,就能知道 CPack 到底生成了什么。我每次遇到奇怪的问题,第一件事就是去翻这个文件。

嗯,NSIS打包其实没那么神秘。你只要掌握了那几个核心变量,再配合一点NSIS脚本知识,就能做出很专业的Windows安装包。记住,CPack帮你处理了90%的脏活累活,剩下的10%——比如快捷方式位置、自定义页面——才是你需要花心思的地方。