26、CPack中的安装脚本:预安装脚本、后安装脚本、自定义安装逻辑

说实话,CPack 的安装脚本这块,我当年刚接触时也绕了不少弯路。你想想看,一个项目打包出来,有时候需要在安装前检查环境,有时候需要在安装后清理临时文件,甚至还要根据用户的选择执行不同的安装逻辑。这些需求,光靠 CPack 默认的安装规则是搞不定的。

嗯,今天我们就来聊聊 CPack 里的安装脚本。说白了,就是让你在打包的「前、中、后」三个时间点,插入自定义的脚本逻辑。

26.1 为什么需要安装脚本?

我在项目中遇到过这样一个场景:我们做了一个跨平台的 SDK,Windows 上安装时需要注册 COM 组件,Linux 上需要创建符号链接,macOS 上还要写 plist 文件。如果只用 install() 命令,根本搞不定这些平台相关的操作。

这时候,安装脚本就派上用场了。它允许你在 CPack 生成的安装包(比如 NSIS、DEB、RPM)中嵌入自定义的 shell 脚本或批处理命令。

核心概念:CPack 的安装脚本分为三类:

  • 预安装脚本(Pre-install):在文件复制之前执行
  • 后安装脚本(Post-install):在文件复制之后执行
  • 自定义安装逻辑:完全替代默认的安装流程

26.2 预安装脚本:先检查,再动手

预安装脚本,说白了就是在安装正式开始前跑一段逻辑。最常见的用途是检查依赖环境。

我记得有一次给客户打包一个工业控制软件,要求安装前必须检测系统上有没有安装特定版本的 OpenGL 驱动。如果没装,直接弹窗提示,而不是等安装到一半才报错。

来看一个 CMake 中的配置示例:

# CMakeLists.txt
set(CPACK_PRE_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pre_install.cmake")

# 或者针对特定生成器
set(CPACK_NSIS_PRE_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/nsis/pre_install.nsh")
set(CPACK_DEB_PRE_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/deb/pre_install.sh")

对于 NSIS 安装包,预安装脚本可以这样写:

; nsis/pre_install.nsh
!macro PreInstall
  ; 检查管理员权限
  UserInfo::GetAccountType
  Pop $0
  StrCmp $0 "Admin" +3
    MessageBox MB_OK "请以管理员身份运行安装程序"
    Abort
!macroend

对于 DEB 包,预安装脚本是一个标准的 shell 脚本:

#!/bin/bash
# deb/pre_install.sh
# 检查系统版本
if [ "$(lsb_release -rs)" != "20.04" ]; then
    echo "警告:此软件仅在 Ubuntu 20.04 上测试过"
    # 这里可以继续安装,也可以退出
fi

我的经验:预安装脚本里尽量只做检查,不要做修改。比如检查磁盘空间、检查依赖库是否存在。一旦在预安装阶段修改了系统文件,万一后续安装失败,回滚会很麻烦。

26.3 后安装脚本:收尾工作

后安装脚本在文件复制完成后执行。我常用它来做三件事:注册服务、创建快捷方式、写入配置文件。

举个例子,我们有个后台服务程序,安装后需要注册为系统服务:

# CMakeLists.txt
set(CPACK_POST_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/post_install.cmake")

# 针对 RPM 包
set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/rpm/post_install.sh")

RPM 的后安装脚本:

#!/bin/bash
# rpm/post_install.sh
# 注册 systemd 服务
systemctl daemon-reload
systemctl enable my-service
systemctl start my-service

NSIS 的后安装脚本:

; nsis/post_install.nsh
!macro PostInstall
  ; 创建桌面快捷方式
  CreateShortCut "$DESKTOP\MyApp.lnk" "$INSTDIR\bin\myapp.exe"
  
  ; 写入注册表
  WriteRegStr HKLM "Software\MyCompany\MyApp" "InstallDir" "$INSTDIR"
!macroend

注意:后安装脚本执行时,所有文件已经复制到目标目录。但此时安装程序可能还没有完成最终的注册表写入。如果你需要依赖安装程序的完整状态,建议使用自定义安装逻辑。

26.4 自定义安装逻辑:完全掌控

有时候,预安装和后安装还不够。比如你要做一个交互式的安装向导,让用户选择安装哪些组件。这时候就需要自定义安装逻辑。

CPack 允许你完全替换默认的安装流程。以 NSIS 为例,你可以编写一个完整的 NSIS 脚本:

# CMakeLists.txt
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\$STARTMENU_FOLDER\\MyApp.lnk' '$INSTDIR\\bin\\myapp.exe'")
set(CPACK_NSIS_DELETE_ICONS_EXTRA "Delete '$SMPROGRAMS\\$STARTMENU_FOLDER\\MyApp.lnk'")

# 使用自定义安装逻辑
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "!include custom_install.nsh")

custom_install.nsh 中,你可以写任何 NSIS 支持的逻辑:

; custom_install.nsh
Section "核心组件" SEC_CORE
  SectionIn RO  ; 只读,必须安装
  SetOutPath "$INSTDIR\core"
  File "bin\core.dll"
  File "bin\core.exe"
SectionEnd

Section "可选插件" SEC_PLUGIN
  SetOutPath "$INSTDIR\plugins"
  File "bin\plugin_a.dll"
  File "bin\plugin_b.dll"
SectionEnd

; 根据用户选择执行不同逻辑
Function .onSelChange
  ${If} ${SectionIsSelected} ${SEC_PLUGIN}
    MessageBox MB_OK "您选择了安装插件,需要额外 50MB 空间"
  ${EndIf}
FunctionEnd

关键点:自定义安装逻辑意味着你要自己处理所有安装细节。CPack 只负责生成安装包框架,具体的文件复制、注册表写入、服务注册等,全都要你手动写脚本。

26.5 跨平台脚本管理技巧

我个人的习惯是,把不同平台的安装脚本放在独立的目录下,用 CMake 的条件判断来加载:

# CMakeLists.txt
if(WIN32)
    set(CPACK_GENERATOR "NSIS")
    set(CPACK_NSIS_PRE_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/windows/pre_install.nsh")
    set(CPACK_NSIS_POST_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/windows/post_install.nsh")
elseif(APPLE)
    set(CPACK_GENERATOR "DragNDrop")
    # macOS 的安装脚本通常用 pkg 的 preinstall/postinstall
    set(CPACK_PRE_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/macos/pre_install.sh")
    set(CPACK_POST_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/macos/post_install.sh")
elseif(UNIX)
    set(CPACK_GENERATOR "DEB")
    set(CPACK_DEB_PRE_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/linux/pre_install.sh")
    set(CPACK_DEB_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/install_scripts/linux/post_install.sh")
endif()

这样做的好处是,每个平台的脚本独立维护,互不干扰。我曾经接手过一个项目,所有平台的安装逻辑全写在一个 CMake 文件里,条件判断嵌套了五六层,改一个地方要测试三个平台,那叫一个痛苦。

26.6 常见坑与避坑指南

我曾经在给客户做 CI/CD 流水线时,遇到过这样一个问题:DEB 包的 postinst 脚本里执行了 systemctl restart,结果在 Docker 容器里构建时直接报错,因为容器里没有 systemd。

解决方案很简单:在脚本里加个判断:

#!/bin/bash
# post_install.sh
if command -v systemctl &> /dev/null; then
    systemctl daemon-reload
    systemctl enable my-service
    systemctl start my-service
else
    echo "警告:未检测到 systemd,请手动启动服务"
fi

另一个常见坑是路径问题。安装脚本执行时的工作目录不一定是安装目录。我建议在脚本开头显式切换到安装目录:

cd "$INSTDIR" || exit 1

小技巧:调试安装脚本时,可以先在本地手动执行一遍。比如 DEB 的 postinst 脚本,你可以直接 bash post_install.sh 看看会不会报错。NSIS 脚本可以用 makensis-V4 参数输出详细日志。

26.7 知识体系图

下面这张图总结了 CPack 安装脚本的核心逻辑:

CPack 安装脚本体系 CPack 打包 预安装脚本 (Pre-install) 文件复制阶段 后安装脚本 (Post-install) 安装完成 自定义安装逻辑 检查依赖 验证环境 注册服务 创建快捷方式 完全替代默认流程 交互式安装

26.8 总结

安装脚本是 CPack 打包中非常灵活的一环。预安装脚本帮你提前发现问题,后安装脚本帮你做好收尾,自定义安装逻辑让你能应对各种奇葩需求。

我个人建议,刚开始接触时先从预安装和后安装入手,把简单的检查和服务注册做好。等熟悉了各个平台的脚本语法,再尝试自定义安装逻辑。毕竟,写一个完整的 NSIS 脚本可比写几行 CMake 配置要复杂得多。

嗯,关于安装脚本就聊到这里。记住一点:脚本越简单越好,能检查就别修改,能后安装就别预安装。这样出了问题,排查起来也容易。


公众号:蓝海资料掘金营,微信deep3321