2、CMake构建系统:CMake基础语法、Qt项目的CMake配置、多模块构建、跨平台编译配置

说到Qt项目的构建,我见过太多人还在用qmake。不是说qmake不好,而是CMake已经成为Qt官方推荐的构建系统。从Qt6开始,官方甚至不再默认安装qmake。我个人从Qt5.12开始全面转向CMake,说实话,真香。

这一章,我们就把CMake在Qt项目中的玩法彻底讲透。从基础语法到多模块构建,再到跨平台编译,一条龙搞定。

2.1 CMake基础语法速览

CMake其实没那么玄乎。它本质上就是个脚本语言,用来描述「怎么编译你的代码」。我刚开始接触时也觉得语法奇怪,但用多了就发现,核心就那么几个命令。

2.1.1 项目声明与版本管理

cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

这里有个坑:cmake_minimum_required 的版本号别乱写。我曾经接手过一个项目,写的是2.8,结果新机器上CMake 3.20一堆新特性用不了。建议至少写3.16,因为这是Qt6的最低要求。

2.1.2 变量与缓存

set(APP_NAME "MyApp")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 缓存变量,用户可以通过 -D 修改
set(BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries")

嗯,这里要注意:set 默认是普通变量,加上 CACHE 才会变成缓存变量。我刚开始经常搞混,导致cmake配置改了不生效,排查了半天。

2.1.3 查找包与链接库

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
target_link_libraries(MyApp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets)

为什么会用 PRIVATE?说白了就是控制依赖传递。如果你的库内部用了Qt,但对外暴露的头文件没有Qt类型,就用 PRIVATE。反之用 PUBLIC。

2.2 Qt项目的CMake配置实战

光讲语法太枯燥,我们直接上一个真实项目的CMakeLists.txt。这是我去年做的一个跨平台桌面应用,麻雀虽小五脏俱全。

2.2.1 基础Qt项目配置

cmake_minimum_required(VERSION 3.16)
project(DataViewer VERSION 2.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)    # 自动处理 MOC
set(CMAKE_AUTORCC ON)    # 自动处理资源文件
set(CMAKE_AUTOUIC ON)    # 自动处理 UI 文件

find_package(Qt6 REQUIRED COMPONENTS
    Core Gui Widgets
    Sql Charts          # 数据库和图表
)

qt_add_executable(DataViewer
    main.cpp
    mainwindow.ui       # UI文件
    resources.qrc        # 资源文件
)

target_link_libraries(DataViewer PRIVATE
    Qt6::Core Qt6::Gui Qt6::Widgets
    Qt6::Sql Qt6::Charts
)

看到 AUTOMOC、AUTORCC、AUTOUIC 这三个开关了吗?我建议你永远把它们设为ON。否则你得手动调用 qt_wrap_cpp 之类的命令,麻烦得很。

我的习惯: 把这三个开关放在项目根目录的CMakeLists.txt里,所有子模块自动继承。省心。

2.2.2 处理Qt资源与翻译

# 资源文件已经在 qt_add_executable 中处理了
# 但翻译文件需要单独处理

find_package(Qt6 REQUIRED COMPONENTS LinguistTools)

qt_add_translations(DataViewer
    TS_FILES
        translations/zh_CN.ts
        translations/en_US.ts
)

翻译这块我踩过坑。以前我手动调用 lupdate 和 lrelease,后来发现 qt_add_translations 一步到位。它会自动扫描源码中的 tr() 调用,生成并编译翻译文件。

2.3 多模块构建:大型项目的骨架

项目大了怎么办?全塞一个CMakeLists.txt里?别闹。我见过一个项目,单文件3000行CMake,改个依赖要翻半天。正确的做法是分模块。

2.3.1 目录结构设计

MyProject/
├── CMakeLists.txt          # 根目录
├── cmake/                  # 自定义CMake模块
│   └── FindMyLib.cmake
├── src/
│   ├── core/               # 核心库
│   │   ├── CMakeLists.txt
│   │   └── ...
│   ├── ui/                 # UI模块
│   │   ├── CMakeLists.txt
│   │   └── ...
│   └── app/                # 主程序
│       ├── CMakeLists.txt
│       └── main.cpp
└── tests/
    └── CMakeLists.txt

2.3.2 根目录CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0)

# 全局设置
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

# 添加子目录
add_subdirectory(src/core)
add_subdirectory(src/ui)
add_subdirectory(src/app)

# 可选:测试
option(BUILD_TESTS "Build tests" OFF)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

2.3.3 子模块CMakeLists.txt

以core模块为例:

# src/core/CMakeLists.txt
add_library(core STATIC
    database.cpp
    network.cpp
    models.cpp
)

target_include_directories(core PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

target_link_libraries(core PUBLIC
    Qt6::Core Qt6::Sql Qt6::Network
)

注意这里用了 PUBLIC 链接Qt。为什么?因为core模块的头文件里引用了Qt的类型,任何依赖core的模块都需要Qt。这就是依赖传递的含义。

我曾经犯过的错: 把所有模块都设为STATIC库,结果链接时符号冲突。后来改成:core和ui用STATIC,app用EXECUTABLE。如果模块会被多个项目复用,就用SHARED。

2.4 跨平台编译配置

跨平台是Qt的强项,但CMake配置不好,照样编译不过。我总结了一套「一次配置,到处编译」的方案。

2.4.1 平台检测与条件编译

# 检测操作系统
if(WIN32)
    message(STATUS "Building on Windows")
    set(PLATFORM_SOURCES
        platform_win.cpp
        platform_win.h
    )
elseif(APPLE)
    message(STATUS "Building on macOS")
    set(PLATFORM_SOURCES
        platform_mac.mm   # Objective-C++ 文件
        platform_mac.h
    )
elseif(UNIX)
    message(STATUS "Building on Linux")
    set(PLATFORM_SOURCES
        platform_linux.cpp
        platform_linux.h
    )
endif()

# 检测编译器
if(MSVC)
    add_compile_options(/W4 /utf-8)
else()
    add_compile_options(-Wall -Wextra -Wpedantic)
endif()

嗯,这里有个细节:macOS上如果用到系统API,文件后缀要用 .mm,CMake会自动用Objective-C++编译器处理。

2.4.2 第三方库的跨平台处理

# 查找第三方库,不同平台路径不同
find_package(OpenSSL REQUIRED)
if(WIN32)
    # Windows上OpenSSL通常装在C盘
    set(OPENSSL_ROOT_DIR "C:/Program Files/OpenSSL-Win64")
elseif(APPLE)
    # macOS上用Homebrew安装
    set(OPENSSL_ROOT_DIR "/usr/local/opt/openssl")
endif()

target_link_libraries(MyApp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

说实话,第三方库的跨平台是最头疼的。我的建议是:能用vcpkg或Conan就别手动找路径。我们团队现在统一用vcpkg,一个 vcpkg.json 搞定所有依赖。

2.4.3 安装与打包配置

# 安装规则
install(TARGETS MyApp
    RUNTIME DESTINATION bin
    BUNDLE DESTINATION .   # macOS的.app bundle
)

install(FILES
    config.ini
    DESTINATION bin/config
)

# CPack打包配置
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "NSIS" "DMG" "DEB")
include(CPack)

CPack是CMake自带的打包工具。一行 cpack 命令,Windows生成exe安装包,macOS生成dmg,Linux生成deb。我当年手动打包的日子一去不复返了。

2.5 避坑指南与最佳实践

最后,分享几个我这些年积累的经验:

  1. 版本号统一管理:在根CMakeLists.txt里定义版本号,子模块用 ${PROJECT_VERSION} 引用。别到处写死。
  2. 善用CMake Presets:Qt6 + CMake 3.20以上支持Presets,可以预设Debug/Release/Profile等配置。我团队现在都用这个。
  3. 不要手动添加源文件:用 file(GLOB ...) 自动收集源文件。但注意,CMake不会自动检测新文件,需要重新cmake。
  4. 区分PRIVATE和PUBLIC:原则是「能不公开就不公开」。减少依赖传递,编译速度会快很多。

核心总结:

  • CMake基础:项目声明、变量、查找包、链接库
  • Qt配置:AUTOMOC/AUTORCC/AUTOUIC三件套、qt_add_executable
  • 多模块:add_subdirectory分层、STATIC/SHARED/EXECUTABLE三种类型
  • 跨平台:平台检测、条件编译、CPack打包

CMake这东西,说白了就是个「配置即代码」的实践。刚开始觉得繁琐,但一旦形成自己的模板,新项目直接复制粘贴改改名字,十分钟搞定构建系统。嗯,这就是工程化的魅力。

CMake构建系统知识体系 CMake构建系统 基础语法 cmake_minimum_required project / set / find_package target_link_libraries Qt项目配置 AUTOMOC / AUTORCC / AUTOUIC qt_add_executable qt_add_translations 多模块构建 add_subdirectory 分层 STATIC / SHARED / EXECUTABLE 跨平台编译 WIN32 / APPLE / UNIX 检测 CPack 打包 (NSIS/DMG/DEB)
可可精品资料库