cmake学习

在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
4
targets: prerequisites
command
command
command

目标(targets)是文件名,用空格隔开.通常情况下,每条规则(rule)只有一个目标.

命令(command)是一系列通常用于创建目标的步骤,需要以制表符而不是空格开头.

先决条件(prerequisites)也是文件名,以空格分隔.在运行目标的命令之前,这些文件必须存在.这些文件也称为依赖项

以 hello world 为例开始:

1
2
3
hello:
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
9
blah: 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
6
some_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
4
some_file: 
touch some_file
clean:
rm -f some_file

variables

变量只能是字符串. 通常情况下,您需要使用 :=,但 = 也可以.

1
2
3
4
5
6
7
8
9
10
11
12
files := 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
5
a := 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
7
x := 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
11
all: 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
9
all: 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
16
thing_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
23
hey: 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 from n.c with a command of the form $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
  • Compiling a C++ program: n.o is made automatically from n.cc or n.cpp with a command of the form $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
  • Linking a single object file: n is made automatically from n.o by running the command $(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@

这些值默认

  • CC: Program for compiling C programs; default cc
  • CXX: Program for compiling C++ programs; default g++
  • CFLAGS: Extra flags to give to the C compiler
  • CXXFLAGS: Extra flags to give to the C++ compiler
  • CPPFLAGS: Extra flags to give to the C preprocessor
  • LDFLAGS: Extra flags to give to compilers when they are supposed to invoke the linke
1
2
3
4
5
6
7
8
9
10
11
12
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info

# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

静态模式规则是在 Makefile 中少写代码的另一种方法.

1
2
targets...: 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
22
objects = 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
19
objects = 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
20
obj_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.
.PHONY: all

# 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. 一种定义自己的隐式规则的方法
  2. 一种更简单的静态模式规则形式
1
2
3
# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

模式规则在目标中包含一个”%”. 该’%’匹配任何非空字符串,其他字符则自行匹配.

模式规则先决条件中的”%”代表与目标中的”%”匹配的同一词干.

双冒号规则很少使用,但允许为同一目标定义多个规则. 如果这些规则是单冒号,则会打印警告,并且只会运行第二组命令.

1
2
3
4
5
all: blah
blah::
echo "hello"
blah::
echo "hello again"

在命令前添加 @,以阻止命令被打印

也可以使用 -s 运行 make,在每一行前添加 @.

1
2
3
all: 
@echo "This make line will not be printed"
echo "But this will"

每条命令都在一个新的 shell 中运行

1
2
3
4
5
6
7
8
9
10
11
all: 
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
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

如果希望字符串带有美元符号,可以使用 $$. 这就是在 bash 或 sh 中使用 shell 变量的方法,请注意 Makefile 变量和 Shell 变量之间的区别.

1
2
3
4
5
6
7
make_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
8
new_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
5
shell_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 set

1
2
3
4
5
6
7
one = hello
one ?= will not be set
two ?= will be set

all:
echo $(one)
echo $(two)

行尾的空格不会被删除,但行首的空格会被删除. 要使用单空格创建变量,请使用 $(nullstring)

1
2
3
4
5
6
7
8
9
with_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
5
foo := 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
12
one = 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
16
all: 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
2
3
4
5
6
7
foo = ok
all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif

检查变量是否为空

1
2
3
4
5
6
7
8
9
10
nullstring =
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
10
bar =
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
5
all:
# 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
3
bar := ${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
11
foo := 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
7
foo := 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
7
foo := $(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
5
sweet_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 \ <目录,空格/冒号分隔> <\pattern> 可以有一个 %,可以匹配任何 0 个或多个字符. 您也可以使用变量 VPATH 进行全局操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vpath %.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_binary

在目标中添加 .PHONY 可以防止 Make 将假目标与文件名混淆. 在本例中,如果创建了 clean 文件,仍将运行 make clean.

“phony”目标的名称通常很少是文件名,在实践中很多人都会跳过这一点

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

.PHONY: clean
clean:
rm -f some_file
rm -f clean

如果命令返回非零的退出状态,make 工具将停止运行规则(并返回先决条件)

如果规则以这种方式失败,DELETE_ON_ERROR 将删除规则的目标. 这将发生在所有目标上,而不仅仅是像 PHONY 这样的目标. 尽管出于历史原因,make 并没有这样做,但最好还是一直使用这个功能.

1
2
3
4
5
6
7
8
9
10
.DELETE_ON_ERROR:
all: one two

one:
touch one
false

two:
touch two
false

cmake fundamental

在Windows上可选择的构建后端有vs,codeblocks这种软件的文件结构,或者单纯的Makefiles以及Ninja.相当于忽略了几个项目构建的差异.

image-20231013105520974

常用变量

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_FLAGSCMAKE_C_FLAGS

设置C/ C++编译选项,CMAKE_C_COMPILER设置对应编译器路径.

BUILD_SHARED_LIBS

用来控制默认的库编译方式,如果不进行设置,使用ADD_LIBRARY 并没有指定库类型的情况下,默认编译生成的库都是静态库.

此外还有一些系统信息

image-20231013163518609

使用$ENV{}调用系统变量.

指定生成程序

1
2
3
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])

将源码source构建成一个库, 供他人使用

[STATIC | SHARED | MODULE] :类型有三种

1
2
add_executable(< name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL] source1 source2 … sourceN)

使用给定的源文件,为工程引入一个可执行文件.引入一个名为< name>的可执行目标,该目标会由调用该命令时在源文件列表中指定的源文件来构建

添加头文件目录和库

1
2
include_directories([AFTER|BEFORE] [SYSTEM]  dir1  dir2 ...)
target_include_directories()

将给定目录 dir1 dir2 加给编译器搜索到的包含文件 .默认情况下,加到目录列表的最后,target_include_directories可以指定针对目标文件添加头文件目录.

1
2
target_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

find_path和find_library分别用来找头文件和库.找到之后可以使用include_directory或者target_link_libraries用来使用.

1
2
FIND_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)

set设置变量,后续使用${}使用变量

控制结构

if elseif else endif

文件中可以使用条件,循环等控制语句.可以用来判断构建时系统的一些环境.

比如

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

添加一个子目录并构建该子目录.source_dir指定源CMakeLists.txt和代码文件所在的目录.

一般用在嵌套的项目中,顶层CMakeLists.txt文件添加子目录,让子目录先构建完成之后添加其中生成的库和头文件.

获取文件

1
FILE (GLOB ALL_SOURCES "*.cpp" "*.c" "./AFolder/*.cpp" )

使用正则匹配响应文件并存到一个变量中

1
aux_source_directory(dir VAR) 

发现一个目录下所有的源代码文件并将列表存储在一个变量中.

vs中显示头文件

1
2
file(GLOB_RECURSE pipe_header_files  ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h )
source_group("Header Files" FILES ${pipe_header_files})

使用source_group增加文件

并添加到生成目标中

1
add_library( lib_pipe_shared SHARED ${pipe_src} ${pipe_header_files})

option与add_definitions

1
option(<variable> "<help_text>" [value])

可以在cmake命令中指定该值.

而add_definition用于指定编译器参数,比如add_definitions("-Wall -g"),此外更推荐使用add_compile_definitions将预处理器定义添加到编译器命令行,使用add_compile_options命令添加其它选项.

比如下面文件,使用add_definition定义了TEST_DEBUG,option定义为OFF并在cmake执行时指定为on,然后在cmake文件中指定option为on,这样就执行了add_definitions(-DTEST_DEBUG),定义了该宏.

1
2
3
4
5
#!/bin/sh

cmake -DTEST_DEBUG=ON .
cmake --build .

1
2
3
4
5
6
project(test)

option(TEST_DEBUG "option for debug" OFF)
if (TEST_DEBUG)
add_definitions(-DTEST_DEBUG)
endif()
1
2
3
4
5
#include "test.h"

#ifdef TEST_DEBUG
...
#endif

CMake中的命令特别多,事实上并不需要去一个一个记住,通常只要知道一个项目的大致构建流程以及可能需要的命令就行了.

1
2
3
4
target_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

生成器表示

生成器表达式是在构建的配置阶段进行的语句.大多数函数允许使用生成器表达式,只有少数例外.以 $ 的形式使用,其中 OPERATOR 或直接使用,或与 VALUE 进行比 较.

1
2
3
target_compile_options(my_target PRIVATE
"$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall>")

若 CXX_COMPILER_ID 变量匹配 GNU, Clang, AppleClang 列表中任意一个,则附加-Wall 选 项到目标——也就是 my_target.生成器表达式在编写独立于平台和编译器的 CMake 文件时非常方便.

除了查询值,生成器表达式还可以用于转换字符串和列表:

1
$<LOWER_CASE:CMake>

这将输出“cmake”.

Advanced cmake

cmake在c++中用的太多了,

最近在看别人项目的时候看见了一些比较高级的cmake使用方法.

macro

定义一个名为 \ 的宏,该宏接收名为 \, … 的参数.

CMake 宏使用 macro()/endmacro() 定义,有点像函数function.不同的是,函数参数是真变量,而 在宏中是字符串替换.这意味着必须使用大括号访问宏的所有参数. 另一个区别是,通过调用函数,作用区域转移到函数内部.执行宏时,就好像宏的主体粘贴到 调用位置一样,宏不会创建变量和控制流的作用域.因此,避免在宏中调用 return().

function

函数由 function()/endfunction() 定义.函数为变量创建了一个新的作用域,因此所有 在内部定义的变量都不能从外部访问,除非将 PARENT_SCOPE 选项传递给 set(). 函数不区分大小写,通过 function 后的名称加上圆括号来使用函数

Effective CMake

通用

使用CMake版本大于3.0.0

将CMake视为代码,像对待任何其他编程语言一样,保持CMakeLists.txt和模块的代码清晰、结构良好.

全局定义项目属性,在顶层CMakeLists.txt文件中定义如编译器警告、代码标准等全局项目属性,确保所有目标使用相同的标准,避免因依赖目标间的编译选项不一致导致的问题

例如,一个项目可能会使用一组通用的编译器警告. 在顶层 CMakeLists.txt 文件中全局定义此类属性,可避免因依赖目标使用更严格的编译器选项而导致依赖目标的公共头文件无法编译的情况. 全局定义此类项目属性可以更方便地管理项目及其所有目标.

直接操作目标(targets),不要使用add_compile_options, include_directories, link_directories, link_libraries等命令,因为它们作用于目录级别,可能引入隐含的依赖关系.相反,直接对特定的目标使用target_compile_options, target_include_directories, target_link_directories, target_link_libraries等命令.

不要直接操作CMAKE_CXX_FLAGS,不同的编译器有不同的命令行参数格式.通过CMAKE_CXX_FLAGS设置C++标准(如-std=c++14)可能会在未来版本的编译器上失效,因为这些要求也可能在其他标准(如C++17)中满足,而且老编译器的选项可能不同.因此,应该告诉CMake所需的编译特性,让CMake根据具体情况选择适当的编译器选项.

合理使用usage requirements,例如,不要将-Wall(开启所有警告)添加到目标的PUBLIC或INTERFACE段的target_compile_options中,因为这不是构建依赖目标所必需的,这样做可能导致不必要的警告信息.

模块Modules

使用声明导出目标的现代查找模块.

从 CMake 3.4 开始,越来越多的查找模块导出了可通过 target_link_libraries 使用的目标.

使用外部软件包的导出目标. 不要重蹈 CMake 使用外部软件包定义的变量的覆辙. 而应通过 target_link_libraries 使用导出的目标.

对于不支持客户端使用 CMake 的第三方库,请使用查找模块. CMake 为第三方库提供了一系列查找模块.

例如,Boost 不支持 CMake. 相反,CMake 提供了一个查找模块,以便在 CMake 中使用 Boost.

如果某个库不支持客户端使用 CMake,请将其作为 bug 报告给第三方库作者. 如果该库是一个开源项目,请考虑发送补丁. CMake 在业界占主导地位. 如果库作者不支持 CMake,那就麻烦了.

为不支持客户端使用 CMake 的第三方库编写查找模块. 可以改造查找模块,将目标正确导出到不支持 CMake 的外部软件包.

如果您是库作者,请导出库的INTERFACE

项目Projects
  • 避免在项目命令参数中使用自定义变量. 保持简单. 不要引入不必要的自定义变量. 不要使用 add_library(a ${MY_HEADERS} ${MY_SOURCES}),而应使用 add_library(a b.h b.cpp).

  • 不要在项目中使用 file(GLOB),CMake 是一个编译系统生成器,而不是一个编译系统. 在生成构建系统时,它会将 GLOB 表达式求值为一个文件列表. 然后,联编系统会对该文件列表进行操作. CMake 无法将 GLOB 表达式转发给联编系统,以便在联编时对表达式进行评估. CMake 希望成为所支持的联编系统的公分母. 并非所有的构建系统都支持这一点,因此 CMake 也无法支持.

  • 将特定于 CI 的设置放在 CTest 脚本中,而不是项目中,这样会让事情更简单.
  • 测试名称应遵循命名约定. 这样可以简化通过 CTest 运行测试时的 regex 过滤.
目标(Targets)及其属性(Properties)

从目标和属性的角度思考

通过从目标的角度定义属性(即编译定义、编译选项、编译特性、包含目录和库依赖关系,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 来处理任何函数中基于参数的复杂行为或可选参数.

循环
  • foreach(var IN ITEMS foo bar baz) ...
  • foreach(var IN LISTS my_list) ...
  • foreach(var IN LISTS my_list ITEMS foo bar baz) ...

使用 CPack 创建软件包.

CPack 是 CMake 的一部分,并与 CMake 完美集成.

编写 CPackConfig.cmake,其中包括 CMake 生成的 CPackConfig.cmake,这样就可以设置无需出现在项目中的其他变量.

交叉编译

使用工具链文件进行交叉编译.工具链文件封装了用于交叉编译的工具链.

保持工具链文件的简洁,这样更易于理解和使用. 不要在工具链文件中加入逻辑. 为每个平台创建一个工具链文件.

警告与报错

正确对待构建错误、修复错误、拒绝拉取请求、暂缓发布.

将警告视为错误

要将警告视为错误,切勿向编译器传递 -Werror. 如果这样做,编译器就会将警告视为错误.

  • You cannot enable -Werror unless you already reached zero warnings.
  • You cannot increase the warning level unless you already fixed all warnings introduced by that level.
  • You cannot upgrade your compiler unless you already fixed all new warnings that the compiler reports at your warning level.
  • You cannot update your dependencies unless you already ported your code away from any symbols that are now [[deprecated]].
  • You cannot [[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.

    将新的警告视为错误

  1. At the beginning of a development cycle (e.g., sprint), allow new warnings to be introduced.
    • Increase warning level, enable new warnings explicitly.
    • Update the compiler.
    • Update dependencies.
    • Mark symbols as [[deprecated]].
  2. Burn down the number of warnings.
  3. Repeat.

使用多个支持的分析器:clang-tidy (\_CLANG_TIDY)、cpplint (\_CPPLINT)、include-what-you-use (\_INCLUDE_WHAT_YOU_USE) 和 LINK_WHAT_YOU_USE 可帮助您发现代码中的问题. 这些工具的诊断输出将显示在构建输出和集成开发环境中.

对于每个头文件,都必须有一个关联的源文件,该源文件 #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

项目结构

1
2
3
4
├── CMakeLists.txt
├── build
├── include/project_name
└── src

最小的项目结构中有三个文件夹和一个文件

• build: 放置构建文件和二进制文件的文件夹.

• include/project_name: 此 文 件 夹 包 含 从 项 目 外 部 公 开 访 问 的 所 有 头 文 件, 包 含 使它更容易看出头文件来自哪个库.

• src: 此文件夹包含所有私有的源文件和头文件

• CMakeLists.txt: 这是主 CMake 文件

构建文件夹可以放置在任何地方,放在项目根目录最方便,但强烈建议不要选择任何非空文件 夹作为构建文件夹.特别是将构建好的文件放入 include 或 src 中,这是一种糟糕的实践.其他文件 夹,如 test 或 doc,在组织测试项目和文档页面时就很方便

嵌套项目

1
2
3
4
5
6
7
8
9
├── CMakeLists.txt
├── build
├── include/project_name
├── src
└── subproject
├── CMakeLists.txt
├── include
│ └── subproject
└── src
1
2
3
4
5
6
7
8
9
10
11
cmake_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)

第一行,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 以外的内容只对库目标有意义.

创建库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_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)

源文件使用 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 属性来处理这个问题.若 设置了此变量,后缀将自动添加到非可执行目标.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
add_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)

这将使库文件和符号链接命名为 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,通过隐藏内联 类成员函数进一步减少导出的符号表

1
2
3
4
5
6
add_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")

接口和纯头文件库

纯头文件的库有点特殊,因为不需要编译; 相反,可以导出它们的头文件,以便直接包含在其 他库中.

大多数情况下,头文件库的工作方式与普通库类似,但是头文件使用 INTERFACE,而非 PUBLIC.

由于仅包含头文件的库不需要编译,因此不会向目标添加源文件.

1
2
3
4
5
6
7
8
project(
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)

对象库

有时,可能想要分离代码,以便部分代码可以重用,而不需要创建完整的库.当想在可执行测 试和单元测试中使用某些代码时,通常的做法是不需要重新编译所有代码两次.为此,CMake 提 供了对象库,其中的源代码是编译的,但不进行归档或链接.通过 add_library(MyLibrary object) 创建对象库.

自 CMake 3.12 起, 这 些 对 象 可 以 像 普 通 库 一 样 使 用, 只 需 将 它 们 添 加 到 target_link_libraries 函数中.3.12 版本之前,对象库需要添加生成器表达式,也就 是 $.这将在生成构建系统期间扩展为一个对象列表.这种方式现 在还可以用,但不推荐这样做,因为这很快就变得不可维护,特别是在一个项目中有多个对象库的 情况下.

使用库

可 以 把 add_library 放 在 同 一 个 CMakeLists.txt 文 件 中, 或 者 使 用 add_subdirectory 将其整合起来.两者都是有效的选项,并取决于项目的设置方式

1
2
3
4
5
6
7
add_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)

target_link_libraries 的目标也可以是另一个库.同样,库的链接说明符,可以是以下 任意一个:

• PRIVATE: 用于链接库,但不是公共接口的一部分.只有在构建目标时才需要链接库.

• INTERFACE: 没有链接到库,但是公共接口的一部分.当在其他地方使用目标时,链接库是必需的.这通常仅限头文件库时使用.

• PUBLIC: 链接到库,是公共接口的一部分.因此,该库既是构建依赖项,也是使用依赖项.

设置编译器和链接器选项

C++ 编译器有很多选项来设置一些常见的标志,从外部设置预处理器定义也是一种常见的做 法.CMake 中,这些是使用 target_compile_options 传递,使用 target_link_options 更改链接器行为,但编译器和链接器可能有不同的设置标志的方法.例如,在 GCC 和 Clang 中,选 项用减号 (-) 传递,而 Microsoft 编译器将斜杠 (/) 作为选项的前缀.但是通过生成器表达式,可以很容易地在 CMake 中处理这个问题:

1
2
3
4
5
6
target_link_options(
hello
PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/SomeOption>
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-
someOption>
)

$$:/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 和类似的库.

1
2
3
add_library(Chapter3::hello ALIAS hello)
...
target_link_libraries(SomeLibrary PRIVATE Chapter3::hello)

使用预设值维护构建配置

构建信息可以存储在 CMakePresets.json 文件中,放在项目的根目录中.此外,每个用户都可以将他们配置添加到 CMakeUserPresets.json 文 件中.基本预设通常置于版本控制之下

1
2
3
4
5
6
7
8
9
10
11
12
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [...],
"buildPresets": [...],
"testPresets": [...]
}

要查看项目中配置了哪些预设值,请运行 cmake —list-presets 查看可用预设值的列表. 要使用预设进行生成,请执行 cmake —build —preset name

打包、部署和安装

install() 指令 install(…) 指令是一个内置的 CMake 命令,允许生成用于安装目标、文件、目录等的构建系统说明.CMake 不会生成安装指令,除非明确地说明.因此,安装总是在开发者的控制中.

要使 CMake 目标可安装,必须用至少一个参数指定 TARGETS 参数.指令的签名如下所示:

1
install(TARGETS ... [...])

TARGETS 参数表示 install 可以接受一组 CMake 目标,以便为其生成安装代码,只安装目 标的输出构件.最常见的目标输出工件定义如下:

• ARCHIVE (静态库、DLL 导入库和链接器导入文件): – 除了在 macOS 中标记为 FRAMEWORK 的目标

• LIBRARY (动态库): – 除了在 macOS 中标记为 FRAMEWORK 的目标 – 除了 dll (Windows)

• RUNTIME (可执行文件和 dll): – 除了在 macOS 中标记为 MACOSX_BUNDLE 的目标

这些目录的默认值由 CMake 提供,具体取决于目标类 型.提供默认安装路径信息的 CMake 模块称为 GNUInstallDirs 模块.GNUInstallDirs 模块定义了各 种 CMAKEINSTALL的路径.默认安装目录如下表所示:

image-20240723111850609

覆盖内置默认值,在 install(…) 指令中需要使用 DESTINATION 参数.

1
2
3
install(TARGETS ch4_ex01_executable
RUNTIME DESTINATION qbin
)
1
2
3
4
5
6
7
8
9
10
add_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}"
)

多一个 DIRECTORY 参数,这是使静态库的头文 件可安装.这样做的原因是 CMake 不会安装任何非输出工件,而 STATIC 库目标只生成一个二进制 文件作为输出工件.头文件不是输出工件,应该单独安装

安装文件

安装的东西并不总是目标输出构件的一部分.它们可能是目标的运行时依赖项,例如图片、源文件、脚本和配置文件

install(FILES…) 指令接受一个或多个文件作为参数,还需要 TYPE 或 DESTINATION 参 数,这两个参数用于确定指定文件的目标目录.

TYPE 用于指示哪些文件将使用该文件类型的默认路径作为安装目录,可以通过设置相关的 GNUInstallDirs 变量来重写默认值

image-20240724193228436

1
2
3
4
5
install(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)

安装目录

install(DIRECTORY…) 指令用于安装目录,目录的结构将按原样复制到目标.目录既可以 作为一个整体安装,也可以有选择地安装.

1
install(DIRECTORY dir1 dir2 dir3 TYPE LOCALSTATE)
1
2
3
4
5
6
7
8
include(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)

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

包 配 置 文 件的内容可能如下,主要设置包含头文件和库:

1
2
set(Foo_INCLUDE_DIRS ${PREFIX}/include/foo-1.2) 
set(Foo_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)

搜索包时,find_package(…) 会查找 ${CMAKE_PREFIX_PATH}$/cmake 目录.

1
2
3
include(GNUInstallDirs)
set(ch4_ex05_lib_INSTALL_CMAKEDIR cmake CACHE PATH
"Installation directory for config-file package cmake files")

include(GNUInstallDirs) 用 于 包 含 GNUInstallDirs 模 块. 这 提 供 了 CMAKE_INSTALL_INCLUDEDIR 变 量, set(ch4_ex05_lib_INSTALL_CMAKEDIR…) 是一个用户定义的变量,是导出目标的安装路径.

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
8
install(TARGETS ch4_ex05_lib
EXPORT ch4_ex05_lib_export
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install (
DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # 头文件安装目录
)

EXPORT 参 数, 用 于 从 给 定 的 install(…) 目 标 创 建 一 个 导 出 名 称. 然 后, 可 以 使 用 此 导 出 名 称 导 出 这 些 目 标. 使 用 INCLUDES DESTINATION 参 数 指 定 的 路 径, 将 用 于 填 充 导 出 目 标 的 INTERFACE_INCLUDE_DIRECTORIES 属 性, 并 自 动 使 用 安 装 前 缀 路 径 作 为 前 缀(就是安装头文件,因为其不会被显示安装或导入)

1
2
3
4
5
install(EXPORT ch4_ex05_lib_export
FILE ch4_ex05_lib-config.cmake
NAMESPACE ch4_ex05_lib::
DESTINATION ${ch4_ex05_lib_INSTALL_CMAKEDIR}
)

EXPORT 参数接受现有的导出名 称来进行导出,它引用的是 ch4_ex05_lib_export 导出名称,在之前的 install(TARGETS…) 中创建的.

FILE 用于确定导出的文件名,并设置为 ch4_ex05_lib-config.cmake.

NAMESPACE 用于 为所有导出的目标添加命名空间前缀.这允许将所有导出的目标连接到通用的命名空间下,并避免 与具有相似目标名称的包发生冲突.

最后,DESTINATION 确定生成导出文件的安装路径.设置为 ${ch4_ex05_lib_INSTALL_CMAKEDIR} 以便 find_package() 发现它

要实现对 find_package(…) 的完全支持,还需要生成包版本文件.

1
2
3
4
5
6
7
8
9
10
11
12
include(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}"
)

使用 include(CMakePackageConfigHelpers),导入 CMakePackageConfigHelpers 模块.这个模块 提供了 write_basic_package_version_file(…) 函数,用于根据给定的参数自动生成包版本文件

简单来说,通过生成包配置和包版本文件,方便find_package导入.

而包配置文件需要通过install(export )安装,包版本文件利用write_basic_package_version_file从项目版本获取导出版本,匹配find_package中的版本

如何使用

1
2
3
4
5
6
7
8
9
10
if(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
#include <chapter4/ex05/lib.hpp> # 引入头文件
int main(void){
chapter4::ex05::greeter g;
g.greet();
}

创建安装包

CMake 的打包工具 CPack 默认随 CMake 安装一起提供,可以利用现有的 CMake 代码来生成特定于平台的安装和包

image-20240725113947270

CPack 使用 CPackConfig.cmake 中的配置细 节,CPackSourceConfig.Cmake 文件生成包.这些文件可以手动填写,也可以由 CMake 在 CPack 模块的帮助下自动生成.

包含 CPack 模块会生成 CPackConfig.cmakeCPackSourceConfig.cmake 文件,这是打包项目所需的 CPack 配置

当 CMake 或用户正确设置了 CPack 配置文 件,就可以使用 CPack.CPack 模块允许定制包装过程,从而可以设置大量的 CPack 变量.这些变 量分为两组——普通变量和生成器特定变量.公共变量影响所有包生成器,而生成器特定的变量只 影响特定类型的生成器

image-20240725113958063

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_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/

项目配置完成后,CpackConfig.cmake 和 CpackConfigSource.cmake 文件将生成到 build/CPack* 目录下

1
2
cmake --build build/
cpack --config build/CPackConfig.cmake -B build/

参数—config 是 CPack 命令的主要输入.参数-B 修改了默认的包目录,CPack 将把它的工 件写入该目录

寻找包和文件

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

CMake 能够查找定义目标、包括路径和包特定变量的整个包.更多细节请参考 CMake 项目部 分中的库. 有五个 find_…指令,它们选项和行为非常相似:

• find_file: 定位单个文件.

• find_path: 查找包含特定文件的目录.

• find_library: 查找库文件.

• find_program: 查找可执行程序.

• find_package: 查找完整的包

find_pacakge

1
2
3
4
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])

find_package 有两个签名: 一个基本签名或短签名,一个完整签名或长签名.通常, 使用短签名就足以找到正在寻找的包,因为它更容易维护,应该是首选.短格式同时支持模块和配置包,而长格式只支持配置模式

  • 模块模式下运行时,find_package 会搜索名为 Find\.cmake 的文件; 首先会 在 CMAKE_MODULE_PATH 指定的路径中搜索,然后是在外部提供的路径中查找模块.
  • 在配置模式下运行时,find_package 会搜索以下的文件:
  • • -config.cmake

    • Config.cmake

    • -config-version.cmake (若指定了版本详细信息)

    • ConfigVersion.cmake (若指定了版本详细信息)

    1
    2
    3
    find_package(OpenSSL REQUIRED COMPONENTS SSL)
    add_executable(find_package_example)
    target_link_libraries(find_package_example PRIVATE OpenSSL::SSL)

    find_package搜索的目录顺序

    image-20240725144118496

    配置模式下搜索路径如下

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

    编写查找模块(如果想使用的库没有cmake)

    目前仍然有很多库没有使用 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

    find 模块如何找到必要的头文件和二进制文件,以及为 CMake 创建导入目标的指令.当使用 find_package 时,CMake 在 CMAKE_MODULE_PATH 中搜索名为 Find\.cmake 的文件

    处理逻辑是首先在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 使用已安装的包,

    1
    2
    Cmake -S <source_dir> -D <binary_dir> -DCMAKE_TOOLCHAIN_
    FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake

    设置工具链文件可能会在交叉编译时导致问题,因为 CMAKE_TOOLCHAIN_FILE 可能已经 指向一个不同的文件,所以第二个工具链文件可以通过 VCPKG_CHAINLOAD_TOOLCHAIN_FILE 传递

    1
    2
    3
    cmake -S <source_dir> -D <binary_dir> -DCMAKE_TOOLCHAIN_
    FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake -DVCPKG
    _CHAINLOAD_TOOLCHAIN_FILE=/path/to/other/toolchain.cmake

    主要是要让cmake知道包管理器安装了哪些库并指定cmake安装哪些库,可以通过CMakePresets.json进行配置

    获取依赖项源代码

    对于构建的外部项目,使用 FetchContent 模块是添加源依赖项的一种方法.对于二进制依 赖,使用 find_package的方式仍是首选.

    下载并将第三方软件的副本集成到产品中的做法,称为供应商方式.优点是常常使构建软件 变得容易,但在打包库方面产生了问题

    FetchContent

    FetchContent 提供了一系列函数来拉取源依赖项,主要是 FetchContent_Declare,它定 义了下载和构建 FetchContent_MakeAvailable 的参数,FetchContent_MakeAvailable 填充依赖项的目标,并使它们可用于构建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    include(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
    )

    自动生成文档

    Doxygen 是一个非常流行的 C++ 项目文档软件,允许从代码生成文档.,Doxygen 要求注释采用预定义的一组格式, 还需要一个 Doxyfile,其包含文档生成的所有参数,比如输出格式、排除的文件模式、 项目名称等,因为配置参数太多,开始配置 Doxygen 可能会让人望而生畏,但 CMake 可以自动生 成 Doxyfile.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    find_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)

    • DOXYGEN_OUTPUT_DIRECTORY: 设置 Doxygen 的输出目录,
    • DOXYGEN_GENERATE_HTML: 生成超文本标记语言 (HTML)
    • DOXYGEN_GENERATE_MAN: 生成 MAN 页面,
    • DOXYGEN_AUTOLINK_SUPPORT: Doxygen 自动链接语言符号和文件名到相关文档页面 (若 可用),
    • DOXYGEN_HAVE_DOT: Doxygen 环境有可用的 dot,该命令可用于生成图像,这将使 Doxygen 能够使用依赖关系图、继承图和协作图来丰富生成的文档,
    • DOXYGEN_COLLABORATION_GRAPH: 为类生成协作图
    • DOXYGEN_CLASS_GRAPH: 生成类图,
    • DOXYGEN_UML_LOOK: Instructs 生成类似统一建模语言 (UML) 的图,
    • DOXYGEN_DOT_UML_DETAILS: 将类型和参数信息添加到 UML 图中,
    • DOXYGEN_DOT_WRAP_THRESHOLD: 为 UML 图设置行换行阈值,
    • DOXYGEN_CALL_GRAPH: 为函数文档中的函数生成调用图,
    • DOXYGEN_QUIET: 静默生成到标准输出 (stdout) 的 Doxygen 输出
    1
    2
    3
    4
    5
    6
    7
    doxygen_add_docs(
    ch6_ex01_doxdocgen_generate_docs
    "${CMAKE_CURRENT_LIST_DIR}"
    ALL
    COMMENT "Generating documentation for Chapter 6 - Example
    01 with Doxygen"
    )

    使用 doxygen_add_docs(…) 来生成文档,该函数将生成一个名为 targetName 的自定义目标

    参数列表 filesOrDirs, 包含生成文档的代码的文件或目录的列表

    ALL 参数用于使 CMake 的 ALL 元目标依赖于 doxygen_add_docs(…) 创建的文档目标,因此在构建 ALL 元目标时自动生成文档

    COMMENT 参数用于让 CMake 在构建目标时输出一条消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function(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()

    CPack打包文档

    1
    2
    3
    4
    5
    6
    7
    include(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
    5
    set(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)

    集成代码质量工具

    代码测试

    通过 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 构建步骤的一部 分,来执行测试,以下两个命令都可执行测试:

    1
    2
    ctest --test-dir  <build_dir>
    cmake --build <build_dir> --target test

    有选择地执行测试的另一种方法是使用 LABELS 属性进行标记,然后使用 CTest 的-L选项选 择要运行的标签,一个测试可以有多个用分号分隔的标签

    1
    2
    3
    4
    5
    6
    7
    add_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")

    -L 命令行选项接受一个正则表达式来过滤标签

    1
    ctest -L "example|will_fail"

    更好的测试库比如使用googleTest.

    此外还有代码静态检查(包括格式化,语法检查等)、覆盖率以及代码消杀等工具,但其实其中一些功能已经由其他程序代替了(比如IDE和一些编辑器),这里不做描述

    执行自定义任务

    构建时执行自定义任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add_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
    3
    add_executable(SomeExe)
    add_custom_target(CreateHash ALL COMMAND Somehasher
    $<TARGET_FILE:SomeExe>)

    定义一个自定义的目标,该目标本身不是一个可执行文件或库,而是一个独立的任务,比如清理操作、生成文档等,

    add_custom_command

    在构建目标时可能需要执行外部任务,CMake 中可以使用 add_custom_command 来 实现这一点,它有两个签名,一个用于将命令与现有目标挂钩,而另一个用于生成文件,

    1
    2
    3
    4
    5
    6
    7
    add_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 用于定义一个命令,该命令会在构建过程中运行,并可以与一个目标关联.

    它可以用来生成源文件或执行其他任务,希望自定义任务产生特定的输出文件,这可以通过定义自定义目标,并在目标之间设置 必要的依赖项来实现

    • PRE_BUILD: 在 Visual Studio 中,此命令在执行其他构建步骤之前执行,当使用其他生成器 时,会在 PRE_LINK 命令之前运行,
    • PRE_LINK: 此命令将在编译源代码之后运行,在可执行文件或存档工具链接到静态库之前运行,
    • POS_BUILD: 这将在执行所有其他构建规则后运行该命令,

    主要用于对目标进行读写,或是生成一些文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    add_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_commandadd_custom_target 中定义的命令行,当你在命令中使用 VERBATIM 关键字时,CMake 会将整个命令行作为单个字符串传递给构建系统,而不是尝试解析其中的变量或表达式

    配置时执行自定义任务

    生成在构建前需要的信息,或者 需要更新文件以重新运行 CMake,另一个情况是在配置步骤中生成 CMakeLists.txt 文件或其他输入 文件,也可以通过 configure_file 实现

    1
    2
    3
    4
    5
    6
    execute_process(
    COMMAND SomeExecutable
    COMMAND AnotherExecutable
    COMMAND_ERROR_IS_FATAL_ANY
    )

    构建软件时,常见的任务是在构建前将一些文件复制到特定位置或进行修改,解决方案是使用 configure_file 指令,可以将文件从一个位置复制到 另一个位置,

    1
    2
    3
    4
    5
    configure_file(<input> <output>
    NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
    FILE_PERMISSIONS <permissions>...]
    [COPYONLY] [ESCAPE_QUOTES] [@ONLY]
    [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

    configure_file 将\ 文件复制到\文件,将创建输出文件的路径,路径可以是相对路径或绝对路径,若使用相对路径,则将从当前源目录搜索输入文件,但输出文件的路径将相对于当前构建目录,

    若不能写入输出文件,命令失败,配置停止,通常,输出文件与 目标文件具有相同的权限,若当前用户与输入文件所属的用户不同,所有权可能会发生变化, 若添加了 NO_SOURCE_PERMISSION,则权限不会转移,输出文件将获得默认权限:rw-r—r—, 也可以使用 FILE_PERMISSIONS 选项手动指定权限,该选项接受一个三位数的数字作为参数, USE_SOURCE_PERMISSION 已经是默认选项,这个选项只是为了更明确地进行说明

    可复制的构建环境

    为了使用预设,项目的顶层目录必须包含名为 CMakePresets.json 或 CMakeUserPresets.json 的文 件,若两个文件都存在,将先解析 CMakePresets.json,再解析 CMakeUserPresets.json,这两个文件 有相同的格式,但使用方式略有不同:

    • CMakePresets.json 由项目本身提供,并处理项目特定的事情,比如运行 CI 构建,或若项目本 身需要哪些工具链,应该使用哪些工具链进行交叉编译,CMakePresets.json 用于特定的项目, 不应该引用项目结构外的文件或路径,由于这些预设与项目紧密相连,通常也处于版本控制 中,
    • CMakeUserPresets.json 通常由开发人员定义,以便在自己的机器或构建环境中使用, CMakeUserPresets.json 可以尽可能的具体,包含项目之外的路径或特定系统设置特有的路径, 因此,项目不应提供此文件,也不应将其置于版本控制中,

    预设是将缓存变量、编译器标志等移出 CMakeLists.txt 文件的好方法,同时以一种可以在 CMake 中使用的方式保持可用的信息,从而提高项目的可移植性

    若预置可用,通过命令 cmake —listpresets 可以从源目录中列出预设

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "version": 3,
    "cmakeMinimumRequired": {"major": 3,"minor": 21,"patch":
    0 },
    "configurePresets": [...],
    "buildPresets": [...],
    "testPresets": [...],
    "vendor": {
    "microsoft.com/VisualStudioSettings/CMake/1.9":
    { "intelliSenseMode": "windows-msvc-x64"
    } }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "version": 3,
    "configurePresets": [
    {
    "name": "ninja",
    "displayName": "Ninja Debug",
    "description": "build in debug mode using Ninja generator",
    "generator": "Ninja",
    "binaryDir": "build",
    "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
    ]
    }

    可以使用inherits字段进行继承

    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
    {
    "version": 3,
    "configurePresets": [
    {
    "name": "ci-ninja",
    "generator": "Ninja",
    "hidden": true,
    "binaryDir": "build"
    },
    {
    "name": "ci-ninja-debug",
    "inherits": "ci-ninja",
    "cacheVariables": {
    "CMAKE_BUILD_TYPE": "Debug"
    }
    },
    {
    "name": "ci-ninja-release",
    "inherits": "ci-ninja",
    "cacheVariables": {
    "CMAKE_BUILD_TYPE": "Release"
    }
    }
    ]
    }

    基本模块

    函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function(git_get_branch_name result_var_name)
    execute_process(
    COMMAND git symbolic-ref -q --short HEAD
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    OUTPUT_VARIABLE git_current_branch_name
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
    )
    set(${result_var_name} ${git_current_branch_name}
    PARENT_SCOPE)
    endfunction()

    函数定义了一个新的变量作用域,因此对 CMake 变量所做的更改只在函数体中可见,独立作 用域是函数最重要的属性,有了新的作用域就能避免意外地将变量暴露给使用者,除非我们想这样做

    大多数时候,我们希望在函数的作用域中包含更改,并且只将函数的结果反映给使用者,由于 CMake 没有返回值的概念,我们将采用在调用者的作用域方法中定义一个变量来将函数结果返回给使用者

    宏不定义新的变量作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    macro(git_get_branch_name_m result_var_name)
    execute_process(
    COMMAND git symbolic-ref -q --short HEAD
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    OUTPUT_VARIABLE ${result_var_name}
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
    )
    endmacro()

    具体项目

    参考UCMake/cmake/UbpaBasic.cmake at master · Ubpa/UCMake (github.com)

    首先介绍一下其中用到的方法,搭一个cmake项目,主要逻辑是利用include和add_subdirectory增加模块,同时设置更多变脸. 使用function和macro写一些工具方法.

    1
    cmake_parse_arguments(PREFIX  [OPTION names]  [ONE_VALUE names]  [MULTI_VALUE names]  [UNPARSED_ARGUMENTS var]  [args...] )

    参数解释

    • PREFIX:一个字符串,用于前缀命名解析出来的变量.
    • OPTION:可选关键字参数列表,每个参数只接受或不接受一个值,通常用于开关选项.
    • ONE_VALUE:关键字参数列表,每个参数接受一个值.
    • MULTI_VALUE:关键字参数列表,每个参数可以接受一个或多个值.
    • UNPARSED_ARGUMENTS:一个变量名,未解析的参数将被存储在这个变量中.
    • args…:传递给cmake_parse_arguments()的实际参数列表

    CMAKE_MODULE_PATH是CMake中的一个环境变量,用于指定CMake应该搜索额外模块文件的目录列表.这些模块文件通常包含自定义的CMake函数和宏,或者包含用于查找特定库、包的配置文件.

    需要在开始设置的属性比较多,包括编译器及其版本,输出目标目录,构建类型等等

    这些其实可以在CMAKE -D中设置

    1
    2
    3
    4
    CMAKE_BUILD_TYPE:STRING #构建类型
    CMAKE_CXX_COMPILER #修改编译器
    DCMAKE_CXX_FLAGS:STRING="-Wall
    -Werror" # 构建标志

    CMAKE_BUILD_TYPE 变量只对单配置生成器有意义,例如 Unix Makefiles 和 Ninja.在多配 置生成器中,例如 Visual Studio,构建类型是一个构建时参数,不是一个配置时参数,因此 不能通过使用 CMAKE_BUILD_TYPE 来配置

    CMake 本身提供了四种构建类型:

    • Debug: 未优化,包含所有调试符号,所有的断言都是启用的.这与 GCC 和 Clang 设置-O0 -g 是一样的.

    • Release: 对运行速度进行优化,没有调试符号和断言禁用.通常,这是用于交付的构建类型. 这与-O3 -DNDEBUG 相同.

    • RelWithDebInfo: 提供优化,并包括调试符号,禁用断言,这与-O2 -g -DNDEBUG 相同.

    • MinSizeRel: 这和 Release 是一样的,但优化了二进制大小,而不是速度,这与-Os -DNDEBUG 相同.注意,并不是所有平台上的生成器都支持此配置.

    构建标志可以为每个构建类型定制,方法是在它们后面加上大写的构建类型字符串.有四个变 量用于四个不同的构建类型,它们对于根据编译器标志指定构建类型非常有用.这些变量中指定的 标志只在配置构建类型匹配时有效: 1. CMAKE__FLAGS_DEBUG 2. CMAKEFLAGS_RELEASE 3. CMAKE\FLAGS_RELWITHDEBINFO 4. CMAKE__FLAGS_MINSIZEREL

    参考资料

    1. cmake指令汇总_cmake命令大全_nuosen123的博客-CSDN博客
    2. C++静态库与动态库 | 菜鸟教程 (runoob.com)
    3. 使用C++创建并调用动态链接库(dll) - 知乎 (zhihu.com)
    4. CMake指令详解_cmake -d-CSDN博客
    5. VS的包含目录、库目录、引用目录、可执行目录解释_vs包含目录和引用目录-CSDN博客
    6. cmake之Visual studio无法显示头文件 - mohist - 博客园 (cnblogs.com)
    7. Linux之cmake的指令以及内部构建和外部构建_cmake 外部编译-CSDN博客
    8. cmake 常用变量和常用环境变量 - 小果子啊 - 博客园 (cnblogs.com)
    9. 【CMake】CMakeLists.txt的超傻瓜手把手教程(附实例源码)_【cmake】cmakelists.txt的超傻瓜手把手教程(附实例源码)-CSDN博客
    10. make的link_directories命令不起作用-阿里云开发者社区 (aliyun.com)
    11. CMake Reference Documentation — CMake 3.28.0-rc1 Documentation
    12. CMakeLists.txt 语法介绍与实例演练-CSDN博客
    13. 现代的 CMake 的介绍 - 《Modern CMake 简体中文版》 - 书栈网 · BookStack
    14. CMake by Example
    15. Makefile Tutorial By Example
    16. C ++现在2017年:丹尼尔·菲费尔“有效的CMake” (youtube.com)
    -------------本文结束感谢您的阅读-------------
    感谢阅读.

    欢迎关注我的其它发布渠道