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 之类的命令,麻烦得很。
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。这就是依赖传递的含义。
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 避坑指南与最佳实践
最后,分享几个我这些年积累的经验:
- 版本号统一管理:在根CMakeLists.txt里定义版本号,子模块用
${PROJECT_VERSION}引用。别到处写死。 - 善用CMake Presets:Qt6 + CMake 3.20以上支持Presets,可以预设Debug/Release/Profile等配置。我团队现在都用这个。
- 不要手动添加源文件:用
file(GLOB ...)自动收集源文件。但注意,CMake不会自动检测新文件,需要重新cmake。 - 区分PRIVATE和PUBLIC:原则是「能不公开就不公开」。减少依赖传递,编译速度会快很多。
核心总结:
- CMake基础:项目声明、变量、查找包、链接库
- Qt配置:AUTOMOC/AUTORCC/AUTOUIC三件套、qt_add_executable
- 多模块:add_subdirectory分层、STATIC/SHARED/EXECUTABLE三种类型
- 跨平台:平台检测、条件编译、CPack打包
CMake这东西,说白了就是个「配置即代码」的实践。刚开始觉得繁琐,但一旦形成自己的模板,新项目直接复制粘贴改改名字,十分钟搞定构建系统。嗯,这就是工程化的魅力。