cmake构建工具

本文最后更新于 2025年10月5日 凌晨

[toc]

cmake只是一个工具而已,最本质的那些东西永远是不变的,你要理解你的需求,你要怎么样去设计整个编译框架来满足你的需求,你要用cmake的那些东西来迎合你的设计,你只拿你需要的部分,剩下的那些杂枝细节可以通通扔掉。而不是反过来因为cmake有这个功能,有那个功能,所以设计应该是这样的或者那样的

构建框架 CMake

样例

good
不错的例子

B站讲解视频,还不错

在掌握基本的cmake原理和用法的基础上,研究开源项目的cmake文件是如何编写的。

推荐的开源项目
常见的应用场景:

  • general usage
  • qt usage
  • cuda usage
  • python binding usage

更进一步掌握原理-阅读官方文档
cmake实战指南**非常好

构建(Build)

Cmake比较核心的一些东西就是:

  • 怎么去组织一个项目的编译框架
  • 最终输出目标有哪些(可执行程序,动态库,静态库等等)
  • 怎么为指定的输出目标指定编译参数(需要哪些源文件,需要哪些编译参数)
  • 怎么为指定的输出目标指定链接参数(需要哪些外部库,需要哪些链接参数)
  • 如果存在多个独立输出目标是否有执行先后顺序(比如项目有自动配置工具,用来自动生产一些源文件,那么自动配置工具输出目标就要先于其他目标比如输出可执行程序目标)

CMake一般使用流程

mkdir build
cd build && cmake .. && make

CMake配合脚本一起使用

Win

Linux

通过命令构建项目

1
2
3
4
5
6
#可以实现out of tree build
cmake -G"Unix Makefiles" #指定生成的构建系统类型
-D<var>=<value> #设置cmake变量的值,例如windows下手动指定c++编译器:-DCMAKE_CXX_COMPILER=D:/path/to/bin/g++.exe
<path-to-source> #源树的路径,其下必须包含一个CMakeLists.txt文件


通过命令编译项目

Build a Project

1
2
3
#统一了各平台的编译阶段,等同于在linux平台下执行make window下执行?
cmake --build <dir> [<options>] [-- <build-tool-options>]
#其中 -- 告诉cmake --build命令,后面的参数<build-tool-options>不是cmake --build本身的参数,而是应该传递给底层构建系统(如make)的参数。

核心语法

cmake有全局作用域、目录作用域、函数作用域

  • 全局作用域
    xxx
  • 目录作用域:
    子目录的CMakeLists.txt会将父目录的所有变量拷贝到当前CMakeLists.txt中,当前CMakeLists.txt中的变量的作用域仅在当前子目录有效。
    目录作用域有两个特点:向下有效,值拷贝。
  • 函数作用域:
    xxx

明确作用域:在项目的不同层级清晰地设置和管理作用域,以确保路径设置正确地应用。
深入理解add_subdirectory和include

头文件路径设置问题

CMAKE变量

cmake变量 官方文档

CMAKE_XXX_DIR等内置变量辨析

基础命令1

1
2
3
4
5
6
set(<variable> <value>... [PARENT_SCOPE]) #给变量设置值。   
${} #取值。
$<> #生成器表达式 作用为根据不同配置生成特定内容,例子add_compile_option($<$<COMPILE_LANGUAGE:C>:${flag}>)
string(REPLACE <match-string> <replace-string> <out-var> <input>...) #将<input>中所有出现的<match_string>替换为<replace_string>,并将结果存储在<out-var>中。若<match_string><input>中未出现,<out-var>的值将为<input>
$ENV{VAR} to read environment variable VAR,To test whether an environment variable is defined, use the signature if(DEFINED ENV{<name>})
set(ENV{变量名} 值) 设置环境变量

基础命令2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#-----------------------构建目标------------------------------------
#通过添加不同搜索路径(src src/lib) 在代码include 中可以灵活使用
target_include_directories(myapp PUBLIC /path) #添加头文件搜索目录 局部于当前目标
include_directories(/path) #添加头文件搜索目录 会为当前CMakeLists.txt的所有目标,以及之后添加的所有子目录的目标添加头文件搜索路径
add_link_options(option) #添加链接器选项
link_directories(/path/) #添加库文件的搜索路径
add_executable(MyExe xxx)

#在 CMake 中,你可以指定链接是私有的、公共的还是接口的:
#PRIVATE:仅被链接到当前目标中,不会被导出给第三方。
#PUBLIC:被链接到当前目标中,并且其符号会被导出,供第三方使用。是PRIVATE和INTERFACE的结合
#INTERFACE:不会被链接到当前目标本身,仅应用于链接当前目标的其他目标。

target_link_libraries(myapp <PUBLIC | INTERFACE> hellolib) #添加要链接的库
target_link_libraries(MyExe PRIVATE SomePrefix::LibName)

#使顶层目标依赖于其他顶层目标,以确保它们在目标之前构建。顶层目标是由add_executable(),add_library()或add_custom_target()命令之一创建的。
#这在大型项目中尤其重要,因为编译顺序可能不会遵循CMakeLists.txt中add_subdirectory引入的顺序,导致在构建过程中出现找不到库的错误。
add_dependencies(<target> [<target-dependency>]...) #其中,<target> 是需要指定依赖关系的目标名称,<target-dependency> 是该目标依赖的其他目标名称。

add_library可以生成多种类型的库,包括普通库 对象库 接口库 导入库 别名库。
add_library(<name> INTERFACE) #可以为依赖项指定使用要求,不编译源文件,也不在磁盘生成库文件。通常用作头文件库 或 将一组相关的依赖项组织到一起,sdk 中很多功能模块是以interface库的形式提供。
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL] [<source>...]) #添加一个从source列表列出的文件构建而来的目标名为name的库。name必须全局唯一;
构建库的源文件(.h也可)可以直接指定,**也可以后续使用target_sources()指定**;STATIC(静态库) SHARED(动态库) MODULE(模块库)用来指定库的类型。

target_sources(myapp PUBLIC hello.cc other.cc) #添加要编译的源文件

set_target_properties(target1 PROPERTIES prop1 value1 …)# 设置目标的属性,可以是目标文件的输出名称 目录 版本号
get_target_property()
define_property(<GLOBAL | TARGET> PROPERTY <name>)#定义自定义属性
set_propert(<GLOBAL | TARGET [<target1> ...] > [APPEND] PROPERTY <name> [<value1> ...])
get_property(<variable> <GLOBAL | TARGET > PROPERTY <name>)

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])#用于语句的复用.cmake文件 。CMAKE_MODULE_PATH是cmake用来查找模块的路径列表,使用include()和find_package()时,cmake会在这个路径下搜索对应的模块文件。

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) #添加一个子目录并构建该子目录。source_dir可以是相对也可以是绝对,
相对是相对当前目录的,是必选参数。
# 因为add_subdirectory增加的构建子目录,CMake构建工程会自动将该子目录添加到编译和链接的搜索目录中,以保证整个构建工程能满足依赖,
#这也是为什么使用add_subdirectory后不需要将子文件夹加入到头文件或库文件搜索目录也能搜索到子目录的头文件或库文件。

#------------------------------------------------------
#-------------------源码添加宏定义相关------------------
#-D不是cmake 特有语法,而是c/c++预处理指令,用于在编译时定义宏。该指令被传递给编译器,成为编译器参数的一部分(参考在gcc中传递宏定义)
#将-D定义的标志添加到源文件中
add_definitions(-DMY_MACRO)
add_definitions(-DFOO=${xxx})
#添加预处理定义到源码,使用VAR或VAR=value形式传参,效果和上面的一样,但传参形式不同
add_compile_definitions(<definition> ...)
#添加宏定义到源码 作用域关键字INTERFACE PUBLIC PRIVATE 指定宏定义的作用范围,其中PRIVATE 定义只在当前目标中有效 ,PUBLIC 定义在当前目标及其链接的目标中有效,INTERFACE 定义不在当前目标中使用,但对链接了此目标的其他目标有效。
target_compile_definitions(<target><INTERFACE|PUBLIC|PRIVATE>[item1…])
target_add_definitions(myapp PUBLIC MY_MACRO=1) # 废弃

#--------------------------------------------------------
#------------------添加编译器选项-----------------------
#为后面出现的target(add_executable和add_library)统一添加编译参数。
#使用时注意:一定放在target申明之前,放在后面不生效;添加效果为叠加式;
add_compile_options(-fopenmp)
#一是,在target申明后用,二是,这个命令作用于特定的target,而不是对所有的target生效;添加效果为叠加式
target_compile_options(target PRIVATE "-g")
set(CMAKE_C_FLAGS "-g") #设置CMAKE_C_FLAGS 本质是操作全局变量,通过set命令完成,是否为叠加取决于使用方式,
#set(CMAKE_C_FLAGS "-g") 为非叠加方式, set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")为叠加式,
#这个全局变量在拼接最后的编译命令时最先使用。
#这个变量仅限于C语言,C++对应CMAKE_CXX_FLAGS, 两者分离。

#命令验证方法 可以看compile_commands.json中的命令
#-------------------------------------------------------

#可以用来设置目标的运行时/静态库等的输出目录(另一种方法是通过预定义宏CMAKE_LIBRARY_OUTPUT_DIRECTORY等)
set_target_properties(target1 target2 PROPERTIES prop1 value1 prop2 value2)

控制流

1
2
3
foreach(<loop_var> <items>)
<commands>
endforeach() #<items>是一个item列表,由空格或分号分隔。
1
2
3
字符串比较
STREQUAL 比较字符串是否完全相等
MATCHES 检查字符串是否符合正则表达

函数

1
2
3
function(<name> [<arg1> …])

endfunction()

函数或宏中常用专用变量:

1
2
3
$(ARGC) 函数传递参数个数
$(ARGV) 所有传递的参数
$(ARGN) 函数声明的参数之后的所有参数

进阶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
file(<COPY|INSTALL> <files>... DESTINATION <dir>
[NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS]
[FILE_PERMISSIONS <permissions>...]
[DIRECTORY_PERMISSIONS <permissions>...]
[FOLLOW_SYMLINK_CHAIN]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>] #模式匹配
[EXCLUDE] [PERMISSIONS <permissions>...]] [...])
file(GLOB <variable> [LIST_DIRECTORIES true|false] [RELATIVE <path>] [CONFIGURE_DEPENDS] [<globbing-expressions>...])¶ #生成一个匹配全局表达式的文件列表,并将其存储到变
#将文件中的内容解析成字符串list并存储到变量中,忽略空行和换行
file(STRINGS <filename> <variable> [<options>])
#execute_process 命令用于执行一个外部进程,并可以捕获该进程的输出或状态。它非常强大,允许CMake脚本在配置或构建过程中调用外部工具或脚本。可以在一次调用 execute_process 时执行多个命令。但请注意,每个命令的输出将通过管道传输到下一个命令中
execute_process(COMMAND <cmd1> [<arguments>]
[COMMAND <cmd2> [<arguments>]]...
[WORKING_DIRECTORY <directory>]
[TIMEOUT <seconds>]
[RESULT_VARIABLE <variable>]
[RESULTS_VARIABLE <variable>]
[OUTPUT_VARIABLE <variable>]
[ERROR_VARIABLE <variable>]
[INPUT_FILE <file>]
[OUTPUT_FILE <file>]
[ERROR_FILE <file>]
[OUTPUT_QUIET]
[ERROR_QUIET]
[COMMAND_ECHO <where>]
[OUTPUT_STRIP_TRAILING_WHITESPACE]
[ERROR_STRIP_TRAILING_WHITESPACE]
[ENCODING <name>]
[ECHO_OUTPUT_VARIABLE]
[ECHO_ERROR_VARIABLE]
[COMMAND_ERROR_IS_FATAL <ANY|LAST>])
# add_custom_command(TARGET ...) 提供了一种在构建目标的不同阶段执行自定义命令的
# 机制,常用于需要在构建过程中对目标进行额外处理的场景,例如在构建完成后复制文件
# 、生成文档或执行测试。而 add_custom_command(OUTPUT ...) 则用于生成特定的文件,
# 常用于生成源文件、头文件或其他构建过程中需要的文件。
add_custom_command(
OUTPUT output1 [output2 ...] #cmake会跟踪OUTPUT列出的文件是否存在或比依赖的文件更旧,若不存在或过时则触发该命令重新生成。进一步,允许其他cmake目标通过DEPENDS 或 add_custom_target依赖这些输出文件
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]] #列出该命令依赖的文件或目标。只有当这些依赖项发生变化时,命令才会被重新执行
[BYPRODUCTS [files...]]
[IMPLICIT_DEPENDS <lang1> depend1 [<lang2> depend2] ...]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[DEPFILE depfile]
[JOB_POOL job_pool]
[JOB_SERVER_AWARE <bool>]
[VERBATIM]
[APPEND]
[USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[DEPENDS_EXPLICIT_ONLY]
)
add_custom_command(TARGET <target>
PRE_BUILD | PRE_LINK | POST_BUILD #分别表示编译之前执行命令,链接之前执行命令,生成目标文件后执行命令
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[DEPENDS] [depends ...]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]) #在生成目标文件(add_executable)时自动执行其指定的命令
add_custom_target(Name [ALL] [command1 [args1...]] #ALL 表示该目标会被添加到默认的构建目标
[COMMAND command2 [args2...] ...] #构建时所执行的命令
[DEPENDS depend depend depend ... ] # 指定命令所依赖的文件
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[JOB_SERVER_AWARE <bool>]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]]) #定义一条自定义的构建规则,这条规则可以在构建过程中执行一系列的命令

cmake:添加自定义操作

引入第三方依赖

官方文档-Using Denpendencies Guide
blog: cmake集成第三方库
总结 CMake 添加第三方库依赖方式

1.代码依赖

这种方式是把第三方库的完整代码直接添加到我们的项目中,当做项目代码的一部分进行编译,这种方式会把第三方代码和我们的代码混在一起,并不推荐使用。

2.内部工程依赖(作为子模块)

内部工程依赖会把第三方库的管理职责交给第三方库工程CMakeLists.txt文件,这种方式的好处是职责分明,是最常用的依赖方式。
d92fa87d92c7db0a4e6f07924cce5ea6.png
纯头文件
用法:只需把他的include目录或头文件下载下来,然后
include_directories(xxlib/include) or add_library(mylib INTERFACE)

3.find_library:编译库方式引入

这种方式是用来依赖已经打包好的二进制文件,这种方式也分为静态库(.a、.lib)和动态库(.so、.dll)方式引入,这种方式也可以查找本机已经安装好的库,比如 Android 的 log 库就是通过这种方式引入。
f9368f535240601fdb60fbd872a28ae2.png

4.FetchContent 下载并构建源码

1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.17)
project(fetch_content_example)
include(FetchContent)
#FetchContent_Declare(jsoncpp
# GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp.git
# GIT_TAG 1.9.4)
# 建议使用压缩包的方式依赖,下载速度更快
FetchContent_Declare(jsoncpp
URL https://github.com/open-source-parsers/jsoncpp/archive/1.9.4.tar.gz)
FetchContent_MakeAvailable(jsoncpp)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} jsoncpp_lib)

5.find_package 引用系统中预安装的第三方库

可以通过find_package命令来寻找系统中的包、库
这个包必须是已经安装到系统中。
find_package添加依赖库用法
find_package提供了两种主要的方式来搜索

  1. config mode
    就是你要使用的包已经给你提供了cmake所需的config file(?)
  2. module mode
    有的包不是cmake构建的,没有提供所需的文件,那就需要自己写一个module file——FindSomePackage.cmake 文件来供find_package()命令来使用。FindSomePackage.cmake文件通常放在${CMAKE_CURRENT_SOURCE_DIR}/cmake目录下。官方提供许多已经写好的不同库的 module file。

note:
如果没有module file被提供,cmake将在系统中搜索config file

标准示例:

1
2
3
4
5
6
# Make project-provided Find modules available
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

find_package(SomePackage REQUIRED)
add_executable(MyExe main.cpp)
target_link_libraries(MyExe PRIVATE SomePrefix::LibName)

standard variable name:
Xxx_INCLUDE_DIRS
Xxx_LIBRARIES
Xxx_FOUND
……

6.git submodule

这种方式是利用git的submodule实现,推荐Android使用,通过git添加另外一个仓库的依赖,可更新另外一个仓库的依赖,但是代码不会包含进来。

编译目标文件

静态库 动态库

可执行文件

版本控制 安装和打包

support CMake install and find_package()
知乎 Cmake打包

对于版本控制暂时了解有两种方法:
1.通过configure_file() 将头文件中的宏值用cmakelists中的定义替换
2.KConfig 会生产cmake文件 然后参与构建

If we write find_package(my_library ...), it will go and look for a file named my_library-config.cmake (among others) in a directory named my_library* under the ${CMAKE_INSTALL_PREFIX}/lib (among many others).

用到的变量:
${CMAKE_INSTALL_PREFIX}——>/usr/local under linux,可以通过set另外赋值来指定install的目录。

测试

配置框架

Kconfig
广泛用于U-Boot等项目的开源配置框架,支持组件的参数、使能、模块化等配置,参见Kconfig Language。

mconf

mconf 是 Linux 内核中的一个配置工具,用于配置和编辑内核的配置文件。它通常与 Kconfig 构建系统一起使用。以下是一些关键点:

Kconfig 文件: 内核的配置信息存储在一个名为 Kconfig 的文件中。这个文件包含了内核的各种配置选项,包括驱动、功能和系统参数。

Makefile: mconf 通过读取 Kconfig 文件,并根据用户的选择生成内核的配置文件(通常是 .config 文件)。这个文件将被用于后续的编译过程。

菜单界面: mconf 提供了一个文本菜单界面,允许用户通过键盘导航和选择配置选项。用户可以启用或禁用特定功能、模块或驱动

qconf

qconf 是配置工具,用于配置和管理项目的编译选项,主要用于CMake项目。以下是一些关键特点:

图形界面: qconf 提供了一个图形用户界面(GUI),使用户能够通过可视化的方式设置项目的配置选项,而不必手动编辑配置文件。

Kconfig配置文件(输入): 项目的配置信息通常存储在一个名为 Kconfig 的配置文件中,该文件包含了项目的各种编译选项和设置。

预设配置(输入): qconf 支持预设配置,用户可以在启动时加载特定的配置集,以简化不同构建配置之间的切换。预设配置通常是一些事先定义好的配置选项集,用户可以在启动 qconf 时加载这些预设配置。预设配置可以简化特定构建配置的选择

CMake 集成(输出): qconf 可以生成与 CMake 构建系统兼容的配置文件,通常是 config.cmake。这样,可以使用 CMake 来进行项目的构建。

Makefile集成(输出):根据用户的选择生成配置文件(通常是 .config 文件)。这个文件将被用于后续的编译过程。

1
2
3
4
5
6
7
qconf --fontsize xx
--prefix "xx"
--cmakefile xxx/config.cmake #生成用于CMake
--cfgfile xxx/.config #生成用于Makefile
--loadcfg "xxxx" #加载预设配置
"xxxx/Kconfig" #配置文件

Kconfig

Kconfig 是一种配置语言,广泛用于 Linux 内核和其他一些开源项目中,用于配置和定制软件的构建选项,参见Kconfig Language。

  • 菜单和菜单配置:
1
2
3
4
5
6
7
8
menu "My Configuration Menu"
hidden if M1
config MENU_OPTION1
bool "Option 1"
config MENU_OPTION2
bool "Option 2"
endmenu

MENU_OPTION1 和 MENU_OPTION2 是属于名为 “My Configuration Menu” 的菜单的配置选项。
其中,使用 hidden if 条件表达式,表示当 M1 为真时,显示该菜单,可根据其他配置项来动态控制菜单的显示隐藏。

  • 配置选项:
1
2
3
4
5
6
7
8
9
10
config MY_FEATURE
bool "Enable My Feature"
depends on C1 || C2
default y if (D1 && (C1 || C2))
default n if D1
help_chs
开启
help
Enable this option to include support for My Feature.

MY_FEATURE 是配置选项的标识符,Enable My Feature 是一个用户友好的描述。
depends on

  • 选择块
1
2
3
4
5
6
7
8
9
10
11
choice
prompt "xxxxx"
default D1 if D1_CFG
default D2 if D2_CFG
help
xxxxxxxxxxxxx
config D1
bool "D1"
config D2
bool "D2"
endchoice

choice 是一个 Kconfig 中的关键字,用于定义一组互斥的配置选项,用户可以从中选择一个,可以添加depend on来控制

  • 其他文件引入
1
source "xxxx/Kconfig"

配置框架和编译框架综合

drawio

不同项目有不同的配合方式

rtos系统

PX4

https://blog.csdn.net/one__leaf/category_12140969.html

drawio

ITE

linux系统

ORIN


cmake构建工具
https://leelewin.github.io/p/6794a2a13383499c958246dfcd6296ab/
作者
liminglei
发布于
2022年4月11日
许可协议
BY_NC_SA