红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【C++】CMake

CMake

使用CMake构建文件

编写CMakeLists

在项目文件夹中创建文件CMakeLists.txt,CMake会根据该文件生成相应的构建指令

1
2
3
4
5
6
7
8
# 指定所需CMake的最小版本号,只需要提供主次版本号
# 该命令必须放在其余命令之前
cmake_minimum_required(VERSION 4.0) 
# 指定项目信息,包括版本号、编程语言等
# 若语言不指定则默认为CXX
project(Test VERSION 0.1.0 LANGUAGE CXX)
# 添加生成的可执行文件,可以指定文件名、需要使用的源文件
add_executable(test main.cpp)

以上三条语句为最基本的CMake指令,接下来就可以生成构建文件

生成构建文件

创建一个文件夹build,并在命令行中cd到当前路径

使用cmake ..来生成构建文件,..提示CMakeLists文件在上级目录中

若未指定构建生成系统,则会使用默认构建生成系统

使用cmake -G列出所有支持的构建系统(称为生成器Generator),其中带星号的是默认生成器

返回上级文件,使用命令

cmake -B build_mingw -G "MinGW Makefiles"

  • -B指定生成的构建文件存放目录
  • -G指定生成器

这样就在文件夹中生成了MinGW的构建文

生成可执行文件

使用其中的mingw32-make命令直接生成可执行文件(.exe)

或直接使用cmake生成可执行文件

cmake --build .

会在当前文件夹下生成可执行文件

若想直接调用VS的编译器生成可执行文件,我们可以首先生成对应的build文件,再返回上级

使用cmake --build ./build生成可执行文件

对于VS这种有多个配置的项目,使用参数--config指定配置

cmake --build ./build --config Release --target helloworld

目标属性

add_executable可以用于添加可执行程序目标,其中第一个参数为目标名称,可以在后面的代码中继续使用

一个目标具有很多属性,构建程序将根据这些属性来生成可执行程序,可执行程序的名称也基于目标名称命名,Linux下名称相同,Windows下会加上.exe拓展名

后面的参数跟着的是一个或多个源文件,头文件也可以加入,不会影响编译,但可以影响IDE的显示

当源文件过多时,我们可以使用target_sources添加多个源文件

本质上是设置了该目标的SOURCES属性,即传给编译器的源文件信息

1
2
3
4
5
6
target_source(demo
	PRIVATE
		main.cpp
		test.cpp
		test.h
)

以下属性均为目标的属性,可以调用对应的target的函数设置

目标属性

添加库

  • 使用add_library用于定义一个库类型的目标,根据设置的属性来创建静态或动态库

  • Linux下,静态库的名称为lib(目标名称).a,MSVC是目标名称.lib

  • 使用target_link_libraries设置程序所需链接库,对应gcc中的-l参数,实质为设置目标的LINK_LIBRARIES属性(PRIVATE参数)或INTERFACE_LINK_LIBRARIES属性(INTERFACE参数)

  • 使用--verbose可以打印出构建细节

变量

定义

设置普通变量:set(变量名 变量值... [父级作用域])

注意:

  • 建议使用字母、数字、_与-

  • 变量名区分大小写

  • CMAKE_开头的变量是CMake中的预定义变量,不要自己定义

  • CMake中所有变量的类型都为字符串

  • 定义便量时,若变量有多个值,变量相当于列表,各项间以分号分割,定义时可以带分号也可以不带,若要正常使用分号需要转义符

  • 若值包含空格,可以用双引号包起来

还可以定义多行变量

定义字符串:使用[[字符串]]来定义字符串,其中的内容不会被替换,两个方括号直接可以有若干个等号,但左右两侧数量需要一致

未定义

使用set(变量名)unset(变量名)将一个变量取消定义

当使用一个未定义变量时,返回一个空串,但与set(myVar "")的空串仍有区别

使用

引用变量:${变量名}

打印:message()

还可以用一个变量定义另一个变量,也可以嵌套引用

1
2
3
4
5
6
set(myVar "hello world")
message("myVar = ${myVar}") # myVar = hello world

set(myVar pet)
set(${myVar} cat)
message("${myVar} = ${${myVar}}") # myVar = "pet"

环境变量

设置:set(ENV{varName} value)

这种方法只会临时添加环境变量,而不会改变系统环境变量

缓存变量

缓存变量存放在CMakeCache.txt中,这意味着每次需要重新编译一遍CMakefile才可以获得变量更新

定义:

1
2
3
4
5
6
7
8
set(
	varName
	value...
	CACHE
	type
	"说明变量用途"
	[FORCE]
)
  • type用于图形界面的处理,不用类型有不同显示方式

    如BOOL以勾选框形式表示

    • 真值表示为on,但true、yes、Y、1也可以表示真值
    • 假值表示为off,但false、no、N、0也可以表示假值
  • [FORCE]表示强制更新缓存,若关闭则只有当缓存中没有这个变量时才会写入,若已经有则不会更新,使用了会更新

对于BOOL类型的缓存变量,可以使用option

option(optVar helpString [初始值]),初始默认为off

若同时定义了同名普通与缓存变量,优先使用普通变量,但要尽量避免重名

命令行参数

  • -D设置缓存变量 cmake -D "MY_VAR:STRING=hello world"
  • -U删除缓存变量 cmake -U "MY*",星号为通配符,以MY开头的都会被删除

作用域

  • 全局:缓存变量
  • 局部:函数、子目录

局部作用域:使用block()endblock()设置

1
2
3
4
5
6
set(myVar cat)
block()
	set(myVar dog)
	message("myVar=${myVar}")
endblock()
message("myVar=${myVar}")
1
2
myVar=dog
myVar=cat

若在set中使用了PARENT_SCOPE,则设置的是外层同名变量

1
2
3
4
5
6
set(myVar cat)
block()
	set(myVar dog PARENT_SCOPE)
	message("[block]myVar=${myVar}")
endblock()
message("[outer]myVar=${myVar}")
1
2
[block]myVar=cat
[outer]myVar=dog

也可以在block中使用PROPAGATE关键字,后面跟着的变量都是上级作用域变量中的引用

1
2
3
4
5
6
set(myVar cat)
block(SCOPE_FOR VARIABLES PROPAGATE myVar)
	set(myVar dog)
	message("[block]myVar=${myVar}")
endblock()
message("[outer]myVar=${myVar}")

常用CMake内置变量

  • PROJECT_NAME :项目名称

  • PROJECT_BINARY_DIR :项目的二进制文件目录,即编译后的可执行文件和库文件的输出目录

  • PROJECT_SOURCE_DIR :项目的源文件目录,即包含CMakeLists.txt文件的目录

  • CMAKE_BINARY_DIR :当前CMake运行的二进制文件目录,通常和PROJECT_BINARY_DIR是同一个目录

  • CMAKE_SOURCE_DIR :当前CMake运行的源文件目录,通常和PROJECT_SOURCE_DIR是同一个目录

  • CMAKE_C_STANDARD :指定C语言的标准版本

  • CMAKE_CXX_STANDARD :指定C++语言的标准版本

  • CMAKE_CXX_FLAGS :指定编译C++代码时使用的编译选项

  • CMAKE_C_FLAGS :指定编译C代码时使用的编译选项

  • CMAKE_EXE_LINKER_FLAGS :指定链接可执行文件时使用的链接选项

  • CMAKE_SYSTEM_NAME :指定当前操作系统名称(如Windows、Linux等)

  • CMAKE_SYSTEM_PROCESSOR :指定当前处理器的类型(如x86、x86_64等)

  • CMAKE_CXX_COMPILER_ID :指定了当前使用的C++编译器,同理可得C的编译器对应的名字。

流程控制命令

条件判断

语法:

1
2
3
4
5
6
7
if(expr)
	# expr为真时执行命令
elseif(expr2)
	# 否则若expr2为真时执行命令
else()
	# 否则执行此处命令
endif()

真值:1、ON、YES、TRUE、Y、非0数值(均不区分大小写)

假值:0、OFF、NO、FLASE、N、IGNORE、NOTFOUND、*-NOTFOUND、“”

若表达式是字符串,则当字符串中内容是真值对应内容才为真,不建议直接使用字符串作为表达式

若表达式是变量,则当变量值不为假值是判断为真,注意单纯变量与${Var}的区别

可以使用逻辑运算符AND、OR或NOT

比较运算符:

比较运算符

文件操作:

文件操作

存在性测试:

预定义变量:有些变量是内置预定义的

1
2
3
4
5
6
7
if(MSVC)
	message("build with MSVC")
else if (MINGW)
	message("build with MINGW")
else()
	message("build with other compiler")
endif()

Option变量:可以使用option设置缓存变量实现条件编译

1
2
3
4
5
6
option(BUILD_MYLIB "构建MyLib目标")
if(BUILD_MYLIB)
	add_library(MyLib mylib.cpp)
else()
	message("忽略构建MyLib目标")
endif()

若在命令行使用-D设置为on

cmake . -DBUILD_MYLIB=in则会编译生成MYLIB库

实例:常用的条件判断代码,放在文件开头,用于防止在CMakeLists文件所在的源路径中进行构建

1
2
3
if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
	message(FATAL_ERROR "错误:禁止源码内编译 请为构建文件创建一个单独的目录")
endif()
  • CMAKE_SOURCE_DIR表示源树顶层路径
  • CMAKE_BINARY_DIR表示生成树顶层路径

foreach

语法:

1
2
3
foreach(<loop_var> <items>)
	<commands>
endforeach()

可以使用IN LISTS与IN ITEMS关键字遍历列表,使用IN ZIP_LISTS合并遍历两个列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foreach(v IN ITEMS 1 2 3 4 5)
	message("v=${v}")
endforeach()

set(list1 2 4 6 8)
set(list2 1 3 5 7)

foreach(v IN LISTS list1)
	message("v=${v}")
endforeach()

foreach(v IN ZIP_LISTS list1 list2)
	message("v=(${v_0},${v_1})")
endforeach()

也可以使用RANGE指定开始结束与步长(默认为1)

foreach(v RANGE ${start} ${end} ${step})

1
2
3
4
5
6
set(start 1)
set(end 8)
set(step 2)
foreach(v RANGE ${start} ${end} ${step})
	message("v=${v}")
endforeach()

while

语法:

1
2
3
while(<condition>)
	<commands>
endwhile()

使用continue()跳过本次循环,使用break()退出循环

函数与宏

定义函数

1
2
3
function(name args...)

endfunction()
  • 第一个参数为函数名
  • 接下来为函数的实际参数

传参方式

  • 命名参数:

    1
    2
    3
    function(f a, b)
    	message("a:${a},b:${b}")
    endfunction()

    函数中可以直接引用这两个参数

  • 未命名参数:

​ 若传参过多,则多出来的参数称为未命名参数

​ 这时使用内置的参数变量可以访问:

​ 参数数量:ARGC

​ 参数列表:ARGV,使用ARGVn表示第n个参数(n大于ARGC时为未定义行为)

​ 未命名参数列表:ARGN

1
2
3
4
5
6
7
8
9
function(f a, b)
	message("a:${a},b:${b}")
	message("argc:${ARGC}")
	message("argv:${ARGV}")
	message("argv0:${ARGV0}, argv1:${ARGV1}")
	message("argn:${ARGN}")
endfunction()

f(1,2,3,4,5)
1
2
3
4
5
a:1,b:2
argc:5
argv:1;2;3;4;5
argv0:1,argv1:2
argn:3;4;5
  • 关键字参数:

    关键字参数有三种类型:

    • 选项关键字:后面没有值,如果传入该关键字则对应参数值为true
    • 单个值关键字:后面只能跟一个值
    • 多值关键字:后面可以跟多个值

​ 关键字和值在参数列表中都是一样的实参,故我们需要进行区分

​ 我们需要使用内置函数cmake_parse_arguments来转换为可用的局部变量,即将关键字作为变量名,关键字后的值作为参数

格式1:

cmake_parse_arguments(<prefix> <options> <one_value_keywords> <multi_value_keyworlds> <args>...)

  • prefix用来生成关键字对应变量,名字为prefix_关键字
  • options即选项表达式
  • one_value_keywords/multi_value_keyworlds分别对应单个值关键字多值关键字
  • args即为要解析的参数列表,通常使用ARGN

1
2
3
4
5
6
7
8
9
10
11
12
13
function(my_functionz targetName)
    set(options USE_MYLIB)
    set(oneValueArgs MYLIB_PATH)
    set(multiValueArgs SOURCES INCLUDES)
    cmake_parse_arguments(arg_myfunction2 "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
    message("

    USE_MYLIB:${arg_myfunction2_USE_MYLIB}
    MYLIB_PATH:${arg_myfunction2_MYLIB_PATH}
    SOURCES: ${arg_myfunction2_SOURCES}
    INCLUDES:${arg_myfunction2_INCLUDES}
    targetName: ${targetName}")
endfunction()

1
2
3
4
5
USE MYLIB: FALSE
MYLIB PATH:/usr/local/lib
SOURCES: main. cpp
INCLUDES:/include;/include2
targetName:hello

还有两个关键字:

  • <prefix>_unparsed_arguments:获得未解析的多余变量
  • <prefix>_keywords_missing_values:获得没有提供值的表达式

格式2:

cmake_parse_arguments(PARSE_ARGV <N> <prefix> <options> <one_value_keywords> <multi_value_keyworlds>)

这种格式解析的是ARGV变量,N表示从第几个变量开始解析

函数返回值

通常使用传参的方式获得返回值

方法有两种:

  • 使用set,在结尾添加PARENT_SCOPE来修改外层变量修改返回值
  • 使用return(PROPAGATE, 传参变量名)也可以修改外层变量

定义:

1
2
3
macro(myMacro arg...)
	# command
endmacro()

区别:函数有自己的栈,宏相当于直接插入代码,参数会被换为字符串

常用命令与生成器表达式

list

语法:list(操作关键字 <listvar> <其他参数>)

例:获得列表长度list(LENGTH listVar, listLENGTH)

​ 连接元素list(JOIN listVar "-" outtstr)

string

语法:list(操作关键字 <list> <其他参数>)

支持修改、替换、查找、子串、正则表达式匹配等字符串

例:查找字符串:string(FIND interesting int fIndex REVERSE)

​ 结果存放fIndex中

math

cmake中,一个普通的数学表达式被当作一个普通的字符串

语法:math(EXPR <variable> "<expr>" [OUTPUT_FORMAT <format>])

math函数会给expr这个表达式求值,并将结果赋值给variable

file

cmake中可以使用file进行文件操作

例:获得源目录中cpp文件列表并添加到目标中(不建议使用)

1
2
3
4
file(GLOB varFilelist LIST_DIRECTORIES false CONFIGURE_DEPENDS RELATIVE ${CMAKE_SOURCE_DIR} "*.cpp")
message("varFilelist: ${varFilelist}")

add_executable(MyApp ${varFilelist})

生成器表达式

格式:$<...>,里面是一些与逻辑有关的表达式

生成器表达式是在构建时才会生成的值

若想要把他们的值输出,可以使用file命令:

file(GENERATE OUTPUT generatorExam.txt CONTENT "platform=$<PLATFORM_ID>" TARGET myApp)

或者是自己提供一个目标

add_custom_target(MyTarget ALL COMMAND ${CMAKE_COMMAND} -E echo "platform = $<PLATFORM_ID>")

常见的生成器表达式:

  • $<PLATFORM_ID>:生成的平台
  • $<CXX_COMPILER_ID>:使用的的编译器
  • $<CXX_COMPILER_VERSION>:编译器的版本号
  • $<condition: trueValue>:若condition为1获得trueValue,为0则得到空串
  • $<IF: condition, trueValue, falseValue>:若condition为1获得前面的值,为0获得后面的值
  • $<BOOL:value>:将变量转换为0或1
  • 逻辑运算:传入的值都需要是0或1
    • $<AND:...>
    • $<OR:...>
    • $<NOT:value>

例:根据编译器不同来配置不同的编译选项

1
2
3
4
5
6
target_compile_options(MyApp PRIVATE
    $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Debug>>:-Og>
    $<$<AND:$<CXX_COMPILER_ID:GNU>,$<CONFIG:Release>>:-O2>
    $<$<AND:$<CXX_COMPILER_ID:MSVC>,$<CONFIG:Debug>>:/Od>
    $<$<AND:$<CXX_COMPILER_ID:MSVC>,$<CONFIG:Release>>:/O2>
)

权限管理

在对项目、库等进行target链接时,可以附带权限控制,权限修饰符包含以下三种

链接时,可以想象成继承

如若链接一个子文件夹中的CMakelists,则设置的权限也会影响到内部

PRIVATE下,只是隐藏了路径对应的符号,但链接是已经链接的,故可以手写一份声明来使用对应的内容

常用命令

project

定义项目名称、版本号和语言

1
project(项目名称)

cmake_minimum_required

指定cmake的最小版本

1
cmake_minimum_required(version 版本号)

add_executable

添加可执行文件,第一个参数为target,可以成为target命令的接收对象

add_library

添加库文件,可以添加静态库或动态库,第一个参数为target,可以成为target命令的接收对象

1
2
add_library(test_lib a.cc b.cc) #默认生成静态库
add_library(test_lib SHARED a.cc b.cc) #默认生成静态库

add_definitions

添加宏定义,只要项目中用该命令定义了宏,则所有源代码都会被定义这个宏

add_subdirectory

添加子项目目录,若有该条语句,就会先去执行子项目的cmake代码

注意:

这样会导致该语句后需要执行后立马生效的语句无效,如include_directories和link_directories如果执行在这条语句后面,则他们添加的目录在子项目中无法生效

而target相关的语句是根据target是否被链接使用来生效的,故作用范围与执行顺序无关

将可执行文件或库文件链接到库文件或可执行文件,第二个参数可以进行权限控制

include_directories

用于指定头文件搜索路径,无法进行权限控制,一旦被执行,后续所有代码都可以搜索到

对应的link_directories是对于库而言的

target_include_directories

用于指定头文件搜索路径,并将搜索路径关联到一个target中,可以设置导出权限

对应的target_link_directories是对于库而言的

aux_source_directory

扫描第一个参数的目录并将所有源文件放到第二个参数定义的变量名中,第一个参数只能是文件夹

1
aux_source_directory(${PROJECT_SOURCE_DIR} SRC)

file

掌握有关文件系统的几乎所有功能

常用于获取文件到变量中

1
file(GLOB SRC "${PROJECT_SOURCE_DIR}/*.cpp")

GLOB会产生一个所有匹配后面表达式的文件组成的列表保存到第二个参数的变量中

若改为GLOB_RECURSE则上述命令会递归的搜寻其子目录所有符合条件的文件,而不仅仅是一层

execute_process

用于执行外部命令

1
2
execute_process(COMMAND git clone https://github.com/<username>/<repository>.git
                WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/<repository>)

如该代码执行了git clone命令,工作目录为${CMAKE_BINARY_DIR}/deps

find_package

查找外界的package,其实是查找对应的<package>Config.cmakeFind<package>.cmake 文件,这些文件里有外界包对应的变量信息以及库和头文件的各种路径信息。

  • CMAKE_PREFIX_PATH 是一个路径列表,CMake 会在这些路径中搜索包的 Config.cmake 文件。

  • <Package>_DIR 变量是指向包的 Config.cmake 文件的路径。如果你手动设置了这个变量,那么 find_package 命令就可以找到包。

include

加载文件或模块

如加载FetchContent模块,该模块可以从代码仓库拉去代码

包管理

FetchContent

直接在cmake中执行指令进行包的拉取,然后通过将下载的包和本地链接就可以使用

vcpkg

以添加OpenCV库为例:

在对应目录终端下输入

1
vcpkg new --application

此时在根目录下生成两个json文件:

  • vcpkg:存放项目所需依赖清单
  • vcpkg-configuration:默认配置文件

接下来使用命令添加依赖项:

1
vcpkg add port opencv

其中port提供一个包的基本信息与生成方法

接下来需要将cmake变量CMAKE_TOOLCHAIN_FILE设置为vcpkg提供的工具链文件,即vcpkg的完整路径

可以创建一个CMakePresets.json文件,在其中设置

设置完毕后进行build,vcpkg就会自动从github上下载相关的库文件