CMake

CMakeLists.txt

  1. 指定语言版本

    1
    set(CMAKE_CXX_STANDARD 11)

    CMAKE_开头的变量都是CMAKE内置变量,CMAKE保留变量

  2. 配置编译选项

    1
    2
    3
    add_compile_options(-Wall -Wextra -pedantic -Werror)
    #或者
    set(CMAKE_CXX_FLAGS ""${CMAKE_CXX_FLAGS} -pipe -std=c++11"")
  3. 配置编译类型
    类型可设置为:Debug, Release, RelWithDebInfo, MinSizeRel。可以针对不同的编译类型设置不同的编译选项

    1
    2
    3
    set(CMAKE_BUILD_TYPE Debug)
    #-g开启调试信息 -o0不进行代码优化
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
  4. 全局宏定义
    通过判断不同的宏定义在命令行cmake -DDEBUG=1 -DREAL_COOL_ENGINEER=0实现.cpp源代码中宏的流程控制
    1
    add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
  5. 添加include目录
    1
    include_directories(src/include)

多工程嵌套时使用target_include_directories管理头文件包含

当工程A需要使用工程B的头文件时,target_include_directories是CMake中管理这种依赖关系的推荐方式。以下是详细解决方案:

一、基本配置方法

1. 在工程B中设置头文件可见性

1
2
3
4
5
6
7
8
9
# 工程B的CMakeLists.txt
add_library(projectB STATIC srcB.cpp includeB/projectB.h)

# 关键设置:声明头文件目录为PUBLIC
target_include_directories(projectB
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/includeB>
$<INSTALL_INTERFACE:include>
)

2. 在工程A中引用工程B

1
2
3
4
5
# 工程A的CMakeLists.txt
add_executable(projectA srcA.cpp)

# 链接时会自动包含工程B的头文件路径
target_link_libraries(projectA PRIVATE projectB)

二、多工程嵌套的完整示例

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
workspace/
├── projectA/
│ ├── CMakeLists.txt
│ ├── src/
│ └── main.cpp
└── projectB/
├── CMakeLists.txt
├── include/
│ └── projectB/
│ └── module.h
└── src/
└── module.cpp

工程B配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# projectB/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ProjectB)

add_library(projectB STATIC src/module.cpp)

target_include_directories(projectB
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

# 安装规则(可选)
install(TARGETS projectB
EXPORT projectBTargets
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include
)

install(DIRECTORY include/projectB DESTINATION include)

工程A配置

1
2
3
4
5
6
7
8
9
10
# projectA/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(ProjectA)

# 添加子项目(假设在同一个workspace)
add_subdirectory(../projectB ${CMAKE_BINARY_DIR}/projectB)

add_executable(projectA main.cpp)

target_link_libraries(projectA PRIVATE projectB)

三、高级用法

1. 使用find_package(安装后引用)

1
2
3
4
5
6
7
8
9
10
# 工程B配置安装导出
install(EXPORT projectBTargets
FILE ProjectBConfig.cmake
NAMESPACE ProjectB::
DESTINATION lib/cmake/ProjectB
)

# 工程A通过find_package引用
find_package(ProjectB REQUIRED)
target_link_libraries(projectA PRIVATE ProjectB::projectB)

2. 跨工程包含不同目录级别

1
2
3
4
5
6
7
8
# 工程B设置多层包含目录
target_include_directories(projectB
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/projectB/core>
$<INSTALL_INTERFACE:include>
$<INSTALL_INTERFACE:include/projectB/core>
)

3. 条件包含设置

1
2
3
4
5
6
target_include_directories(projectB
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/private_headers
)

四、常见问题解决方案

问题1:找不到头文件

解决方案

1
2
3
# 确保工程B正确设置了PUBLIC包含路径
get_target_property(projectB_includes projectB INCLUDE_DIRECTORIES)
message(STATUS "ProjectB includes: ${projectB_includes}")

问题2:安装后路径错误

解决方案

1
2
3
4
5
# 使用相对路径确保可移植性
target_include_directories(projectB
PUBLIC
$<INSTALL_INTERFACE:include/projectB>
)

问题3:多配置生成器问题

解决方案

1
2
3
4
5
target_include_directories(projectB
PUBLIC
$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/include/debug>
$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/include/release>
)

五、最佳实践

  1. 使用生成器表达式$<BUILD_INTERFACE>$<INSTALL_INTERFACE>确保路径正确
  2. 命名空间隔离:为安装的目标添加命名空间避免冲突
  3. 明确作用域
    • PUBLIC:依赖项目需要包含的头文件
    • PRIVATE:仅本项目内部使用的头文件
  4. 版本兼容性:为安装的库生成版本文件
1
2
3
4
5
6
7
# 工程B的版本文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
ProjectBConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)

通过这种方式,可以清晰地管理多工程间的头文件依赖关系,确保无论是在开发时(同workspace)还是安装后引用,都能正确找到所需的头文件。

Qt中使用多个CMakeLists.txt实现模块化项目管理

在大型Qt项目中,使用多个CMakeLists.txt文件进行模块化管理可以提高项目的可维护性和可扩展性。以下是完整的模块化项目管理方案:

一、基本项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyProject/
├── CMakeLists.txt # 根CMake
├── app/
│ ├── CMakeLists.txt # 应用层
│ └── main.cpp
├── core/
│ ├── CMakeLists.txt # 核心模块
│ ├── core.h
│ └── core.cpp
├── gui/
│ ├── CMakeLists.txt # GUI模块
│ ├── mainwindow.h
│ └── mainwindow.cpp
└── tests/
├── CMakeLists.txt # 测试模块
└── core_test.cpp

二、根CMakeLists.txt配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 设置Qt版本和组件
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)

# 添加子目录
add_subdirectory(core)
add_subdirectory(gui)
add_subdirectory(app)
add_subdirectory(tests) # 可选

三、模块化CMakeLists.txt示例

1. 核心模块 (core/CMakeLists.txt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建核心库
add_library(core_lib STATIC
core.cpp
core.h
)

# 设置包含目录
target_include_directories(core_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

# 链接Qt模块
target_link_libraries(core_lib
PRIVATE
Qt6::Core
)

# 安装规则(可选)
install(TARGETS core_lib DESTINATION lib)
install(FILES core.h DESTINATION include/core)

2. GUI模块 (gui/CMakeLists.txt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查找UI文件
file(GLOB UI_FILES *.ui)
file(GLOB RESOURCES *.qrc)

# 创建GUI库
add_library(gui_lib STATIC
mainwindow.cpp
mainwindow.h
mainwindow.ui
${RESOURCES}
)

# 链接依赖
target_link_libraries(gui_lib
PRIVATE
Qt6::Widgets
Qt6::Gui
PUBLIC
core_lib # 链接核心库
)

# 设置包含目录
target_include_directories(gui_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

3. 应用层 (app/CMakeLists.txt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建可执行文件
add_executable(MyProject
main.cpp
)

# 链接所有必要库
target_link_libraries(MyProject
PRIVATE
Qt6::Widgets
gui_lib
core_lib
)

# 设置可执行文件属性
set_target_properties(MyProject PROPERTIES
WIN32_EXECUTABLE TRUE
MACOSX_BUNDLE TRUE # 如果是macOS应用
)

四、高级模块化技巧

1. 条件编译模块

1
2
3
4
5
# 根CMakeLists.txt中
option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()

2. 动态库模块

1
2
3
4
5
6
7
8
# 在模块CMakeLists.txt中
add_library(my_plugin SHARED
plugin.cpp
)
target_link_libraries(my_plugin
PRIVATE
Qt6::Core
)

3. 资源文件处理

1
2
3
4
5
6
# 在模块中添加资源
qt_add_resources(gui_lib
PREFIX "/gui"
FILES
resources.qrc
)

4. 翻译文件支持

1
2
3
4
5
6
7
8
# 查找翻译文件
set(TS_FILES
translations/myproject_zh_CN.ts
)

qt_add_translations(MyProject
TS_FILES ${TS_FILES}
)

五、跨模块依赖管理

1. 创建配置头文件

1
2
3
4
5
6
7
8
9
# 在core/CMakeLists.txt中
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/core_config.h.in
${CMAKE_CURRENT_BINARY_DIR}/core_config.h
)
target_include_directories(core_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)

2. 使用target_include_directories

1
2
3
4
5
6
7
# 在gui/CMakeLists.txt中
target_include_directories(gui_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
)

3. 导出目标

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在core/CMakeLists.txt中
install(TARGETS core_lib
EXPORT core_libTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)

install(EXPORT core_libTargets
FILE core_libTargets.cmake
DESTINATION lib/cmake/core_lib
)

六、测试模块配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# tests/CMakeLists.txt
find_package(Qt6 REQUIRED COMPONENTS Test)

# 创建测试可执行文件
add_executable(core_tests
core_test.cpp
)

# 链接测试库
target_link_libraries(core_tests
PRIVATE
Qt6::Test
core_lib
)

# 添加测试
add_test(NAME core_tests COMMAND core_tests)

七、现代CMake最佳实践

  1. 使用target_命令:优先使用target_include_directories()而非全局include_directories()
  2. 明确作用域:区分PUBLICPRIVATEINTERFACE依赖
  3. 避免全局变量:使用target_link_libraries()而非link_libraries()
  4. 生成导出头文件:使用configure_file()生成版本信息
  5. 使用生成器表达式:如$<BUILD_INTERFACE:...>$<INSTALL_INTERFACE:...>

八、完整示例:模块间通信

1. 在core模块定义接口

1
2
3
4
5
6
7
8
9
10
11
// core/icore.h
#pragma once
#include <QObject>

class ICore : public QObject {
Q_OBJECT
public:
virtual void initialize() = 0;
signals:
void coreInitialized();
};

2. 在GUI模块使用接口

1
2
3
4
5
6
7
8
9
// gui/mainwindow.cpp
#include <core/icore.h>

MainWindow::MainWindow(ICore* core, QWidget *parent)
: QMainWindow(parent), m_core(core)
{
connect(m_core, &ICore::coreInitialized,
this, &MainWindow::onCoreInitialized);
}

3. 在CMake中确保包含路径

1
2
3
4
5
# core/CMakeLists.txt
target_include_directories(core_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

通过这种模块化结构,您的Qt项目将获得更好的:

  • 可维护性:清晰的项目结构
  • 可扩展性:轻松添加新模块
  • 可测试性:独立测试各模块
  • 复用性:模块可独立编译使用

CMake 配置(重载)与构建的区别

CMake 的配置(重载)构建是两个完全不同但相互关联的阶段,理解它们的区别对于有效使用 CMake 至关重要。

CMake 配置(Configure / 重载)

定义

配置阶段是 CMake 处理 CMakeLists.txt 文件并生成构建系统文件的过程。

执行命令

1
2
3
cmake -B build_dir -DCMAKE_BUILD_TYPE=Release  # 配置
# 或者
cmake -S . -B build_dir # 配置

主要任务

  1. 解析 CMakeLists.txt:读取和分析项目配置
  2. 检测系统环境:检查编译器、工具链、依赖库
  3. 生成构建系统文件
    • Unix: 生成 Makefile
    • Windows: 生成 *.sln*.vcxproj
    • Ninja: 生成 build.ninja
  4. 设置变量和选项:处理 -D 参数定义的变量
  5. 生成自动生成的文件:如 *_autogen 目录中的文件

输出结果

  • 构建系统文件(Makefile、*.sln 等)
  • CMakeCache.txt(缓存变量)
  • CMakeFiles/ 目录
  • 自动生成的文件(如 moc*.cpp、ui*.h 等)

CMake 构建(Build)

定义

构建阶段是使用生成的构建系统文件实际编译源代码和链接目标文件的过程。

执行命令

1
2
3
cmake --build build_dir --config Release  # 构建
# 或者
cd build_dir && make # 使用生成的 Makefile

主要任务

  1. 编译源代码:将 .cpp 文件编译为 .o 对象文件
  2. 运行代码生成器:执行 moc(Qt)、protoc(Protocol Buffers)等
  3. 链接目标文件:将对象文件链接成可执行文件或库
  4. 处理依赖关系:确保正确的构建顺序

输出结果

  • 可执行文件(bin/)
  • 库文件(lib/)
  • 对象文件(*.o)
  • 最终的应用程序

关键区别对比

特性 CMake 配置(重载) CMake 构建
目的 生成构建系统 编译和链接代码
输入 CMakeLists.txt 源代码文件
输出 Makefile/MSBuild 文件 可执行文件/库
执行频率 修改配置时执行 修改代码时执行
耗时 相对较短 可能很长(取决于代码量)
命令 cmake -B build_dir cmake --build build_dir
依赖 需要 CMake 需要编译器

典型工作流程

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 首次配置(生成构建系统)
cmake -B build -DCMAKE_BUILD_TYPE=Release

# 2. 构建项目(编译代码)
cmake --build build --parallel 8

# 3. 修改代码后,只需要重新构建
cmake --build build

# 4. 修改 CMakeLists.txt 后,需要重新配置
cmake -B build # 重新配置
cmake --build build # 重新构建

什么时候需要重新配置?

需要执行 cmake -B build_dir 的情况:

  • 修改了 CMakeLists.txt 文件
  • 添加或删除了源文件
  • 更改了 CMake 变量(如 -D 参数)
  • 系统环境发生变化(编译器路径变更等)
  • 依赖库版本更新

什么时候只需要重新构建?

只需要执行 cmake --build build_dir 的情况:

  • 修改了源代码文件(.cpp, .h)
  • 修改了资源文件
  • 需要重新链接(但配置未变)

实际示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 初始设置
cmake -B cmake-build-release -DCMAKE_BUILD_TYPE=Release

# 开发过程中...
# 只修改了 main.cpp -> 只需要构建
cmake --build cmake-build-release

# 修改了 CMakeLists.txt -> 需要重新配置和构建
cmake -B cmake-build-release
cmake --build cmake-build-release

# 清理构建(但不删除配置)
cmake --build cmake-build-release --target clean

# 完全重新开始(删除整个构建目录)
rm -rf cmake-build-release
cmake -B cmake-build-release -DCMAKE_BUILD_TYPE=Release

常见误区

  1. 错误:每次代码修改都运行 cmake -B build

    • 正确:只需要运行 cmake --build build
  2. 错误:认为配置很耗时,避免运行

    • 正确:配置相对较快,确保构建系统正确很重要
  3. 错误:手动修改生成的 Makefile

    • 正确:总是通过修改 CMakeLists.txt 来改变配置

理解这两个阶段的区别可以显著提高 CMake 的使用效率和项目的构建速度。

install

install 命令用于定义编译完成后如何安装目标文件(如可执行文件、库)、头文件、配置文件等到指定位置,是构建安装流程的核心指令。该命令不会在构建阶段执行,而是在用户显式调用 cmake --install 时生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
install(TARGETS <目标名称>...   # 安装构建目标(如可执行文件、库)
[EXPORT <导出名称>]
[RUNTIME DESTINATION <目录>] # 可执行文件(.exe, 无后缀)安装位置
[LIBRARY DESTINATION <目录>] # 共享库(.so, .dylib, .dll)安装位置
[ARCHIVE DESTINATION <目录>] # 静态库(.a, .lib)安装位置
[INCLUDES DESTINATION <目录>] # 头文件搜索路径
)

install(FILES <文件列表>... # 安装单个文件(如配置文件)
DESTINATION <目录>
)

install(DIRECTORY <目录路径>... # 安装整个目录(如头文件目录)
DESTINATION <目录>
[PATTERN <模式> EXCLUDE] # 过滤文件(如排除临时文件)
)

cmake文件与CMakeLists.txt区别

核心区别:

  1. 角色与定位:

    • CMakeLists.txt 这是 项目构建的入口点和主控文件。它是 CMake 处理一个项目(或项目中的一个目录)的起点和核心。每个包含源代码或需要构建逻辑的目录必须有一个 CMakeLists.txt 文件。它定义了项目的整体结构、目标(可执行文件、库)、编译选项、链接依赖关系以及包含哪些子目录。
    • .cmake 文件: 这些是 模块化、可重用的脚本片段或功能包。它们本身通常不是 CMake 的起点。它们包含函数、宏、变量定义、查找特定库或工具的复杂逻辑(Find<Package>.cmake)、设置特定功能的代码等。它们被设计成可以被 include() 指令加载到 CMakeLists.txt 文件或其他 .cmake 文件中。
  2. 命名与位置:

    • CMakeLists.txt 严格命名。必须叫 CMakeLists.txt(注意大小写和下划线)。通常位于项目的根目录以及每个需要参与构建的子目录中。
    • .cmake 文件: 灵活命名。可以是任何有效的文件名(通常具有描述性),只要以 .cmake 结尾即可(例如:ConfigureCompiler.cmake, VersionHelpers.cmake, FindOpenSSL.cmake)。它们可以放在项目的任何位置(通常组织在 cmake/ 目录下),也可以放在系统的 CMake 模块路径中(如 /usr/share/cmake/Modules/)。
  3. 执行方式:

    • CMakeLists.txt CMake 工具 (cmake 命令) 直接处理这些文件。当你运行 cmake <path-to-source> 时,你指向的就是包含顶层 CMakeLists.txt 的目录。CMake 会处理该文件,并递归处理其中 add_subdirectory() 指令指定的子目录中的 CMakeLists.txt
    • .cmake 文件: 它们需要通过 include() 指令显式加载CMakeLists.txt 文件(或另一个 .cmake 文件)中才会被执行。例如:include(cmake/MyHelpers.cmake)find_package(OpenSSL REQUIRED)(后者会在模块路径中查找 FindOpenSSL.cmake 并执行它)。
  4. 内容侧重点:

    • CMakeLists.txt
    侧重于

    项目结构定义

    和

    构建目标声明

    。

    - 项目声明 (`project()`)
    - 添加可执行文件 (`add_executable()`)
    - 添加库 (`add_library()`)
    - 链接库 (`target_link_libraries()`)
    - 设置编译/链接选项 (`target_compile_options()`, `target_link_options()`)
    - 包含目录 (`target_include_directories()`)
    - 添加子目录 (`add_subdirectory()`)
    - 条件分支 (`if()`, `elseif()`, `else()`)
    - 循环 (`foreach()`, `while()`)
    - **加载 `.cmake` 文件 (`include()`, `find_package()`)**

- `.cmake` 文件:



    侧重于

    封装可重用逻辑

    。

    - 定义函数 (`function() ... endfunction()`)
    - 定义宏 (`macro() ... endmacro()`)
    - 定义变量(尤其是供外部使用的变量)
    - 封装复杂的查找逻辑(`find_path()`, `find_library()`, `find_program()` 等),通常用于 `Find<Package>.cmake` 脚本。
    - 封装特定平台或编译器的设置。
    - 提供工具函数(如字符串处理、文件操作辅助函数)。
    - 实现自定义的 CMake 命令或策略。