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 路径)
其中,PUBLIC
和 PRIVATE
表示是否会在指定路径递归搜索相关文件。
添加子模块
例如想要进行单元测试,先在最外层的 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
,那么:
- 在
Dependencies
的 CMakeLists 中添加add_subdirectory(glfw)
。 - 在根目录的 CMakeLists 的主项目配置中添加
target_link_libraries(LearnOpenGL PUBLIC glfw)
。
只有一个头文件的库
如果第三方库只有一个头文件,例如 stb_image
,那么:
在
Dependencies
的 CMakeLists 中添加add_subdirectory(stb_image)
。在
Dependencies
下建立如下文件结构:在这里的 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,可以先在源库对应的 CMakeLists
或 xxx.cmake
中加入:
target_compile_definitions(源库项目名 INTERFACE SOURCE_PATH="${源库路径变量名}")
然后这个源库的路径在 C++ 程序中以宏的形式存在:
dll_load(SOURCE_PATH);