CMake 学习记录
小记录
参考:
https://www.zhihu.com/search?type=content&q=premake
https://www.zhihu.com/question/58949190/answer/999701073
口诀:
- Declare a target
- Declare target's traits
- It's all about targets
add_definitions 已经被 add_compile_definitions 取代
Windows下是 .dll 和 .lib,Linux下是 .so 和 .a
cmake区分大小写,但是cmake指令不分(比如set和SET)还有函数啥的也不分
cmake中永远用正斜杠 / ,cmake会自动将反斜杠转成正斜杠
学习资料
cmake-examples中文版:https://sfumecjf.github.io/cmake-examples-Chinese/
cmake-examples:https://github.com/ttroy50/cmake-examples
CMake “菜谱”:https://www.bookstack.cn/read/CMake-Cookbook/README.md
CMake 官方文档:https://cmake.org/cmake/help/latest/
CMake 中文文档:https://runebook.dev/zh-CN/docs/cmake/-index-
高性能并行编程与优化 - 课件:https://github.com/parallel101/course
More Modern CMake:
https://hsf-training.github.io/hsf-training-cmake-webpage/aio/index.html
https://github.com/hsf-training/hsf-training-cmake-webpage/
Modern CMake:https://cliutils.gitlab.io/modern-cmake/chapters/basics/structure.html
configuration stage / build stage
https://cmake.org/cmake/help/v3.25/manual/cmake.1.html#generate-a-project-buildsystem
cmake-examples
https://sfumecjf.github.io/cmake-examples-Chinese/
https://github.com/ttroy50/cmake-examples
1. basic
1.1 hello-cmake
最基础的当然是
cmake_minimum_required(VERSION 3.5) #设置CMake最小版本
project (hello_cmake) #设置工程名
add_executable(hello_cmake main.cpp) #生成可执行文件
细节就是,在这一行语句中project (hello_cmake) #设置工程名
CMake构建包含一个项目名称,上面的命令会自动生成一些变量,在使用多个项目时引用某些变量会更加容易。
比如生成了: PROJECT_NAME 这个变量。PROJECT_NAME是变量名,${PROJECT_NAME}是变量值,值为hello_cmake
因此上面的代码和下面的一样:
cmake_minimum_required(VERSION 3.5) #设置CMake最小版本
project (hello_cmake) #设置工程名
add_executable(${PROJECT_NAME} main.cpp) #生成可执行文件
1.2 头文件
对于头文件的情况,我们这样写:
cmake_minimum_required(VERSION 3.5)#最低CMake版本
project (hello_headers)# 工程名
set(SOURCES
src/Hello.cpp
src/main.cpp
)#创建一个变量,名字叫SOURCE。它包含了所有的cpp文件。
add_executable(hello_headers ${SOURCES})#用所有的源文件生成一个可执行文件,因为这里定义了SOURCE变量,所以就不需要罗列cpp文件了
#等价于命令: add_executable(hello_headers src/Hello.cpp src/main.cpp)
target_include_directories(hello_headers
PRIVATE
${PROJECT_SOURCE_DIR}/include
)#设置这个可执行文件hello_headers需要包含的库的路径
#PROJECT_SOURCE_DIR指工程顶层目录
#PROJECT_Binary_DIR指编译目录
#PRIVATE指定了库的范围,下一节讲
我们可以用 message 函数来查看,比如我在上面的最后一行加一句:message(${CMAKE_SOURCE_DIR})
我们在 cmake .. 的时候会打印输出:
我们在 make 的时候可以这样来详细列出(否则仅显示构建状态):make VERBOSE=1
1.3 Static Library
将源文件直接传递给add_library调用,这是modern CMake的建议。(而不是先把Hello.cpp赋给一个变量)
cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#库的源文件Hello.cpp生成静态库hello_library
add_library(hello_library STATIC
src/Hello.cpp
)
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
# target_include_directories为一个目标(可能是一个库library也可能是可执行文件)添加头文件路径。
############################################################
# Create an executable
############################################################
# Add an executable with the above sources
#指定用哪个源文件生成可执行文件
add_executable(hello_binary
src/main.cpp
)
#链接可执行文件和静态库
target_link_libraries( hello_binary
PRIVATE
hello_library
)
#链接库和包含头文件都有关于scope这三个关键字的用法。
关于 private pubic interface 的范围
官方文档讲解:https://cmake.org/cmake/help/v3.0/command/target_include_directories.html
- PRIVATE - 目录被添加到目标(库)的包含路径中。
- INTERFACE - 目录没有被添加到目标(库)的包含路径中,而是链接了这个库的其他目标(库或者可执行程序)包含路径中
- PUBLIC - 目录既被添加到目标(库)的包含路径中,同时添加到了链接了这个库的其他目标(库或者可执行程序)的包含路径中
也就是说,根据库是否包含这个路径,以及调用了这个库的其他目标是否包含这个路径,可以分为三种scope。
拿刚才的项目为例,结构如下:
那么对于我们的 CMake 命令:
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
我们这里 Hello.cpp 和 main.cpp 都是这样包含头文件的:#include "static/Hello.h"
而这里我们要加入 ${PROJECT_SOURCE_DIR}/include,为 PUBLIC 的时候,就会是正确的;为 PRIVATE 的时候,那么此时只有静态库能找到这个路径,对于链接这个静态库的最终可执行文件而言是没有添加这个路径的,也就是 main.cpp 没有添加这个路径,所以在 main.cpp 中会报错
而若是换成 INTERFACE ,那么就只有要链接这个静态库的东西添加了这个路径,因此会在 Hello.cpp 中报错:
同样,在代码:
target_link_libraries( hello_binary
PRIVATE
hello_library
)
中,若是把 PRIVATE 换成 INTERFACE 也会在 main.cpp 报错,因为 INTERFACE 会让 main.cpp 找不到这个路径。(这告诉CMake在链接期间将hello_library链接到hello_binary可执行文件。 同时,这个被链接的库如果有INTERFACE或者PUBLIC属性的包含目录,那么,这个包含目录也会被传递( propagate )给这个可执行文件。)
这里对于hello_binary,它不是库,所以不会被链接。直接private自己用这个库就行。
建议:
对于公共的头文件,最好在include文件夹下建立子目录。
传递给函数target_include_directories()的目录,应该是所有包含目录的根目录,然后在这个根目录下建立不同的文件夹,分别写头文件。
这样使用的时候,不需要写${PROJECT_SOURCE_DIR}/include,而是直接选择对应的文件夹里对应头文件。下面是例子:#include "static/Hello.h"而不是#include "Hello.h"使用此方法意味着在项目中使用多个库时,头文件名冲突的可能性较小。
1.4 Shared Library
CMakeLists:
cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#根据Hello.cpp生成动态库
add_library(hello_library SHARED
src/Hello.cpp
)
#给动态库hello_library起一个别的名字hello::library
add_library(hello::library ALIAS hello_library)
#为这个库目标,添加头文件路径,PUBLIC表示包含了这个库的目标也会包含这个路径
target_include_directories(hello_library
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################
#根据main.cpp生成可执行文件
add_executable(hello_binary
src/main.cpp
)
#链接库和可执行文件,使用的是这个库的别名。PRIVATE 表示
target_link_libraries( hello_binary
PRIVATE
hello::library
)
1.5 build-type、set、CACHE变量
强烈建议看:https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.5 build-type.html
cmake_minimum_required(VERSION 3.5)
#如果没有指定则设置默认编译方式
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
#在命令行中输出message里的信息
message("Setting build type to 'RelWithDebInfo' as none was specified.")
#不管CACHE里有没有设置过CMAKE_BUILD_TYPE这个变量,都强制赋值这个值为RelWithDebInfo
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)
# 当使用cmake-gui的时候,设置构建级别的四个可选项
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
"MinSizeRel" "RelWithDebInfo")
endif()
project (build_type)
add_executable(cmake_examples_build_type main.cpp)
关于编译器参数:
参考:http://cn.voidcc.com/question/p-fvbzitto-bhq.html
-DNDEBUG 由两部分组成,标记 -D 和参数 NDEBUG 。该标志被用于创建预处理器定义,因此这将创建一个新的预处理器 #define 称为 NDEBUG
-g 是一个编译器开关,以产生调试信息。它与 -D 完全分开创建定义。
在命令行运行CMake的时候, 使用cmake命令行的-D选项配置编译类型:
cmake .. -DCMAKE_BUILD_TYPE=Release
1.6 Compile Flags
cmake_minimum_required(VERSION 3.5)
#强制设置默认C++编译标志变量为缓存变量,如CMake(五) build type所说,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)
project (compile_flags)
add_executable(cmake_examples_compile_flags main.cpp)
#为可执行文件添加私有编译定义
target_compile_definitions(cmake_examples_compile_flags
PRIVATE EX3
)
在现代CMake中设置C ++标志的推荐方法是专门针对某个目标(target)设置标志,可以通过target_compile_definitions()函数设置某个目标的编译标志。
target_compile_definitions(cmake_examples_compile_flags
PRIVATE EX3
)
如果这个目标是一个库(cmake_examples_compile_flags),编译器在编译目标时添加定义-DEX3 ,并且选择了范围PUBLIC或INTERFACE,该定义-DEX3也将包含在链接此目标(cmake_examples_compile_flags)的所有可执行文件中。 注意,本语句使用了PRIVATE,所以编译选项不会传递。
1.7 Including Third Party Library
https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.7 Including Third Party Library.html
https://www.cnblogs.com/hebohang/p/15990146.html
1.8 设置 C++ 标准
# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.1)
# Set the project name
project (hello_cpp11)
# set the C++ standard to C++ 11
set(CMAKE_CXX_STANDARD 11)
# Add an executable
add_executable(hello_cpp11 main.cpp)
1.9 installing
https://github.com/ttroy50/cmake-examples/tree/master/01-basic/E-installing
2. sub projects
https://sfumecjf.github.io/cmake-examples-Chinese/02-sub-projects/A-basic/
小彭老师 CMake 教学
1
https://www.bilibili.com/video/BV1fa411r7zp/?spm_id_from=333.788
关于构建系统、CMake
首先是为什么出现了构建系统(比如 Makefile(GNU Make)、Ninja 之类的)
参考
https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.9 J-building-with-ninja.html
https://github.com/ttroy50/cmake-examples/tree/master/01-basic/J-building-with-ninja
比如老师这个例子:
有 hello.cpp main.cpp Makefile,我们make一下,会编译出 hello.o 和 main.o,要是我们改变 hello.cpp,我们就知道 main.o 不需要改变,只要再编译一次 hello.cpp 到 hello.o 即可,这便是增量编译。我想,检测可以由操作系统打时间戳,GNU Make 再去检测。
为了应对不同平台的差异,于是出现了 CMake,一个构建系统的构建系统,生成的 makefile 之类的再有对应的编译器编译成可执行文件。
想切换成 clang++ 去编译,可以这样写:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++
测试:
这里的 -B 就是指定编译的位置,我们这里相当于指定为文件夹 build 了。-D 则是 define 的意思,在前面有讲过。
还能直接指定C++版本:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_STANDARD=17
关于 library(动态库、静态库)
比如之前写的一个 printf 的代码,被优化成了 puts 函数,用 objdump 查看(反汇编了)如下:
这里的
就是一个插桩函数,会去 libc.so 找到对应的位置。
Linux是没有与 .so 配套的 .a 的,它会自动生成插桩。可以用 ldd a.out
查看可执行文件 a.out 链接了谁(.so)。
CMake 的一些指令
上图的 -DMY_MACRO=1 只是为了符合某些人的习惯,其实还是相当于 #define MY_MACRO 1
没有 target 的下面的指令就像是全局的,不推荐使用。
解决菱形依赖的问题:
安装第三方库-包管理器:
2
https://www.bilibili.com/video/BV16P4y1g7MH/?spm_id_from=333.788
现代 CMake 命令行调用:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
cmake --build build --target install
这里的 --build 在Linux中就会调用make,在 Windows 中就会调用 visual studio 的那个 msbuild 去构建。--parallel 4 就代表用 4 个进程并行地构建,并且可以用 --target 来指定要构建的目标,和这个 make install 是一样的。
还有就是现代 CMake 中 public 的应用:

一般比较常用的是 Ninja 这个后端,比较快:
Ninja 还是跨平台的,Windows和Linux上都能用:
但是像 CUDA 在Windows上强制用 MSBuild 而不能用 Ninja。
可以使用 GLOB 来直接加所有的 .cpp 和 .h,但是可能存在删掉或者新增cpp却不被识别的问题,小彭老师推荐加关键字 CONFIGUE_DEPENDS:
不过前面说的现代 CMake 推荐都写下来。
可以用 aux_source_directory 来自动根据当前语言搜索所需要的文件:
同时还有递归地去搜索的关键词 GLOB_RECURSE(能自动包含所有子文件夹下的文件):
但是会把 CMake 构建时build中的测试的临时 cpp 也被加进来:
注:在CMake看来编译和链接是一起的,
默认是 Debug 构建模式:
https://cmake.org/cmake/help/latest/command/project.html
一般会把 CMAKE_CXX_EXTENSIONS 设置为 OFF,表示不需要有那些GCC的特性(夹带私货!否则MSVC可能会通不过)。
这些变量最好设置在 project 之前,这样编译器在对project的时候启用这个语言的时候就会检测一下,就不容易出错。
CMAKE常见变量:https://blog.csdn.net/fuyajun01/article/details/8891749
小彭老师建议卸载Ubuntu,使用Arch Linux.
一个标准的 CMakeLists.txt 模板
cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(MyProjName LANGUAGES C CXX)
if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
message(WARNING "The binary directory of CMake cannot be the same as source directory!")
endif()
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
if (WIN32)
add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()
if (NOT MSVC)
find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
message(STATUS "Found CCache: ${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PROGRAM})
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PROGRAM})
endif()
endif()
链接库文件
小彭老师推荐用静态库,因为动态库在Windows上有很多坑点。
对象库:https://www.scivision.dev/cmake-object-libraries/
在自己的项目里可以都用对象库来组织。对象库的一个优点是,如上面的 mylib 作为一个对象库可以指定不同的编译选项,有时候就会做到。还有一点就是可以绕开编译器和操作系统的各种规则,保证跨平台统一性。
常见坑点:
原因是动态库在内存中的地址是会变化的,他在编译时会指定一个 -fPIC 选项,但是静态库却没有这个选项。静态库不想变换地址而动态库却需要变换地址,因此把一个静态库链到动态库上就会报错:cannot relocate x86_ to loc 之类的错误。
解决方法:要么把静态库变为一个对象库,要么让静态库编译时也生成位置无关的代码(PIC)
不过这样就导致那些本来不需要为 PIC 的那些库也变为 PIC 了。
前面的set(CMAKE_POSITION_INDEPENDENT_CODE ON)
会把那个文件后面所有的静态库都生成 PIC ,因此我们可以用 set_property 来只针对某一个库:
对象的属性
注:经尝试,set_property(TARGET TinyRaytracer PROPERTY WIN32_EXECUTABLE TRUE)
必须要 WinMain 才可以,main 入口是不可以的,会报错。
手动拷贝 dll 太麻烦了,有如下几个解决方法:
链接第三方库
输出与变量
set(myvar hello world)
相当于 set(myvar "hello;world")
,即视作列表。
所以如果不确定的时候最好打上引号。
结论:除非确实需要列表,建议始终在你不确定的地方加上引号,例如: set(sources “main.cpp” “mylib.cpp” “C:/Program Files/a.cpp”) message(“${sources}”)
变量与缓存
CMake 的最大坑点就是缓存了。
清除缓存,其实只需删除 build/CMakeCache.txt 就可以了:rm build/CMakeCache.txt
这个和参数的语法是一样的,就是前面加个 -D 而已。
bool 值还有 TRUE 和 FALSE,但他们是由于历史原因保留的,目前普遍用的是 ON 和 OFF。
如上图,设置 WITH_TBB 是一个 BOOL 类型的缓存变量,那么用户使用的时候就可以这样来设置是否开启:
cmake -B build -DWITH_TBB:BOOL=OFF
CMake 对 BOOL 类型缓存的 set 指令提供了一个简写:option
但是上图的问题是,比如 ccmake(或者Windows的CMake GUI?)会查缓存,如果是不带 CACHE 的这样 set 就不进缓存,于是 ccmake 就看不到。
跨平台与编译器
相当于 gcc -DMY_MACRO=233
虽然名字叫 WIN32,实际上对 32 位 Windows 和 64 位 Windows 都适用。
生成器表达式
https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:PLATFORM_ID
这个生成器表达式就相当于,当满足 PLATFORM_ID:Windows 的时候才会定义后面的 MY_NAME="Bill Gates",如果不满足就会定义为一个空字符串。总之就相当于刚才的那三个指令,且自动做了if判断。
分支与判断
建议同学们始终使用 ON/OFF 避免混淆
变量与作用域
小彭老师小建议
3
推荐的目录组织方式
多套一层名字,防止头文件名重复:
.cmake 文件
find_package
科普:语义版本号(semantic versioning)系统
其他记录
怎么判断64位平台和32位平台
CMAKE_SIZEOF_VOID_P 表示 void* 的大小(例如为 4 或者 8),可以使用其来判断当前构建为 32 位还是 64 位
if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "TinyRaytracer only supports 64-bit platforms")
endif ()
经测试,这一行应该包含在 project 指定语言之后,否则不知道 CMAKE_SIZEOF_VOID_P 是多少上面那个代码就始终会报错。
add_library是纯头文件或者没有源文件
此时不能用 STATIC 啥的,发现会报错:no sources given to target
https://stackoverflow.com/questions/65415872/issue-regarding-cmake-error-no-source-given-to-target
得换成 INTERFACE
CMake 输出路径
https://blog.csdn.net/q610098308/article/details/121157418
CMake warining管理
有时候引入第三方库我不想让它报warning,很烦人,学习如下链接:
https://www.foonathan.net/2018/10/cmake-warnings/
target_compile_options(my_library PRIVATE
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
-Wall>
$<$<CXX_COMPILER_ID:MSVC>:
/W4>)
add_compile_definitions 似乎和变量一样遵从“子不影响父”的关系
add_compile_definitions 似乎和变量一样遵从“子不影响父”的关系?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)