在Windows上开发c++相比Linux还是有点不方便,这里介绍CMake,跨平台的构建工具.
Before cmake
cmake可以生成makefiles,很多时候我们也是使用的makefiles,当然它也可以生成vsproj,ninja等构建文件.这里以makefiles为例介绍一下构建系统.
Makefile 用于帮助决定大型程序的哪些部分需要重新编译. 在绝大多数情况下,编译的是 C 或 C++ 文件. 其他语言通常也有自己的工具,其作用与 Make 类似. 除了编译之外,Make 还可以用于需要根据文件变化运行一系列指令的情况.
流行的 C/C++ 替代构建系统有 SCons、CMake、Bazel 和 Ninja一些代码编辑器(如 Microsoft Visual Studio)也有自己的内置构建工具.
Java 有 Ant、Maven 和 Gradle,Go、Rust 和 TypeScript 等其他语言也有自己的构建工具.
Python、Ruby 和单纯的Javascript 等解释型语言不需要类似 Makefile 的工具. Makefile 的目标是根据哪些文件发生了变化,编译哪些需要编译的文件.
但是,当解释型语言中的文件发生变化时,就不需要重新编译了. 程序运行时,将使用文件的最新版本.
syntax
Makefile 由一系列规则组成. 一条规则通常是这样的:1
2
3
4targets: prerequisites
command
command
command
目标(targets)是文件名,用空格隔开.通常情况下,每条规则(rule)只有一个目标.
命令(command)是一系列通常用于创建目标的步骤,需要以制表符而不是空格开头.
先决条件(prerequisites)也是文件名,以空格分隔.在运行目标的命令之前,这些文件必须存在.这些文件也称为依赖项
以 hello world 为例开始:1
2
3hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
有一个名为 hello 的目标,这个目标有两条命令,这个目标没有先决条件,然后运行 make hello.
只要 hello 文件不存在,命令就会运行. 要知道hello 既是目标,也是文件.
这是因为两者直接联系在一起. 通常情况下,当运行目标文件时(也就是运行目标文件的命令时),命令会创建一个与目标文件同名的文件. 在这种情况下,hello 目标不会创建 hello 文件.1
blah: cc blah.c -o blah
运行 make 命令. 由于 make 命令的参数中没有提供目标文件,因此会运行第一个目标文件. 在本例中,只有一个目标(blah)
第一次运行时,blah 将被创建. 第二次运行时,你会看到 make: ‘blah’ 是最新的. 这是因为 blah 文件已经存在. 但有一个问题:如果我们修改了 blah.c,然后运行 make,什么都不会重新编译.
通过添加一个前提条件来解决这个问题:1
blah: blah.c cc blah.c -o blah
当我们再次运行 make 时,会发生以下一系列步骤: 第一个目标被选中,因为第一个目标是默认目标
这个目标的前提条件是 blah.c ,Make 决定它是否应该运行 blah 目标. 只有当 blah 不存在或 blah.c 比 blah 新时,它才会运行
最后一步至关重要,是 make 的精髓所在. 它要做的是判断自上次编译 blah 以来,blah 的先决条件是否发生了变化. 也就是说,如果 blah.c 被修改,运行 make 就应该重新编译该文件. 为了做到这一点,它会使用文件系统的时间戳作为代理来判断是否有改动. 这是一种合理的启发式方法,因为文件时间戳通常只有在文件被修改时才会发生变化. 但必须认识到,情况并非总是如此. 例如,你可以修改一个文件,然后把该文件的修改时间戳改成旧的. 如果你这样做了,Make 就会错误地认为该文件没有改动,因此可以忽略.
more examples
下面的 Makefile 最终会运行所有三个目标.
当你在终端运行 make 时,它会通过一系列步骤编译一个名为 blah 的程序:
Make 选择目标 blah,因为第一个目标是默认目标 blah 需要 blah.o,所以 make 搜索 blah.o
目标 blah.o 需要 blah.c,所以 make 搜索 blah.c
目标 blah.c 没有依赖关系,所以运行 echo 命令.然后运行 cc -c 命令,因为所有 blah.o 的依赖关系都已处理完毕. 然后运行 top cc 命令,因为所有 blah 的依赖关系都已处理完毕.1
2
3
4
5
6
7
8
9blah: blah.o
cc blah.o -o blah # Runs third
blah.o: blah.c
cc -c blah.c -o blah.o # Runs second
# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
echo "int main() { return 0; }" > blah.c # Runs first
如果删除 blah.c,所有三个目标都将重新运行. 如果编辑它(从而将时间戳改为比 blah.o 新),则会运行前两个目标. 如果运行 touch blah.o(从而将时间戳改为比 blah 更新),则只有第一个目标会运行. 如果什么都不改,则所有目标都不会运行1
2
3
4
5
6some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
这个例子, 它将始终运行两个目标,因为 some_file 依赖于 other_file,而 other_file 从未创建.
clean
clean 经常被用作删除其他目标输出的目标,但在 Make 中它并不是一个特殊的词.
请注意,clean 在这里做了两件新事情:它不是第一个目标(默认),也不是先决条件.这意味着除非你明确调用 make clean,否则它永远不会运行. 如果你碰巧有一个名为 clean 的文件,这个目标就不会运行,这不是我们想要的. 如何解决这个问题,使用 .PHONY 😋1
2
3
4some_file:
touch some_file
clean:
rm -f some_file
variables
变量只能是字符串. 通常情况下,您需要使用 :=,但 = 也可以.1
2
3
4
5
6
7
8
9
10
11
12files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
单引号或双引号对 Make 没有任何意义. 它们只是分配给变量的字符. 不过,引号对 shell/bash 很有用,在 printf 等命令中需要用到. 在本例中,两条命令的行为是一样的:1
2
3
4
5a := one two# a is set to the string "one two"
b := 'one two' # Not recommended. b is set to the string "'one two'"
all:
printf '$a'
printf $b
使用 ${} 或 $() 引用变量1
2
3
4
5
6
7x := dude
all:
echo $(x)
echo ${x}
# Bad practice, but works
echo $x
Targets
make多个目标,并希望所有目标都运行?
写一个一个all. 由于这是列出的第一条规则,如果调用 make 时没有指定目标,它将默认运行.1
2
3
4
5
6
7
8
9
10
11all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
当一条规则有多个目标时,将针对每个目标运行命令. $@ 是一个自动变量,包含目标名称.1
2
3
4
5
6
7
8
9all: f1.o f2.o
f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o
automatic variables and wildcards
在 Make 中, 和 % 都被称为通配符,但它们的含义完全不同. 在文件系统中搜索匹配的文件名.
建议始终将其封装在通配符函数中1
2
3# Print out file information about every .c file
print: $(wildcard *.c)
ls -la $?
*可以在目标、先决条件或通配符功能中使用
注意: 不能在变量定义中直接使用 *
注意: 当 * 不能匹配任何文件时,它将保持原样(除非在通配符功能中运行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)
all: one two three four
# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)
# Stays as *.o if there are no files that match this pattern :(
two: *.o
# Works as you would expect! In this case, it does nothing.
three: $(thing_right)
# Same as rule three
four: $(wildcard *.o)
% 非常有用,但由于其使用场合多种多样,所以有些令人困惑. 在 “匹配 “模式下使用时,它会匹配字符串中的一个或多个字符,这种匹配称为词干.
在 “替换 “模式下使用时,它会将匹配到的字符串替换为字符串中的字符串.% 最常用于规则定义和某些特定函数中.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23hey: one two
# Outputs "hey", since this is the target name
echo $@
# Outputs all prerequisites newer than the target
echo $?
# Outputs all prerequisites
echo $^
# Outputs the first prerequisite
echo $<
touch hey
one:
touch one
two:
touch two
clean:
rm -f hey one two
隐式规则
下面是一个隐含规则列表:
- Compiling a C program:
n.o
is made automatically fromn.c
with a command of the form$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
- Compiling a C++ program:
n.o
is made automatically fromn.cc
orn.cpp
with a command of the form$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
- Linking a single object file:
n
is made automatically fromn.o
by running the command$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
这些值默认
CC
: Program for compiling C programs; defaultcc
CXX
: Program for compiling C++ programs; defaultg++
CFLAGS
: Extra flags to give to the C compilerCXXFLAGS
: Extra flags to give to the C++ compilerCPPFLAGS
: Extra flags to give to the C preprocessorLDFLAGS
: Extra flags to give to compilers when they are supposed to invoke the linke
1 | CC = gcc # Flag for implicit rules |
静态模式规则是在 Makefile 中少写代码的另一种方法.1
2targets...: target-pattern: prereq-patterns ...
commands
其本质是通过目标模式(通过 % 通配符)匹配给定目标. 匹配到的内容称为词干. 然后,将词干代入先决条件模式,生成目标的先决条件.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
foo.o: foo.c
$(CC) -c foo.c -o foo.o
bar.o: bar.c
$(CC) -c bar.c -o bar.o
all.o: all.c
$(CC) -c all.c -o all.o
all.c:
echo "int main() { return 0; }" > all.c
# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.
%.c:
touch $@
clean:
rm -f *.c *.o all
更高效的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.
%.c:
touch $@
clean:
rm -f *.c *.o all
过滤器函数可用于静态模式规则,以匹配正确的文件.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"
# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"
%.c %.raw:
touch $@
clean:
rm -f $(src_files)
模式规则经常被使用,但很容易混淆. 你可以从两个方面来看待它们:
- 一种定义自己的隐式规则的方法
- 一种更简单的静态模式规则形式
1 | # Define a pattern rule that compiles every .c file into a .o file |
模式规则在目标中包含一个”%”. 该’%’匹配任何非空字符串,其他字符则自行匹配.
模式规则先决条件中的”%”代表与目标中的”%”匹配的同一词干.
双冒号规则很少使用,但允许为同一目标定义多个规则. 如果这些规则是单冒号,则会打印警告,并且只会运行第二组命令.1
2
3
4
5all: blah
blah::
echo "hello"
blah::
echo "hello again"
在命令前添加 @,以阻止命令被打印
也可以使用 -s 运行 make,在每一行前添加 @.1
2
3all:
@echo "This make line will not be printed"
echo "But this will"
每条命令都在一个新的 shell 中运行1
2
3
4
5
6
7
8
9
10
11all:
cd ..
# The cd above does not affect this line, because each command is effectively run in a new shell
echo `pwd`
# This cd command affects the next because they are on the same line
cd ..;echo `pwd`
# Same as above
cd ..; \
echo `pwd`
修改默认shell/bin/sh
1
2
3
4SHELL=/bin/bash
cool:
echo "Hello from bash"
如果希望字符串带有美元符号,可以使用 $$. 这就是在 bash 或 sh 中使用 shell 变量的方法,请注意 Makefile 变量和 Shell 变量之间的区别.1
2
3
4
5
6
7make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var
# Same as running "echo I am a make variable" in the shell
echo $(make_var)
在运行 make 时添加 -k,即使出现错误也能继续运行. 在命令前添加 - 可以抑制错误
在 make 中添加 -i 可以让每条命令都出错.
要递归调用 makefile,请使用特殊的 $(MAKE) 而不是 make,因为它会为你传递 make 标志,而自身不会受其影响.1
2
3
4
5
6
7
8new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)
clean:
rm -rf subdir
当 Make 启动时,它会自动从执行时设置的所有环境变量中创建 Make 变量.1
2
3
4
5
6
7# Run this with "export shell_env_var='I am an environment variable'; make"
all:
# Print out the Shell variable
echo $$shell_env_var
# Print out the Make variable
echo $(shell_env_var)
export指令使用一个变量,并将其设置为所有 shell 命令的环境:1
2
3
4
5shell_env_var=Shell env var, created inside of Make
export shell_env_var
all:
echo $(shell_env_var)
echo $$shell_env_var
设置一个目标导出变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"
cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)
clean:
rm -rf subdir
变量有两种类型:
递归(使用 =)—只在使用命令时查找变量,而不是在定义变量时查找.
简单扩展(使用 :=)—就像普通的命令式编程—只扩展目前已定义的变量.1
2
3
4
5
6
7
8
9
10# Recursive variable. This will print "later" below
one = one ${later_variable}
# Simply expanded variable. This will not print "later" below
two := two ${later_variable}
later_variable = later
all:
echo $(one)
echo $(two)
?=
only sets variables if they have not yet been set1
2
3
4
5
6
7one = hello
one ?= will not be set
two ?= will be set
all:
echo $(one)
echo $(two)
行尾的空格不会被删除,但行首的空格会被删除. 要使用单空格创建变量,请使用 $(nullstring)1
2
3
4
5
6
7
8
9with_spaces = hello # with_spaces has many spaces after "hello"
after = $(with_spaces)there
nullstring =
space = $(nullstring) # Make a variable with a single space.
all:
echo "$(after)"
echo start"$(space)"end
未定义变量实际上是一个空字符串
使用+=
添加1
2
3
4
5foo := start
foo += more
all:
echo $(foo)
使用 override 可以覆盖命令行变量.1
2
3
4
5
6
7# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
echo $(option_one)
echo $(option_two)
定义指令并不是一个函数,尽管它看起来像. define/endef 只是创建一个变量,并将其设置为一系列命令. 请注意,这与在命令之间使用分号有点不同,因为每个命令都会在单独的 shell 中运行.1
2
3
4
5
6
7
8
9
10
11
12one = export blah="I was set!"; echo $$blah
define two
export blah="I was set!"
echo $$blah
endef
all:
@echo "This prints 'I was set'"
@$(one)
@echo "This does not print 'I was set' because each command runs in a separate shell"
@$(two)
可为特定目标设置变量,也可以为特定目标模式设置变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16all: one = cool
all:
echo one is defined: $(one)
other:
echo one is nothing: $(one)
%.c: one = cool
blah.c:
echo one is defined: $(one)
other:
echo one is nothing: $(one)
条件
1 | foo = ok |
检查变量是否为空1
2
3
4
5
6
7
8
9
10nullstring =
foo = $(nullstring) # end of line; there is a space here
all:
ifeq ($(strip $(foo)),)
echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
echo "nullstring doesn't even have spaces"
endif
ifdef 不会扩展变量引用;它只是查看是否定义了某个变量1
2
3
4
5
6
7
8
9
10bar =
foo = $(bar)
all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif
查看make标志,使用 findstring 和 MAKEFLAGS 测试 make 标志. 使用 make -i 运行此示例,即可看到它打印出 echo 语句.1
2
3
4
5all:
# Search for the "-i" flag. MAKEFLAGS is just a list of single characters, one per flag. So look for "i" in this case.
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif
函数
函数主要用于文本处理. 使用 $(fn, arguments) 或 ${fn, arguments} 调用函数. Make 有大量内置函数.1
2
3bar := ${subst not,"totally", "I am not superman"}
all:
@echo $(bar)
函数 subst 将not替换为totally.1
comma := , empty:= space := $(empty) $(empty) foo := a b c bar := $(subst $(space), $(comma) , $(foo)) # Watch out! all: # Output is ", a , b , c". Notice the spaces introduced @echo $(bar)
此外还有patsubst,foreach,if,call,shell,filter等函数,
$(patsubst pattern,replacement,text)
还有一种只替换后缀的速记方法:$(text:suffix=replacement).注意:不要为这种速记方法添加额外的空格.它会被视为搜索或替换词.1
2
3
4
5
6
7
8
9
10
11foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)
all:
echo $(one)
echo $(two)
echo $(three)
$(foreach var,list,text)
它将一个单词列表(用空格分隔)转换为另一个单词列表.
var 设置为列表中的每个单词,text 则为每个单词展开.1
2
3
4
5
6
7foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)
all:
# Output is "who! are! you!"
@echo $(bar)
if 检查第一个参数是否为非空. 如果是,则运行第二个参数,否则运行第三个参数1
2
3
4
5
6
7foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)
all:
@echo $(foo)
@echo $(bar)
m ake支持创建基本函数,只需通过创建一个变量来 “定义 “函数,但要使用 (0)、(1) 等参数. 然后使用特殊的调用内置函数调用该函数. 语法是 (call variable,param,param)
. (0)是变量,(1)、(2)等是参数.1
2
3
4
5sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)
all:
# Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
@echo $(call sweet_new_fn, go, tigers)
filter
函数用于从列表中选择符合特定模式的某些元素. 例如,这将选择 obj_files 中以 .o 结尾的所有元素.1
2
3
4
5
6
7
8
9# call function
all:
@echo $(shell ls -la) # Very ugly because the newlines are gone!
obj_files = foo.result bar.o lose.o
filtered_files = $(filter %.o,$(obj_files))
all:
@echo $(filtered_files)
include 指令告诉 make 读取一个或多个其他 makefile. 它是 makefile 中的一行
这在使用 -M 等编译器标志时特别有用,这些标志会根据源代码创建 Makefile. 例如,如果某些 c 文件包含一个头文件,那么该头文件就会被添加到由 gcc 编写的 Makefile 中.
使用 vpath 指定存在某些先决条件集的位置. 其格式为 vpath \ 在目标中添加 .PHONY 可以防止 Make 将假目标与文件名混淆. 在本例中,如果创建了 clean 文件,仍将运行 make clean. “phony”目标的名称通常很少是文件名,在实践中很多人都会跳过这一点 如果命令返回非零的退出状态,make 工具将停止运行规则(并返回先决条件) 如果规则以这种方式失败,DELETE_ON_ERROR 将删除规则的目标. 这将发生在所有目标上,而不仅仅是像 PHONY 这样的目标. 尽管出于历史原因,make 并没有这样做,但最好还是一直使用这个功能. 在Windows上可选择的构建后端有vs,codeblocks这种软件的文件结构,或者单纯的Makefiles以及Ninja.相当于忽略了几个项目构建的差异. 编译生成项目的目录 顶层目录 分别用来重新定义最终结果的存放目录 设置C/ C++编译选项, 用来控制默认的库编译方式,如果不进行设置,使用ADD_LIBRARY 并没有指定库类型的情况下,默认编译生成的库都是静态库. 此外还有一些系统信息 使用$ENV{}调用系统变量. 将源码source构建成一个库, 供他人使用 使用给定的源文件,为工程引入一个可执行文件.引入一个名为< name>的可执行目标,该目标会由调用该命令时在源文件列表中指定的源文件来构建 将给定目录 dir1 dir2 加给编译器搜索到的包含文件 .默认情况下,加到目录列表的最后,target_include_directories可以指定针对目标文件添加头文件目录. 该指令的作用为将目标文件与库文件进行链接. 上述指令中的 可以使用 find_path和find_library分别用来找头文件和库.找到之后可以使用include_directory或者target_link_libraries用来使用. set设置变量,后续使用${}使用变量 if elseif else endif 文件中可以使用条件,循环等控制语句.可以用来判断构建时系统的一些环境. 比如 添加一个子目录并构建该子目录.source_dir指定源CMakeLists.txt和代码文件所在的目录. 一般用在嵌套的项目中,顶层CMakeLists.txt文件添加子目录,让子目录先构建完成之后添加其中生成的库和头文件. 使用正则匹配响应文件并存到一个变量中 发现一个目录下所有的源代码文件并将列表存储在一个变量中. 使用source_group增加文件 并添加到生成目标中 可以在cmake命令中指定该值. 而add_definition用于指定编译器参数,比如 比如下面文件,使用add_definition定义了TEST_DEBUG,option定义为OFF并在cmake执行时指定为on,然后在cmake文件中指定option为on,这样就执行了 CMake中的命令特别多,事实上并不需要去一个一个记住,通常只要知道一个项目的大致构建流程以及可能需要的命令就行了. 生成器表达式是在构建的配置阶段进行的语句.大多数函数允许使用生成器表达式,只有少数例外.以 $ 的形式使用,其中 OPERATOR 或直接使用,或与 VALUE 进行比 较. 若 CXX_COMPILER_ID 变量匹配 GNU, Clang, AppleClang 列表中任意一个,则附加-Wall 选 项到目标——也就是 my_target.生成器表达式在编写独立于平台和编译器的 CMake 文件时非常方便. 除了查询值,生成器表达式还可以用于转换字符串和列表: 这将输出“cmake”. cmake在c++中用的太多了, 最近在看别人项目的时候看见了一些比较高级的cmake使用方法. macro 定义一个名为 \ CMake 宏使用 macro()/endmacro() 定义,有点像函数function.不同的是,函数参数是真变量,而 在宏中是字符串替换.这意味着必须使用大括号访问宏的所有参数. 另一个区别是,通过调用函数,作用区域转移到函数内部.执行宏时,就好像宏的主体粘贴到 调用位置一样,宏不会创建变量和控制流的作用域.因此,避免在宏中调用 return(). function 函数由 function()/endfunction() 定义.函数为变量创建了一个新的作用域,因此所有 在内部定义的变量都不能从外部访问,除非将 PARENT_SCOPE 选项传递给 set(). 函数不区分大小写,通过 function 后的名称加上圆括号来使用函数 使用CMake版本大于3.0.0 将CMake视为代码,像对待任何其他编程语言一样,保持CMakeLists.txt和模块的代码清晰、结构良好. 全局定义项目属性,在顶层CMakeLists.txt文件中定义如编译器警告、代码标准等全局项目属性,确保所有目标使用相同的标准,避免因依赖目标间的编译选项不一致导致的问题 例如,一个项目可能会使用一组通用的编译器警告. 在顶层 CMakeLists.txt 文件中全局定义此类属性,可避免因依赖目标使用更严格的编译器选项而导致依赖目标的公共头文件无法编译的情况. 全局定义此类项目属性可以更方便地管理项目及其所有目标. 直接操作目标(targets),不要使用 不要直接操作CMAKE_CXX_FLAGS,不同的编译器有不同的命令行参数格式.通过CMAKE_CXX_FLAGS设置C++标准(如 合理使用usage requirements,例如,不要将 使用声明导出目标的现代查找模块. 从 CMake 3.4 开始,越来越多的查找模块导出了可通过 target_link_libraries 使用的目标. 使用外部软件包的导出目标. 不要重蹈 CMake 使用外部软件包定义的变量的覆辙. 而应通过 target_link_libraries 使用导出的目标. 对于不支持客户端使用 CMake 的第三方库,请使用查找模块. CMake 为第三方库提供了一系列查找模块. 例如,Boost 不支持 CMake. 相反,CMake 提供了一个查找模块,以便在 CMake 中使用 Boost. 如果某个库不支持客户端使用 CMake,请将其作为 bug 报告给第三方库作者. 如果该库是一个开源项目,请考虑发送补丁. CMake 在业界占主导地位. 如果库作者不支持 CMake,那就麻烦了. 为不支持客户端使用 CMake 的第三方库编写查找模块. 可以改造查找模块,将目标正确导出到不支持 CMake 的外部软件包. 如果您是库作者,请导出库的INTERFACE 避免在项目命令参数中使用自定义变量. 保持简单. 不要引入不必要的自定义变量. 不要使用 add_library(a ${MY_HEADERS} ${MY_SOURCES}),而应使用 add_library(a b.h b.cpp). 不要在项目中使用 file(GLOB),CMake 是一个编译系统生成器,而不是一个编译系统. 在生成构建系统时,它会将 GLOB 表达式求值为一个文件列表. 然后,联编系统会对该文件列表进行操作. CMake 无法将 GLOB 表达式转发给联编系统,以便在联编时对表达式进行评估. CMake 希望成为所支持的联编系统的公分母. 并非所有的构建系统都支持这一点,因此 CMake 也无法支持. 从目标和属性的角度思考 通过从目标的角度定义属性(即编译定义、编译选项、编译特性、包含目录和库依赖关系,compile definitions, compile options, compile features, include directories, and library dependencies),可以帮助开发人员在目标级别对系统进行推理. 开发人员不需要了解整个系统,就能对单个目标进行推理. 构建系统可处理反向性. 将目标想象成对象 调用成员函数会修改对象的成员变量. 与构造函数类比:add_executable add_library 与成员变量类比:target properties(太多,这里不一一列举) 与成员函数类比:target_compile_definitions target_compile_features target_compile_options target_include_directories target_link_libraries target_sources get_target_property set_target_property 如果目标需要内部属性(如编译定义、编译选项、编译特性、包含目录和库依赖关系),请将它们添加到 target_* 命令的 PRIVATE 部分. 使用 target_compile_definitions 声明编译定义 可将编译定义与目标的可见性(PRIVATE、PUBLIC、INTERFACE)关联起来. 这比使用 add_compile_definitions 更好,因为 add_compile_definitions 与目标没有关联. 使用 target_compile_options 声明编译选项 这与编译选项与目标的可见性(PRIVATE、PUBLIC、INTERFACE)相关联. 这比使用 add_compile_options 要好,因为 add_compile_options 与目标没有关联. 但要注意不要声明会影响 ABI 的编译选项. 请全局声明这些选项. target_compile_features同 使用 target_include_directories 声明 include 目录 这将 include 目录与目标的可见性(PRIVATE、PUBLIC、INTERFACE)相关联. 这比使用 include_directories 更好,因为 include_directories 与目标没有关联. 使用 target_link_libraries 声明直接依赖关系,这将把使用要求从依赖目标传播到被依赖目标. 该命令还能解决传递依赖关系. 不要在组件目录之外的路径下使用 target_include_directories 在组件目录之外使用路径是一种隐藏的依赖关系. 相反,应使用 target_include_directories 通过 target_link_directories 将包含目录作为使用要求传播给依赖目标. 使用 target_* 时,始终明确声明属性 PUBLIC、PRIVATE 或 INTERFACE. 明确声明可减少无意中引入隐藏依赖关系的机会. 不要使用 target_compile_options 设置会影响 ABI 的选项. 对多个目标使用不同的编译选项可能会影响 ABI 兼容性. 防止此类问题的最简单解决方案是全局定义编译选项. 使用在同一 CMake 树中定义的库应与使用外部库相同. 在同一 CMake 树中定义的软件包可直接访问. 通过 CMAKE_PREFIX_PATH 获取预编译库. 如果软件包定义在同一个编译树中,那么使用 find_package 查找该软件包就不会有任何问题. 在将目标 Bar 导出到命名空间 Foo 时,还可以通过 add_library(Foo::Bar ALIAS Bar) 创建别名 Foo::Bar. 创建一个变量,列出所有子项目. 定义 find_package 宏来封装原来的 find_package 命令(现在可通过 _find_package 访问). 如果变量包含软件包的名称,宏将禁止调用 _find_package. 除了基于目录的作用域外,CMake 函数也有自己的作用域. 这意味着在函数中设置的变量在父作用域中不可见. 宏则不然. 宏只能用于定义很小的功能,或用于封装有输出参数的命令. 函数有自己的作用域,宏没有. 宏的参数不会被设置为变量,而是在执行宏之前在宏中解析对参数的引用. 这可能会在使用未引用变量时导致意外行为. 一般来说,这个问题并不常见,因为它需要使用名称在父作用域中重叠的非参引变量,但必须注意,因为它可能导致微妙的错误. 不要使用会影响目录树中所有目标的宏,如 include_directories、add_definitions 或 link_libraries. 这些宏是邪恶的. 如果在顶层使用宏,所有目标都可以使用宏定义的属性. 例如,所有目标都可以使用(即 #include)include_directories 所定义的头文件. 如果目标不需要链接(如接口库、内联模板),在这种情况下甚至不会出现编译器错误. 使用这些宏很容易意外地通过其他目标创建隐藏的依赖关系. 建议使用 cmake_parse_arguments 来处理任何函数中基于参数的复杂行为或可选参数. 使用 CPack 创建软件包. CPack 是 CMake 的一部分,并与 CMake 完美集成. 编写 CPackConfig.cmake,其中包括 CMake 生成的 CPackConfig.cmake,这样就可以设置无需出现在项目中的其他变量. 使用工具链文件进行交叉编译.工具链文件封装了用于交叉编译的工具链. 保持工具链文件的简洁,这样更易于理解和使用. 不要在工具链文件中加入逻辑. 为每个平台创建一个工具链文件. 正确对待构建错误、修复错误、拒绝拉取请求、暂缓发布. 将警告视为错误 要将警告视为错误,切勿向编译器传递 -Werror. 如果这样做,编译器就会将警告视为错误. You cannot 将新的警告视为错误 使用多个支持的分析器:clang-tidy (\ 对于每个头文件,都必须有一个关联的源文件,该源文件 #includes 头文件在顶部,即使该源文件本来是空的. 大多数分析工具都会报告当前源文件和关联头文件的诊断结果. 没有关联源文件的头文件不会被分析. 您也许可以设置自定义头文件过滤器,但这样头文件可能会被分析多次. CMake 中存在两个逻辑文件夹.一个是源文件夹,包含项目的层次结构集;另一个是构建文件夹,包含构建指令、缓存,以及所有生成的二进制文件和工件. 源文件夹是 CMakeLists.txt 文件所在的位置.构建文件夹可以放在源文件夹中,也可以将其放在另一个位置.两种方式都可以; 构建文件夹通命名为 build,但也可以使用其他名称,包括不同平台的前缀和后缀.当在源 代码树中使用构建文件夹时,最好将其添加到.gitignore 中. 当配置 CMake 项目时,将在构建文件夹中重新创建源文件夹的项目和文件夹结构,以便所有构 建工件都位于相同的位置.每个文件夹中,都有一个名为 CMakeFiles 的子文件夹,其中包含 CMake 配置步骤生成的信息. CMake 项目的文件结构会映射到 build 文件夹中.每个包含 CMakeLists.txt 文件的文件夹将进行映射,将创建一个名为 CMakeFiles 的子文件夹,其中包含用 于构建的信息 变量的作用域可以通过以下方式确定: • 函数作用域: 在函数内部设置的变量只在函数内部可见. • 目录作用域: 源树中的每个子目录绑定变量,并包括来自父目录的变量. • 持久缓存: 缓存的变量可以是系统的,也可以是用户定义的.在多次运行中保持它们的值不变. 将 PARENT_SCOPE 选项传递给 set() 会使变量在父作用域中可见 cmake-variables(7) — CMake 3.30.1 Documentation 最小的项目结构中有三个文件夹和一个文件 • build: 放置构建文件和二进制文件的文件夹. • include/project_name: 此 文 件 夹 包 含 从 项 目 外 部 公 开 访 问 的 所 有 头 文 件, 包 含 使它更容易看出头文件来自哪个库. • src: 此文件夹包含所有私有的源文件和头文件 • CMakeLists.txt: 这是主 CMake 文件 构建文件夹可以放置在任何地方,放在项目根目录最方便,但强烈建议不要选择任何非空文件 夹作为构建文件夹.特别是将构建好的文件放入 include 或 src 中,这是一种糟糕的实践.其他文件 夹,如 test 或 doc,在组织测试项目和文档页面时就很方便 嵌套项目 第一行,cmake_minimum_required(VERSION 3.21),期望看到的 CMake 的版本,以及 CMake 将启用哪些特性.本书的例子中都使用 CMake 3.21,但是出于兼容性的原因,读者们可以选 择一个较低的版本. 对于本例,3.1 版本将是绝对的最小值,因为在此之前,target_sources 不可用.将 cmake_minimum_required 指令放在每个 CMakeLists.txt 文件的顶部是一个很好的做法. 接下来,使用 project() 指令设置项目.第一个参数是项目的名称——我们的例子中为 “hello_world_standalone”. 接下来,版本设置为 1.0.下面是一个简短的描述和主页的 URL. 最后,LANGUAGES CXX 属 性指定正在构建一个 C++ 项目.除了项目名称之外,所有参数都可选. 调用 add_executable(hello_world) 指令,会创建一个名为 hello_world 的目标.这也将 是可执行的文件名. 现在已经创建了目标,使用 target_sources 完成了向目标添加 C++ 源文件.Chapter3 是目标名,在 add_executable 中指定. PRIVATE 定义源仅用于构建此目标,而不用于依赖的目 标.在范围说明符之后,有一个相对于当前 CMakeLists.txt 文件路径的源文件列表.如果需要,当 前处理的 CMakeLists txt 文件的位置可以通过 CMAKE_CURRENT_SOURCE_DIR 得到. 源码可以直接添加到 add_executable,也可以单独使用 target_sources,将它们与 target_sources 一起添加.通过使用 PRIVATE、PUBLIC 或 INTERFACE,可以显式地定义在何 处使用源码.但是,指定 PRIVATE 以外的内容只对库目标有意义. 源文件使用 PRIVATE 添加,PRIVATE 和 PUBLIC 关键字指定在何处使用源代码进行编译.PRIVATE 指定的源文件将只在目标 hello 中使用 若使用 PUBLIC,那么源文件也会将附加到 hello 和依赖 hello 的目标上,这通常不是想要的结果. INTERFACE 关键字说明源文件不会添加到 hello 目标中,而是会添加到依赖到 hello 的目标上. 通常,为目标指定为 PRIVATE 的内容都可以视为构建需求.最后,包含目录使用 target_include_directories 设置.该指令指定的文件夹内的所有文件都可以使用 #include (带尖括号) 来访问 当使用 add_library() 创建库时,库名称在项目中必须全局唯一.默认情况下,库 的实际文件名是根据平台上的约定构造的,例如 lib.在 Linux 上为 .a,在 Windows 上为 < 名称 >.lib.通过设置目标的 OUTPUT_NAME 属性,可以更改文件的名称 动态库的常用命名约定是在文件名中添加版本以指定构建版本和 API 版本,通过指定 VERSION 和 SOVERSION 属性,CMake 将在构建和安装库时创建必要的文件名和符号链接 项目中经常看到的另一种约定,是为各种构建配置的文件名添加不同的后缀.CMake 通过设 置 CMAKE__POSTFIX 全局变量或添加 _POSTFIX 属性来处理这个问题.若 设置了此变量,后缀将自动添加到非可执行目标. 这将使库文件和符号链接命名为 libhellod 要链接到动态库,链接器必须知道哪些符号可以从库外部使用.这些符号可以是类、函数、类 型等,使它们可见的过程称为导出. 指定符号可见性时,编译器有不同的方式和默认行为,这使得以独立于平台的方式 指定符号可见性有点麻烦. 从默认的编译器可见性开始;gcc 和 clang 假设所有的符号都 是可见的,而 Visual Studio 编译器默认情况下会隐藏所有的符号,除非显式导出. 设置 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS,可以改变 MSVC 的默认行为,这是一种暴力的 解决方法,只有当库的所有符号都应该导出时才能使用 暴露库的内部符号会暴露本应隐藏的东西,所以设置动态库时最好更改默认可见性并暴露需要的类、函数. 要更改符号的默认可见性,请将 _VISIBILITY_PRESET 属性设置为 HIDDEN.此 属性可以全局设置,也可以针对单个库目标设置. 会替换为编写库的语言,例如:CXX 替 换为 C++,C 替换为 C. 若所有要导出的符号都是隐藏符号,必须在代码中特别标记.最常见的方法是指定一个预处理器定义来确定一个符号是否可见 CMake 提供了 generate_export_header 宏,由 GenerateExportHeader 模块导入.下面的例子中,hello 库的符号默认设置为隐藏.然后,通过使用 generate_export_header 宏再次单独启用.另外,本例将 VISIBILITY_INLINES_HIDDEN 属性设置为 TRUE,通过隐藏内联 类成员函数进一步减少导出的符号表 纯头文件的库有点特殊,因为不需要编译; 相反,可以导出它们的头文件,以便直接包含在其 他库中. 大多数情况下,头文件库的工作方式与普通库类似,但是头文件使用 INTERFACE,而非 PUBLIC. 由于仅包含头文件的库不需要编译,因此不会向目标添加源文件. 有时,可能想要分离代码,以便部分代码可以重用,而不需要创建完整的库.当想在可执行测 试和单元测试中使用某些代码时,通常的做法是不需要重新编译所有代码两次.为此,CMake 提 供了对象库,其中的源代码是编译的,但不进行归档或链接.通过 自 CMake 3.12 起, 这 些 对 象 可 以 像 普 通 库 一 样 使 用, 只 需 将 它 们 添 加 到 target_link_libraries 函数中.3.12 版本之前,对象库需要添加生成器表达式,也就 是 $.这将在生成构建系统期间扩展为一个对象列表.这种方式现 在还可以用,但不推荐这样做,因为这很快就变得不可维护,特别是在一个项目中有多个对象库的 情况下. 可 以 把 add_library 放 在 同 一 个 CMakeLists.txt 文 件 中, 或 者 使 用 add_subdirectory 将其整合起来.两者都是有效的选项,并取决于项目的设置方式 target_link_libraries 的目标也可以是另一个库.同样,库的链接说明符,可以是以下 任意一个: • PRIVATE: 用于链接库,但不是公共接口的一部分.只有在构建目标时才需要链接库. • INTERFACE: 没有链接到库,但是公共接口的一部分.当在其他地方使用目标时,链接库是必需的.这通常仅限头文件库时使用. • PUBLIC: 链接到库,是公共接口的一部分.因此,该库既是构建依赖项,也是使用依赖项. C++ 编译器有很多选项来设置一些常见的标志,从外部设置预处理器定义也是一种常见的做 法.CMake 中,这些是使用 target_compile_options 传递,使用 target_link_options 更改链接器行为,但编译器和链接器可能有不同的设置标志的方法.例如,在 GCC 和 Clang 中,选 项用减号 (-) 传递,而 Microsoft 编译器将斜杠 (/) 作为选项的前缀.但是通过生成器表达式,可以很容易地在 CMake 中处理这个问题: $$:/SomeOption 是 一 个 嵌 套 的 生 成 器 表 达 式, 由 内 而 外 求 值. 生 成 器 表 达 式 在 生 成 阶 段 进 行 计 算. 首 先, 当 C++ 编 译 器 等 于 MSVC 时为 true.若是这种情况,那么外部表达式将返回/SomeOption, 然后传递给编译器.若内部表达式的计算结果为 false,则不传递. $<$:-fopenmp> 的工作原理类似,但不是只检查 单个值,而是传递一个包含 GNU,Clang,AppleClang 的列表.若 CXX_COMPILER_ID 匹配其中 任何一个,内部表达式计算为 true,someOption 会传递给编译器. 将编译器或链接器选项传递为 PRIVATE,将其标记为与库接口不需要的此目标的构建需求. 若 使用 PUBLIC,那么编译选项也成为构建需求,所有依赖于原始目标的目标将使用相同的编译选项. 将编译器选项暴露给依赖的目标是需要谨慎做的事情. 若编译器选项只用于使用目标而不用于构建目标,则可以使用关键字 INTERFACE.在构建纯头文件库时,这是最常见的情况. 编 译 器 选 项 的 特 殊 情 况 是 编 译 定 义, 其 会 传 递 给 底 层 程 序. 这 通 过 target_compile_definitions 进行传递. 要查看所有编译选项,可以查看生成的构建文件,例如 Makefiles 或 Visual Studio 项目.更方便 的方法是让 CMake 将所有编译命令导出为 JSON. 通 过 使 用 CMAKE_EXPORT_COMPILE_COMMANDS, 将 生 成 一 个 名 为 compile_commands.json 的文件,其包含用于编译的完整命令. 库别名是在不创建新的构建目标的情况下引用库的一种方法,有时称为命名空间.常见的模式 是为从项目中安装的每个库以 MyProject::library 的形式创建一个库别名,可以用于对多个目标进行 语义分组.有助于避免命名方面的冲突,特别是当项目包含公共目标时,比如名为 utils 的库、helper 和类似的库. 构建信息可以存储在 CMakePresets.json 文件中,放在项目的根目录中.此外,每个用户都可以将他们配置添加到 CMakeUserPresets.json 文 件中.基本预设通常置于版本控制之下 要查看项目中配置了哪些预设值,请运行 cmake —list-presets 查看可用预设值的列表. 要使用预设进行生成,请执行 cmake —build —preset name install() 指令 install(…) 指令是一个内置的 CMake 命令,允许生成用于安装目标、文件、目录等的构建系统说明.CMake 不会生成安装指令,除非明确地说明.因此,安装总是在开发者的控制中. 要使 CMake 目标可安装,必须用至少一个参数指定 TARGETS 参数.指令的签名如下所示: TARGETS 参数表示 install 可以接受一组 CMake 目标,以便为其生成安装代码,只安装目 标的输出构件.最常见的目标输出工件定义如下: • ARCHIVE (静态库、DLL 导入库和链接器导入文件): – 除了在 macOS 中标记为 FRAMEWORK 的目标 • LIBRARY (动态库): – 除了在 macOS 中标记为 FRAMEWORK 的目标 – 除了 dll (Windows) • RUNTIME (可执行文件和 dll): – 除了在 macOS 中标记为 MACOSX_BUNDLE 的目标 这些目录的默认值由 CMake 提供,具体取决于目标类 型.提供默认安装路径信息的 CMake 模块称为 GNUInstallDirs 模块.GNUInstallDirs 模块定义了各 种 CMAKEINSTALL的路径.默认安装目录如下表所示: 覆盖内置默认值,在 install(…) 指令中需要使用 DESTINATION 参数. 多一个 DIRECTORY 参数,这是使静态库的头文 件可安装.这样做的原因是 CMake 不会安装任何非输出工件,而 STATIC 库目标只生成一个二进制 文件作为输出工件.头文件不是输出工件,应该单独安装 安装的东西并不总是目标输出构件的一部分.它们可能是目标的运行时依赖项,例如图片、源文件、脚本和配置文件 install(FILES…) 指令接受一个或多个文件作为参数,还需要 TYPE 或 DESTINATION 参 数,这两个参数用于确定指定文件的目标目录. TYPE 用于指示哪些文件将使用该文件类型的默认路径作为安装目录,可以通过设置相关的 GNUInstallDirs 变量来重写默认值 install(DIRECTORY…) 指令用于安装目录,目录的结构将按原样复制到目标.目录既可以 作为一个整体安装,也可以有选择地安装. DESTINATION 这个参数允许 install(…) 指定安装目录,目录可以是相对路径,也可以是绝对路径.相对路径是相对于 CMAKE_INSTALL_PREFIX 的,建议使用相对路径使安装可重定位. 另外,使用相对路径进行打包也很重要,因为 cpack 要求安装路径是相对路径.最好使用以相关 GNUInstallDirs 变量开始的路径,以便包维护人员在需要时覆盖安装目标. DESTINATION 参数可以与 TARGETS, FILES,IMPORTED_RUNTIME_ARTIFACTS,EXPORT 和 DIRECTORY 安装类型一起使用. PERMISSIONS 此 参 数 允 许 在 支 持 的 平 台 上 更 改 已 安 装 文 件 的 权 限. 可 用 权 限 为:OWNER_READ、 OWNER_WRITE、OWNER_EXECUTE、GROUP_READ、GROUP_WRITE、GROUP_EXECUTE、 WORLD_READ、WORLD_WRITE、WORLD_EXECUTE、SETUID 和 SETGID.PERMISSIONS 参 数可以与 TARGETS, FILES, IMPORTED_RUNTIME_ARTIFACTS, EXPORT 和 DIRECTORY 安装 类型一起使用. CONFIGURATIONS 允许指定构建配置时限制参数的应用. OPTIONAL 此参数使要安装的文件为可选文件,因此当文件不存在时,安装不会失败.可选参数可以与 TARGETS, FILES, IMPORTED_RUNTIME_ARTIFACTS 和 DIRECTORY 安装类型一起使用. 若交付一个库,也必须很容易导入到一个项目——特别是 CMake 项目. CMake 使用依赖的首选方式是通过包.包为基于 CMake 的构建系统传递依赖信息,包可以是 Config-file 包、Find-module 包或 pkg-config 包.所有的包类型都可以通过 find_package() 找到并使用 有 两 种 类 型 的 配 置 文 件 —— 包 配 置 文 件 和 可 选 的 包 版 本 文 件, 两 个 文 件 都 必 须 有 一 个 特 定 的 命 名.比如分别为Config.cmake,ConfigVersion.cmake 包 配 置 文 件的内容可能如下,主要设置包含头文件和库: 搜索包时,find_package(…) 会查找 ${CMAKE_PREFIX_PATH}$/cmake 目录. include(GNUInstallDirs) 用 于 包 含 GNUInstallDirs 模 块. 这 提 供 了 CMAKE_INSTALL_INCLUDEDIR 变 量, set(ch4_ex05_lib_INSTALL_CMAKEDIR…) 是一个用户定义的变量,是导出目标的安装路径. 上面设置好了目标包含头文件,,因为在将目标导入到另一个 项目时,不存在构建时包含路径,其使用生成器表达式来区分构建时包含目录和安装时包含目录 EXPORT 参 数, 用 于 从 给 定 的 install(…) 目 标 创 建 一 个 导 出 名 称. 然 后, 可 以 使 用 此 导 出 名 称 导 出 这 些 目 标. 使 用 INCLUDES DESTINATION 参 数 指 定 的 路 径, 将 用 于 填 充 导 出 目 标 的 INTERFACE_INCLUDE_DIRECTORIES 属 性, 并 自 动 使 用 安 装 前 缀 路 径 作 为 前 缀(就是安装头文件,因为其不会被显示安装或导入) EXPORT 参数接受现有的导出名 称来进行导出,它引用的是 ch4_ex05_lib_export 导出名称,在之前的 install(TARGETS…) 中创建的. FILE 用于确定导出的文件名,并设置为 ch4_ex05_lib-config.cmake. NAMESPACE 用于 为所有导出的目标添加命名空间前缀.这允许将所有导出的目标连接到通用的命名空间下,并避免 与具有相似目标名称的包发生冲突. 最后,DESTINATION 确定生成导出文件的安装路径.设置为 ${ch4_ex05_lib_INSTALL_CMAKEDIR} 以便 find_package() 发现它 要实现对 find_package(…) 的完全支持,还需要生成包版本文件. 使用 include(CMakePackageConfigHelpers),导入 CMakePackageConfigHelpers 模块.这个模块 提供了 write_basic_package_version_file(…) 函数,用于根据给定的参数自动生成包版本文件 简单来说,通过生成包配置和包版本文件,方便find_package导入. 而包配置文件需要通过install(export )安装,包版本文件利用write_basic_package_version_file从项目版本获取导出版本,匹配find_package中的版本 如何使用 CMake 的打包工具 CPack 默认随 CMake 安装一起提供,可以利用现有的 CMake 代码来生成特定于平台的安装和包 CPack 使用 CPackConfig.cmake 中的配置细 节,CPackSourceConfig.Cmake 文件生成包.这些文件可以手动填写,也可以由 CMake 在 CPack 模块的帮助下自动生成. 包含 CPack 模块会生成 CPackConfig.cmake 和 CPackSourceConfig.cmake 文件,这是打包项目所需的 CPack 配置 当 CMake 或用户正确设置了 CPack 配置文 件,就可以使用 CPack.CPack 模块允许定制包装过程,从而可以设置大量的 CPack 变量.这些变 量分为两组——普通变量和生成器特定变量.公共变量影响所有包生成器,而生成器特定的变量只 影响特定类型的生成器 项目配置完成后,CpackConfig.cmake 和 CpackConfigSource.cmake 文件将生成到 build/CPack* 目录下 参数—config 是 CPack 命令的主要输入.参数-B 修改了默认的包目录,CPack 将把它的工 件写入该目录 CMake 能够查找定义目标、包括路径和包特定变量的整个包.更多细节请参考 CMake 项目部 分中的库. 有五个 find_…指令,它们选项和行为非常相似: • find_file: 定位单个文件. • find_path: 查找包含特定文件的目录. • find_library: 查找库文件. • find_program: 查找可执行程序. • find_package: 查找完整的包 find_pacakge find_package 有两个签名: 一个基本签名或短签名,一个完整签名或长签名.通常, 使用短签名就足以找到正在寻找的包,因为它更容易维护,应该是首选.短格式同时支持模块和配置包,而长格式只支持配置模式 • -config.cmake • Config.cmake • -config-version.cmake (若指定了版本详细信息) • ConfigVersion.cmake (若指定了版本详细信息) find_package搜索的目录顺序 配置模式下搜索路径如下 编写查找模块(如果想使用的库没有cmake) 目前仍然有很多库没有使用 CMake 管理,或者不导出 CMake 包.若可以将它们安装在系统的默认位置,找到这些库通常也不是问题.当使用仅为某个项目所需的专有第三方库,或者使用从系 统包管理器安装的库构建的不同版本的库时,使用对应版本的库就成了件麻烦事. find 模块如何找到必要的头文件和二进制文件,以及为 CMake 创建导入目标的指令.当使用 find_package 时,CMake 在 CMAKE_MODULE_PATH 中搜索名为 Find\ 处理逻辑是首先在CMAKE_MODULE_PATH中添加.cmake,然后添加library和path,然后判断目标有没有,如果没有就构建库,并设置库的属性. 其中利用了find_package_handle_standard_args 检查传递的LIBRARY 和 INCLUDE_DIR 变量是否有效,从而会设置 _FOUND 变量. Conan Conan 最强大的特性是可以为多个平台、配置和版本创建和管理二进制包.创建包时,使用 conanfile.py 文件描述它们,该文件列出了所有依赖项、源和构建指令. 用 CMake 使用 Conan 的方法是使用 CMake 本身的 Conan,若不想这样做,可以在外部使用 Conan,但建议在使用 Conan 之前使用 find_program 检查 Conan 程序是否存在 CMake与Conan搭配集成很好,直接在CMakeLists.txt中包含conan.cmake(没有直接下载),然后include并使用conan的一系列命令进行安装. vcpkg 流行的开源包管理器是微软的 vcpkg,工作方式类似于 Conan,使用客户机/服务器架构. 最初构建它是为了与 Visual Studio 编译器环境一起工作,后来添加了 CMake. 当以清单模式运行时,项目的依赖项在 vcpkg.json 中定义,文件在项目的根目录下,清单模式 有一个很大的优势,可以更好地与 CMake 集成,因此请尽可能使用清单模式 若以经典模式运行, 则必须在运行 CMake 之前手动安装这些包,当传递 vcpkg 工具链文件时,可以使用 find_package 和 target_link_libraries 使用已安装的包, 设置工具链文件可能会在交叉编译时导致问题,因为 CMAKE_TOOLCHAIN_FILE 可能已经 指向一个不同的文件,所以第二个工具链文件可以通过 VCPKG_CHAINLOAD_TOOLCHAIN_FILE 传递 主要是要让cmake知道包管理器安装了哪些库并指定cmake安装哪些库,可以通过 对于构建的外部项目,使用 FetchContent 模块是添加源依赖项的一种方法.对于二进制依 赖,使用 find_package的方式仍是首选. 下载并将第三方软件的副本集成到产品中的做法,称为供应商方式.优点是常常使构建软件 变得容易,但在打包库方面产生了问题 FetchContent FetchContent 提供了一系列函数来拉取源依赖项,主要是 FetchContent_Declare,它定 义了下载和构建 FetchContent_MakeAvailable 的参数,FetchContent_MakeAvailable 填充依赖项的目标,并使它们可用于构建 Doxygen 是一个非常流行的 C++ 项目文档软件,允许从代码生成文档.,Doxygen 要求注释采用预定义的一组格式, 还需要一个 Doxyfile,其包含文档生成的所有参数,比如输出格式、排除的文件模式、 项目名称等,因为配置参数太多,开始配置 Doxygen 可能会让人望而生畏,但 CMake 可以自动生 成 Doxyfile. 使用 doxygen_add_docs(…) 来生成文档,该函数将生成一个名为 targetName 的自定义目标 参数列表 filesOrDirs, 包含生成文档的代码的文件或目录的列表 ALL 参数用于使 CMake 的 ALL 元目标依赖于 doxygen_add_docs(…) 创建的文档目标,因此在构建 ALL 元目标时自动生成文档 COMMENT 参数用于让 CMake 在构建目标时输出一条消息 CPack打包文档 通过 CTest,CMake 可以以内置的方法来执行测试,设置 enable_testing(),并使用 add_test() 添加了测试的 CMake 项目都支持运行测试, enable_testing() 将在当前目录和其子目录中启用,并添加测试, 因此在使用 add_subdirectory 前,通常将其设置在顶层的 CMakeLists.txt 中,若使用 include(CTest),CMake 的 CTest 模块会自动设置 enable_testing, 除非 BUILD_TESTING 为 OFF, 根据 BUILD_TESTING 选项禁用构建和运行测试也是一种很好的实践, 这里的常用模式是将项目中与测试相关的所有部分放在其子文件夹中,并且只在 BUILD_TESTING 设置为 ON 时包含子 文件夹, CTest 将使用有关测试的信息并执行它们,通过独立运行 ctest 或作为 CMake 构建步骤的一部 分,来执行测试,以下两个命令都可执行测试: 有选择地执行测试的另一种方法是使用 LABELS 属性进行标记,然后使用 CTest 的-L选项选 择要运行的标签,一个测试可以有多个用分号分隔的标签 -L 命令行选项接受一个正则表达式来过滤标签 更好的测试库比如使用googleTest. 此外还有代码静态检查(包括格式化,语法检查等)、覆盖率以及代码消杀等工具,但其实其中一些功能已经由其他程序代替了(比如IDE和一些编辑器),这里不做描述 构建时执行自定义任务 定义一个自定义的目标,该目标本身不是一个可执行文件或库,而是一个独立的任务,比如清理操作、生成文档等, add_custom_command 在构建目标时可能需要执行外部任务,CMake 中可以使用 add_custom_command 来 实现这一点,它有两个签名,一个用于将命令与现有目标挂钩,而另一个用于生成文件, 它可以用来生成源文件或执行其他任务,希望自定义任务产生特定的输出文件,这可以通过定义自定义目标,并在目标之间设置 必要的依赖项来实现 主要用于对目标进行读写,或是生成一些文件 配置时执行自定义任务 生成在构建前需要的信息,或者 需要更新文件以重新运行 CMake,另一个情况是在配置步骤中生成 CMakeLists.txt 文件或其他输入 文件,也可以通过 configure_file 实现 构建软件时,常见的任务是在构建前将一些文件复制到特定位置或进行修改,解决方案是使用 configure_file 指令,可以将文件从一个位置复制到 另一个位置, configure_file 将\ 文件复制到\1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17vpath %.h ../headers ../other-directory
# Note: vpath allows blah.h to be found even though blah.h is never in the current directory
some_binary: ../headers blah.h
touch some_binary
../headers:
mkdir ../headers
# We call the target blah.h instead of ../headers/blah.h, because that's the prereq that some_binary is looking for
# Typically, blah.h would already exist and you wouldn't need this.
blah.h:
touch ../headers/blah.h
clean:
rm -rf ../headers
rm -f some_binary1
2
3
4
5
6
7
8some_file:
touch some_file
touch clean
clean:
rm -f some_file
rm -f clean1
2
3
4
5
6
7
8
9
10.DELETE_ON_ERROR:
all: one two
one:
touch one
false
two:
touch two
falsecmake fundamental
常用变量
PROJECT_BINARY_DIR
PROJECT_SOURCE_DIR
EXECUTABLE_OUTPUT_PATH
以及LIBRARY_OUTPUT_PATH
CMAKE_ARCHIVE_OUTPUT_DIRECTORY
:默认存放静态库的文件夹位置;CMAKE_LIBRARY_OUTPUT_DIRECTORY
:默认存放动态库的文件夹位置;LIBRARY_OUTPUT_PATH
:默认存放库文件的位置,如果产生的是静态库并且没有指定 CMAKE_ARCHIVE_OUTPUT_DIRECTORY 则存放在该目录下,动态库也类似;CMAKE_RUNTIME_OUTPUT_DIRECTORY
:存放可执行软件的目录;CMAKE_CXX_FLAGS
和CMAKE_C_FLAGS
CMAKE_C_COMPILER
设置对应编译器路径.BUILD_SHARED_LIBS
指定生成程序
1
2
3add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])[STATIC | SHARED | MODULE]
:类型有三种1
2add_executable(< name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL] source1 source2 … sourceN)添加头文件目录和库
1
2include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
target_include_directories()1
2target_link_libraries(<target> [item1] [item2] [...]
[[debug|optimized|general] <item>] ...)<target>
是指通过add_executable()和add_library()指令生成已经创建的目标文件.<a>_FOUND
检查是否通过find加载成功,之后使用target_link_libraries连接.find_package&find_path&find_library
1
2FIND_PATH(myCeres NAMES ceress.h PATHS /ceres/include/ceres NO_DEFAULT_PATH)
INCLUDE_DIRECTORIES(${myCeres})编译时消息输出
1
MESSAGE(STATUS "HELLO")
设置变量
1
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/x86)
控制结构
1
2
3
4
5
6
7
8# include dynamic link path
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86")
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../nwpu_std_msgs/lib/x86)
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../nwpucutils/lib/x86)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../nwpu_std_msgs/lib/arm)
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../nwpucutils/lib/arm)
endif()添加其他子目录
1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
获取文件
1
FILE (GLOB ALL_SOURCES "*.cpp" "*.c" "./AFolder/*.cpp" )
1
aux_source_directory(dir VAR)
vs中显示头文件
1
2file(GLOB_RECURSE pipe_header_files ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h )
source_group("Header Files" FILES ${pipe_header_files}) 1
add_library( lib_pipe_shared SHARED ${pipe_src} ${pipe_header_files})
option与add_definitions
1
option(<variable> "<help_text>" [value])
add_definitions("-Wall -g")
,此外更推荐使用add_compile_definitions将预处理器定义添加到编译器命令行,使用add_compile_options命令添加其它选项.add_definitions(-DTEST_DEBUG)
,定义了该宏.1
2
3
4
5
cmake -DTEST_DEBUG=ON .
cmake --build .1
2
3
4
5
6project(test)
option(TEST_DEBUG "option for debug" OFF)
if (TEST_DEBUG)
add_definitions(-DTEST_DEBUG)
endif()1
2
3
4
5
...1
2
3
4target_compile_definitions(foo PUBLIC FOO)
target_compile_definitions(foo PUBLIC -DFOO) # -D removed
target_compile_definitions(foo PUBLIC "" FOO) # "" ignored
target_compile_definitions(foo PUBLIC -D FOO) # -D becomes "", then ignored生成器表示
1
2
3target_compile_options(my_target PRIVATE
"$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall>")1
$<LOWER_CASE:CMake>
Advanced cmake
Effective CMake
通用
add_compile_options
, include_directories
, link_directories
, link_libraries
等命令,因为它们作用于目录级别,可能引入隐含的依赖关系.相反,直接对特定的目标使用target_compile_options
, target_include_directories
, target_link_directories
, target_link_libraries
等命令.-std=c++14
)可能会在未来版本的编译器上失效,因为这些要求也可能在其他标准(如C++17)中满足,而且老编译器的选项可能不同.因此,应该告诉CMake所需的编译特性,让CMake根据具体情况选择适当的编译器选项.-Wall
(开启所有警告)添加到目标的PUBLIC或INTERFACE段的target_compile_options
中,因为这不是构建依赖目标所必需的,这样做可能导致不必要的警告信息.模块Modules
项目Projects
目标(Targets)及其属性(Properties)
函数与宏
循环
foreach(var IN ITEMS foo bar baz) ...
foreach(var IN LISTS my_list) ...
foreach(var IN LISTS my_list ITEMS foo bar baz) ...
包
交叉编译
警告与报错
-Werror
unless you already reached zero warnings.[[deprecated]]
.[[deprecated]]
your internal code as long as it is still used. But once it is no longer used, you can as well just remove it.[[deprecated]]
.实战
项目结构
1
2
3
4├── CMakeLists.txt
├── build
├── include/project_name
└── src1
2
3
4
5
6
7
8
9├── CMakeLists.txt
├── build
├── include/project_name
├── src
└── subproject
├── CMakeLists.txt
├── include
│ └── subproject
└── src1
2
3
4
5
6
7
8
9
10
11cmake_minimum_required(VERSION 3.21)
project(
hello_world_standalone
VERSION 1.0
DESCRIPTION"A simple C++ project"
HOMEPAGE_URL https://github.com/PacktPublishing/CMake-BestPractices
LANGUAGES CXX
)
add_executable(hello_world)
target_sources(hello_world PRIVATE src/main.cpp)创建库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17cmake_minimum_required(VERSION 3.21)
project(
ch3.hello_lib
VERSION 1.0
DESCRIPTION
"A simple C++ project to demonstrate creating executables
and libraries in CMake"
LANGUAGES CXX)
add_library(hello)
target_sources(
hello
PRIVATE src/hello.cpp src/internal.cpp)
target_compile_features(hello PUBLIC cxx_std_17)
target_include_directories(
hello
PRIVATE src/hello
PUBLIC include)命名库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18add_library(ch3_hello)
set_target_properties(
ch3_hello
PROPERTIES OUTPUT_NAME hello
)
set_target_properties(
hello
PROPERTIES VERSION ${PROJECT_VERSION} # Contains 1.2.3
SOVERSION ${PROJECT_VERSION_MAJOR} # Contains only 1
)
set_target_properties(
hello
PROPERTIES DEBUG_POSTFIX d)
set_target_properties(
hello
PROPERTIES DEBUG_POSTFIX d)动态库的符号可见性
更改默认可见性
1
2
3
4
5
6add_library(hello SHARED)
set_property(TARGET hello PROPERTY CXX_VISIBILITY_PRESET "hidden")
set_property(TARGET hello PROPERTY VISIBILITY_INLINES_HIDDEN TRUE)
include(GenerateExportHeader)
generate_export_header(hello EXPORT_FILE_NAME export/hello/ export_hello.hpp)
target_include_directories(hello PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/export")接口和纯头文件库
1
2
3
4
5
6
7
8project(
ch3_hello_header_only
VERSION 1.0
DESCRIPTION "Chapter 3 header-only example"
LANGUAGES CXX)
add_library(hello_header_only INTERFACE)
target_include_directories(hello_header_only INTERFACE include/)
target_compile_features(hello_header_only INTERFACE cxx_std_17)对象库
add_library(MyLibrary object)
创建对象库.使用库
1
2
3
4
5
6
7add_subdirectory(hello_lib)
add_subdirectory(hello_header_only)
add_subdirectory(hello_object)
add_executable(chapter3)
target_sources(chapter3 PRIVATE src/main.cpp)
target_link_libraries(chapter3 PRIVATE hello_header_only hello
hello_object)设置编译器和链接器选项
1
2
3
4
5
6target_link_options(
hello
PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/SomeOption>
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-
someOption>
)调试编译器选项
库别名
1
2
3add_library(Chapter3::hello ALIAS hello)
...
target_link_libraries(SomeLibrary PRIVATE Chapter3::hello)使用预设值维护构建配置
1
2
3
4
5
6
7
8
9
10
11
12{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [...],
"buildPresets": [...],
"testPresets": [...]
}打包、部署和安装
1
install(TARGETS ... [...])
1
2
3install(TARGETS ch4_ex01_executable
RUNTIME DESTINATION qbin
)1
2
3
4
5
6
7
8
9
10add_library(ch4_ex02_static STATIC)
target_sources(ch4_ex02_static PRIVATE src/lib.cpp)
target_include_directories(ch4_ex02_static PUBLIC include)
target_compile_features(ch4_ex02_static PRIVATE cxx_std_11)
include(GNUInstallDirs) # 引入模块使得修改默认安装位置
install(TARGETS ch4_ex02_static)
install (
DIRECTORY include/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)安装文件
1
2
3
4
5install(FILES "${CMAKE_CURRENT_LIST_DIR}/chapter4_greeter_content"
DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(PROGRAMS "${CMAKE_CURRENT_LIST_DIR}/chapter4_greeter.py"
DESTINATION "${CMAKE_INSTALL_BINDIR}" RENAME chapter4_greeter)安装目录
1
install(DIRECTORY dir1 dir2 dir3 TYPE LOCALSTATE)
1
2
3
4
5
6
7
8include(GNUInstallDirs)
install(DIRECTORY dir1 DESTINATION ${CMAKE_INSTALL_
LOCALSTATEDIR} FILES_MATCHING PATTERN "*.x")
install(DIRECTORY dir2 DESTINATION ${CMAKE_INSTALL_
LOCALSTATEDIR} FILES_MATCHING PATTERN "*.hpp"
EXCLUDE PATTERN "*")
install(DIRECTORY dir3 DESTINATION ${CMAKE_INSTALL_
LOCALSTATEDIR} PATTERN "bin" EXCLUDE)提供项目配置信息
1
2set(Foo_INCLUDE_DIRS ${PREFIX}/include/foo-1.2)
set(Foo_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)1
2
3include(GNUInstallDirs)
set(ch4_ex05_lib_INSTALL_CMAKEDIR cmake CACHE PATH
"Installation directory for config-file package cmake files")1
2
3
4
5/*省略 前面add_library*/
target_include_directories(ch4_ex05_lib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_compile_features(ch4_ex05_lib PUBLIC cxx_std_11)1
2
3
4
5
6
7
8install(TARGETS ch4_ex05_lib
EXPORT ch4_ex05_lib_export
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install (
DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # 头文件安装目录
)1
2
3
4
5install(EXPORT ch4_ex05_lib_export
FILE ch4_ex05_lib-config.cmake
NAMESPACE ch4_ex05_lib::
DESTINATION ${ch4_ex05_lib_INSTALL_CMAKEDIR}
)1
2
3
4
5
6
7
8
9
10
11
12include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"ch4_ex05_lib-config-version.cmake"
# Package compatibility strategy. SameMajorVersion is
essentially 'semantic versioning'.
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/ch4_ex05_lib-config-version.
cmake"
DESTINATION "${ch4_ex05_lib_INSTALL_CMAKEDIR}"
)1
2
3
4
5
6
7
8
9
10if(NOT PROJECT_IS_TOP_LEVEL)
message(FATAL_ERROR "The chapter-4, ex05_consumer project is
intended to be a standalone, top-level project. Do not
include this directory.")
endif()
find_package(ch4_ex05_lib 1 CONFIG REQUIRED) # 找到包配置或包版本文件,设置包含头文件和库
add_executable(ch4_ex05_consumer src/main.cpp)
target_compile_features(ch4_ex05_consumer PRIVATE cxx_std_11)
target_link_libraries(ch4_ex05_consumer ch4_ex05_lib::ch4_ex05_
lib) # 链接库1
2
3
4
5
int main(void){
chapter4::ex05::greeter g;
g.greet();
}创建安装包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18cmake_minimum_required(VERSION 3.21)
project(
ch4_ex06_pack
VERSION 1.0
DESCRIPTION "Chapter 4 Example 06, Packaging with CPack"
LANGUAGES CXX)
if(NOT PROJECT_IS_TOP_LEVEL)
message(FATAL_ERROR "The chapter-4, ex06_pack project is
intended to be a standalone, top-level project.
Do not include this directory.")
endif()
add_subdirectory(executable)
add_subdirectory(library)
set(CPACK_PACKAGE_VENDOR "CTT Authors")
set(CPACK_GENERATOR "DEB;RPM;TBZ2")
set(CPACK_THREADS 0)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "CTT Authors")
include(CPack)1
cmake –S . -B build/
1
2cmake --build build/
cpack --config build/CPackConfig.cmake -B build/寻找包和文件
1
find_package(PackageName [version] [EXACT | QUIET | NO_MODULE | NO_CMAKE_PATH | NO_CMAKE_ENVIRONMENT | NO_SYSTEM_ENVIRONMENT | NO_CMAKE_SYSTEM_PATH | CMAKE_FIND_ROOT_PATH_BOTH | ONLY_CMAKE_FIND_ROOT_PATH] [REQUIRED | OPTIONAL] [COMPONENTS component1 component2 ...] [CONFIG | MODULE])
1
2
3
4find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])1
2
3find_package(OpenSSL REQUIRED COMPONENTS SSL)
add_executable(find_package_example)
target_link_libraries(find_package_example PRIVATE OpenSSL::SSL)1
2
3
4
5
6
7
8
9
10
11
12<prefix>/
<prefix>/(cmake|CMake)/
<prefix>/<packageName>*/
<prefix>/<packageName>*/(cmake|CMake)/
<prefix>/(lib/<arch>|lib*|share)/cmake/<packageName>*/
<prefix>/(lib/<arch>|lib*|share)/<packageName>*/
<prefix>/(lib/<arch>|lib*|share)/<packageName>*/(cmake|CMake)/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/cmake/
<packageName>*/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/<packageName>*/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/<packageName>*/
(cmake|CMake)/1
2
3
4
5
6
7├── dep <-- The folder where we locally keep dependencies
├── cmake
│ └── FindLibImagePipeline.cmake <-- This is what we need to write
├── CMakeLists.txt <-- Main CmakeLists.txt
├── src
│ ├── *.cpp files包管理器
1
2Cmake -S <source_dir> -D <binary_dir> -DCMAKE_TOOLCHAIN_
FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake1
2
3cmake -S <source_dir> -D <binary_dir> -DCMAKE_TOOLCHAIN_
FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake -DVCPKG
_CHAINLOAD_TOOLCHAIN_FILE=/path/to/other/toolchain.cmakeCMakePresets.json
进行配置获取依赖项源代码
1
2
3
4
5
6
7
8
9
10
11include(FetchContent)
FetchContent_Declare(
bertrand
GIT_REPOSITORY https://github.com/bernedom/bertrand.git
GIT_TAG 0.0.17)
FetchContent_MakeAvailable(bertrand)
add_executable(fetch_content_example)
target_link_libraries(
fetch_content_example
PRIVATE bertrand::bertrand
)自动生成文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15find_package(Doxygen)
set(DOXYGEN_OUTPUT_DIRECTORY"${CMAKE_CURRENT_BINARY_DIR}/docs")
set(DOXYGEN_GENERATE_HTML YES)
set(DOXYGEN_GENERATE_MAN YES)
set(DOXYGEN_MARKDOWN_SUPPORT YES)
set(DOXYGEN_AUTOLINK_SUPPORT YES)
set(DOXYGEN_HAVE_DOT YES)
set(DOXYGEN_COLLABORATION_GRAPH YES)
set(DOXYGEN_CLASS_GRAPH YES)
set(DOXYGEN_UML_LOOK YES)
set(DOXYGEN_DOT_UML_DETAILS YES)
set(DOXYGEN_DOT_WRAP_THRESHOLD 100)
set(DOXYGEN_CALL_GRAPH YES)
set(DOXYGEN_QUIET YES)1
2
3
4
5
6
7doxygen_add_docs(
ch6_ex01_doxdocgen_generate_docs
"${CMAKE_CURRENT_LIST_DIR}"
ALL
COMMENT "Generating documentation for Chapter 6 - Example
01 with Doxygen"
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function(pro_BuildDoc doxyfilein)
if(TARGET Doxygen::doxygen)
set(doxyfileout ${CMAKE_CURRENT_BINARY_DIR}/${doxyfilein})
configure_file(${doxyfilein} ${doxyfileout} @ONLY)
set(targetName "${CMAKE_PROJECT_NAME}_doc")
add_custom_target(${targetName} COMMAND Doxygen::doxygen ${doxyfileout}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating API documentation with Doxygen" VERBATIM
SOURCES ${doxyfilein} ${doxyfileout})
set_target_properties(${targetName} PROPERTIES FOLDER ${CMAKE_PROJECT_NAME})
source_group(doxyfile_input FILES ${doxyfilein})
source_group(doxyfile_output FILES ${doxyfileout})
else()
message(STATUS "not have doxygen, ignore")
endif()
endfunction()1
2
3
4
5
6
7include(GNUInstallDirs)
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/docs/html/"
DESTINATION "${CMAKE_INSTALL_DOCDIR}" COMPONENT
ch6_ex01_html)
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/docs/man/"
DESTINATION "${CMAKE_INSTALL_MANDIR}" COMPONENT
ch6_ex01_man)1
2
3
4
5set(CPACK_PACKAGE_NAME cbp_chapter6_example01)
set(CPACK_PACKAGE_VENDOR "CBP Authors")
set(CPACK_GENERATOR "DEB;RPM;TBZ2")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "CBP Authors")
include(CPack)集成代码质量工具
代码测试
1
2ctest --test-dir <build_dir>
cmake --build <build_dir> --target test1
2
3
4
5
6
7add_test(NAME labeled_test_1 COMMAND someTest)
set_tests_properties(labeled_test PROPERTIES LABELS "example")
add_test(NAME labeled_test_2 COMMAND anotherTest)
set_tests_properties(labeled_test_2 PROPERTIES LABELS "will_fail" )
add_test(NAME labeled_test_3 COMMAND YetAnotherText)
set_tests_properties(labeled_test_3 PROPERTIES LABELS "example;will_fail")1
ctest -L "example|will_fail"
执行自定义任务
1
2
3
4
5
6
7
8
9
10add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]])add_custom_target
用于定义一个自定义的目标,这个目标可以运行一个命令或一组命令1
2
3add_executable(SomeExe)
add_custom_target(CreateHash ALL COMMAND Somehasher
$<TARGET_FILE:SomeExe>)1
2
3
4
5
6
7add_executable(MyExecutable)
add_custom_command(TARGET MyExecutable
POST_BUILD
COMMAND hasher $<TARGET_FILE:ch8_custom_command_example>
${CMAKE_CURRENT_BINARY_DIR}/MyExecutable.sha256
COMMENT "Creating hash for MyExecutable"
)add_custom_command
用于定义一个命令,该命令会在构建过程中运行,并可以与一个目标关联.1
2
3
4
5
6
7
8
9
10
11
12add_custom_command(OUTPUT archive.tar.gz
COMMAND cmake -E tar czf ${CMAKE_CURRENT_BINARY_DIR}/archive.tar.gz
$<TARGET_FILE:MyTarget>
COMMENT "Creating Archive for MyTarget"
VERBATIM
)
add_custom_command(OUTPUT archive.tar.gz
COMMAND cmake -E tar czf ${CMAKE_CURRENT_BINARY_DIR}/archive.
tar.gz
${CMAKE_CURRENT_SOURCE_DIR}/SomeFile.txt
APPEND
)VERBATIM
是一个关键字,用于控制如何解释 add_custom_command
或 add_custom_target
中定义的命令行,当你在命令中使用 VERBATIM
关键字时,CMake 会将整个命令行作为单个字符串传递给构建系统,而不是尝试解析其中的变量或表达式1
2
3
4
5
6execute_process(
COMMAND SomeExecutable
COMMAND AnotherExecutable
COMMAND_ERROR_IS_FATAL_ANY
)1
2
3
4
5configure_file(<input> <output>
NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
FILE_PERMISSIONS <permissions>...]
[COPYONLY] [ESCAPE_QUOTES] [@ONLY]
[NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])