从零实现响应式框架——CMakeLists实现

从零实现 CMakeLists:构建 C++ 项目的完整步骤

CMake 是一个非常强大的构建工具,广泛应用于 C++ 项目的构建、测试、安装和包管理。本文将通过从零开始的方式,带领大家实现一个 CMakeLists 文件,并解释每一行代码的含义与作用。通过这篇文章,你将学会如何创建一个适用于 C++20 项目的 CMake 构建配置,并理解如何通过 CMake 管理源文件、库、测试以及安装过程。


1. 初始化项目设置

首先,我们需要使用 cmake_minimum_required() 来指定我们所需的最低 CMake 版本,并使用 project() 设置项目名称和版本。

1
2
cmake_minimum_required(VERSION 3.10)
project(reaction VERSION 1.0 LANGUAGES CXX)
  • cmake_minimum_required(VERSION 3.10):指定 CMake 的最低版本为 3.10,确保使用该版本或更新版本时可以正常工作。
  • project(reaction VERSION 1.0 LANGUAGES CXX):定义项目名称为 reaction,版本为 1.0,并指定该项目使用 C++ 语言。

2. 设置编译选项

我们根据操作系统的不同来设置编译选项。对于 MSVC(微软的 C++ 编译器),我们启用 /W4 且禁用 RTTI;对于其他平台,我们启用警告并禁用 RTTI。

1
2
3
4
5
if(MSVC)
add_compile_options(/W4 /GR-)
else()
add_compile_options(-Wall -Wextra -pedantic -fno-rtti -std=c++20)
endif()
  • add_compile_options():该命令设置了编译器的标志。对于 MSVC,我们禁用了 RTTI(/GR-),并将警告等级设置为 W4;对于其他平台,我们启用了常见的编译器警告,并指定 C++20 标准。

3. 设置构建类型

接下来,我们设置默认的构建类型为 Debug,如果用户没有指定构建类型的话。

1
2
3
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE)
endif()
  • set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE):如果没有设置 CMAKE_BUILD_TYPE,我们将其强制设为 Debug,这样可以方便地进行调试构建。

4. 添加库与目标

在 CMake 中,我们创建一个库并为它添加接口头文件。这里我们创建的是一个 INTERFACE 类型的库,意味着它不产生实际的二进制文件,只提供头文件和相关接口。

1
2
add_library(${PROJECT_NAME} INTERFACE)
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
  • add_library(${PROJECT_NAME} INTERFACE):创建一个接口库,适用于 header-only 类型的库。它没有编译的源文件,只依赖于头文件。
  • add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}):创建一个别名,使得我们在其他地方引用库时使用 reaction::reaction 来替代 reaction,提高代码可读性。

5. 设置包含目录

我们使用 target_include_directories() 来指定库的头文件目录,确保当用户在其他项目中使用该库时,能够正确找到头文件。

1
2
3
4
target_include_directories(${PROJECT_NAME} INTERFACE
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
  • $<INSTALL_INTERFACE:include>:指示安装目录中的头文件路径。
  • $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>:指示构建过程中使用的头文件路径。

6. 处理源文件

我们通过 file(GLOB ...) 命令来收集头文件和源文件,并将它们添加到目标中。

1
2
3
4
5
6
file(GLOB HEADERS_LIST "${CMAKE_CURRENT_SOURCE_DIR}/include/reaction/*.h")
foreach(header_file ${HEADERS_LIST})
target_sources(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${header_file}>
)
endforeach()
  • file(GLOB ...):这个命令会将指定目录下的所有头文件(.h 文件)列入列表。
  • foreach(header_file ${HEADERS_LIST}):遍历收集到的头文件,并将它们添加到目标中。

7. 示例程序

我们接着查找并编译示例程序文件,并将它们与主库连接。

1
2
3
4
5
6
file(GLOB EXAMPLE_SOURCES ${PROJECT_SOURCE_DIR}/example/*.cpp)
foreach(example_file ${EXAMPLE_SOURCES})
get_filename_component(example_name ${example_file} NAME_WE)
add_executable(${example_name} ${example_file})
target_link_libraries(${example_name} PRIVATE ${PROJECT_NAME})
endforeach()
  • file(GLOB ...):收集示例程序的源文件。
  • add_executable(${example_name} ${example_file}):为每个示例文件创建一个可执行文件。
  • target_link_libraries(${example_name} PRIVATE ${PROJECT_NAME}):将库链接到每个示例程序中。

8. 单元测试

如果系统中有 GTest 库,我们将启用单元测试并构建测试执行文件。

1
2
3
4
5
6
7
8
9
10
find_package(GTest)
if(GTest_FOUND)
enable_testing()
file(GLOB TEST_SOURCES ${PROJECT_SOURCE_DIR}/test/*.cpp)
add_executable(runTests ${TEST_SOURCES})
target_link_libraries(runTests PRIVATE GTest::GTest GTest::Main ${PROJECT_NAME})
add_test(NAME reactionTest COMMAND runTests)
else()
message(WARNING "GTest not found, skipping tests.")
endif()
  • find_package(GTest):查找 GTest 库,如果找到则进行后续配置。
  • enable_testing():启用 CTest 测试功能。
  • add_test(NAME reactionTest COMMAND runTests):将测试目标添加到 CTest 测试系统。

9. 安装配置

最后,我们配置了安装路径和文件,使得用户能够将构建的项目安装到指定目录中。

1
2
3
4
5
6
7
8
9
10
11
12
install(
DIRECTORY include/reaction
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

install(
TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}Targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
  • install(...):这个命令指定了如何安装头文件、库文件以及可执行文件。
  • ${CMAKE_INSTALL_INCLUDEDIR}${CMAKE_INSTALL_LIBDIR} 等:这些变量代表了 CMake 的安装目录,通常是 /usr/local/include 或 Windows 下的其他目录。

10. 包配置文件

我们生成并安装了 CMake 包配置文件,使得其他项目可以方便地找到并使用我们的库。

1
2
3
4
5
6
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/${PROJECT_NAME}Config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
  • configure_package_config_file():使用模板文件生成实际的配置文件。
  • write_basic_package_version_file():生成包的版本文件。

11. 总结

通过本文的讲解,我们从零开始一步步实现了一个 CMakeLists 文件。通过这个配置文件,我们不仅能编译项目,还能实现测试、安装、以及其他功能。每一行 CMake 代码都与项目构建、源文件管理、库配置密切相关,因此深入理解这些配置对于开发复杂项目至关重要。


下一篇,我们将继续深入讨论 CMake 的高级特性,如多平台支持、跨平台构建等内容,敬请期待!


通过这篇文章的讲解,读者将能够理解每一行 CMakeLists 文件的作用,以及如何使用 CMake 配置一个完整的 C++ 项目,涵盖从编译到安装的全过程。