1-cmake 实践

时隔大半年,我这个懒狗终于要开始尝试使用 cmake 构建工程了,这里打算跟着教程来边做边学习。

CMake 实践

基本操作

创建工程

现在 Vs2022 可以直接创建 CMake 工程了,创建好后,稍微调整一下文件结构如下:

其中,

  • Dependencies 文件夹准备存放一些依赖的头文件和链接库
  • out 文件夹存放编译好的程序等
  • src 文件夹则存放我们写的代码

CMake 的内容如下:

   
cmake_minimum_required (VERSION 3.8) # 为 MSVC 编译器启用热重载。 if (POLICY CMP0141) cmake_policy(SET CMP0141 NEW) set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>") endif() project ( LearnOpenGL LANGUAGES CXX DESCRIPTION "用CMake构建的LearnOpenGL" VERSION 0.0.1 ) add_executable (LearnOpenGL "src/main.cpp" ) target_compile_features (LearnOpenGL PRIVATE cxx_std_20)

project 中,第一行是项目名,第二行是项目所用到的语言,第三行是项目描述,第四行是项目版本。

接下来的 add_executable() 指定要给项目包含哪些文件,target_compile_features() 则指定项目的语言标准是 C++20。

生成工程

可以用:

cmake -S . -B ./out/cmake-build

来构建项目,其中 -S 后跟源文件位置,-B 后跟构建项目的位置。

也可以用:

cmake -G

查看它可以生成哪些工程,默认生成的工程前面有 *。如果想构建其他工程,需要在工程构建命令后添加 -G"想构建的种类"

直接编译

当然,也可以直接编译然后输出文件:

cmake --build ./out/cmake-build

添加头文件 & 链接文件

能用 target_include_directories 添加头文件, target_link_libraries 添加链接的库,这里以前者为例:

target_include_directories (项目名 PUBLIC/PRIVATE 路径)

其中,PUBLICPRIVATE 表示是否会在指定路径递归搜索相关文件。

添加子模块

例如想要进行单元测试,先在最外层的 CMakeLists 里添加:

   
# 单元测试子模块 include (CTest) enable_testing () add_subdirectory (test)

然后新建 test 文件夹,里边也有个 CMakeLists:

   
# 添加测试项目 add_executable (myTest "./myTest.cpp") add_test(NAME myTest COMMAND $<TARGET_FILE:myTest>)

最后编写好测试代码,即可在 VS2022 中通过 “测试” -> “为 xxx 项目运行 CTest” 进行单元测试。

添加第三方库

首先先在主项目配置前添加:

# 第三方依赖库
add_subdirectory("Dependencies")

直接是 CMake 项目

如果第三方库直接是 CMake 项目,例如 glfw,那么:

  1. Dependencies 的 CMakeLists 中添加 add_subdirectory(glfw)
  2. 在根目录的 CMakeLists 的主项目配置中添加 target_link_libraries(LearnOpenGL PUBLIC glfw)

只有一个头文件的库

如果第三方库只有一个头文件,例如 stb_image,那么:

  1. Dependencies 的 CMakeLists 中添加 add_subdirectory(stb_image)

  2. Dependencies 下建立如下文件结构:

  3. 在这里的 CMakeList 中添加如下内容:

       
    # stb_image add_library(stb_image INTERFACE) add_compile_definitions(STB_IMAGE_IMPLEMENTATION STBI_WINDOWS_UTF8) # 添加宏定义 target_include_directories(stb_image INTERFACE "./") target_compile_features(stb_image INTERFACE cxx_std_20)

只有 lib/dll 的库

例如要添加库 A,可以在 Dependencies 的 CMakeLists 中添加:

include(./A/find_A.cmake)

然后在 A 中创建 find_A.cmake

   
set(A_PATH "" CACHE PATH "存放A库的根路径") set(a_dll "${A_PATH}/foo/A.dll" CACHE INTERNAL) add_library(A SHARED/STATIC IMPORTED GLOBAL) target_include_directory(A INTERFACE "${A_PATH}/include/") set_target_properties(A PROPERTIES IMPORTED_LOCATION "${A_PATH}/foo/A.dll" IMPORTED_IMPLIB "${A_PATH}/bar/A.lib" )

最后在需要用的地方引入 A 就行,并且生成的时候需要指定宏 A_PATH 的内容。

如果 A 库有 DLL,需要在使用 A 库的地方将 A.dll 拷贝到程序运行的地方,可在需要使用 A 库的 CMakeLists 中添加:

   
add_custom_command(TARGET 需要用A的项目名 POST_BUILD # cmake -E copy_if_different 源路径 目的路径 COMMAND ${CMAKE_COMMON} -E copy_if_different ${a_dll} $<TARGET_FILE_DIR:用A的项目名> )

使用预编译头文件 (PCH)

当项目变得很大,需要一次性包含许多头文件时,会产生很多次(重复)编译,十分耗时。这时预编译头文件(PCH,Pre-Compiled Header)便派上用场。

PCH 中会包含项目中常用的头文件(通常是标准库和第三方库),并且 这些头文件尽量不要变动,否则会重复编译,然后会预先将它编译成链接库,之后进行链接操作即可,仅需编译一次。

编写好 PCH 后,接下来康康怎么用 CMake 对它预编译。只需在需要它的地方添加:

target_precompile_headers(项目名 PUBLIC 头文件路径)

即可。需要注意的是:如果还有项目要用到这个 PCH,需要把 PUBLIC 改成 REUSE_FROM

使用宏复用代码

如果碰见需要重复使用一段代码的情形,可以用宏:

   
macro(宏名称 参数名) # 使用宏的代码 add_executable(${参数名} ./${参数名}.cpp) endmarco()

变量与选项

option()

随着项目规模增大,可能分为项目本体、测试、示例几部分。而用户可不想一次性编译所有内容,这时候就需要编写一些开关,让用户去决定他要编译哪些部分。

可以使用 option() 声明一个开关,然后交给 if()

   
option(ENABLE_TESTS "编译项目的测试文件" OFF) # 这里前者是判断CMakeList是不是最外层的 if(PROJECT_IS_TOP_LEVEL OR ENABLE_TESTS) include(CTest) enable_testing() add_subdirectory(test) endif()

用户可以方便地在 GUI 中控制我们编写的工程:

如果没有 GUI,可以通过在生成工程的命令行后加上 -DENABLE_TESTS=ON 来开启测试。

set()

可以用 set() 声明一个变量:

set(engine_name engine)

然后用 ${engine_name} 来使用它。

如果想要把这个变量缓存到 Cache 里边(可以被 GUI 发现),需要这样写:

set(变量名 值 CACHE 类型)

更好地加入头 & 源文件

aux_source_directory()

使用它可快速添加某路径的所有源文件(.cpp)至某个 CMake 变量上:

   
aux_source_directory(路径 变量名) # 使用该变量 add_library(myProj STATIC ${变量名} ......)

file()

如果想快速添加头文件,需要用到 file() 操作:

file(GLOB_RECURSE 变量名 带匹配的路径(如 ./include/*.h))

这样可以将 include 文件夹下的所有头文件(包括子文件夹里的)包含到一个变量中。如果不想包括子文件,可以去掉_RECURSE

输出文本

使用 message() 进行文本输出:

message("xxxxxx ${变量名}")

如果变量的类型是列表(List) ,可以使用 foreach() 进行输出:

   
foreach(var in ${变量名}) message(${var}) endforeach()

和 C++ 程序” 通信”

有时候 C++ 程序需要加载一个 DLL,可以先在源库对应的 CMakeListsxxx.cmake 中加入:

target_compile_definitions(源库项目名 INTERFACE SOURCE_PATH="${源库路径变量名}")

然后这个源库的路径在 C++ 程序中以宏的形式存在:

dll_load(SOURCE_PATH);

参考资料