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);

参考资料

  • 【直播切片】一个半小时入门现代CMake_哔哩哔哩_bilibili