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);
参考资料
- 【直播切片】一个半小时入门现代CMake_哔哩哔哩_bilibili