C++-Linux工程管理

1 Makefile和CMake实践

1.1 Makefile

参考
简介:
Makefile是一种用于自动化构建和管理程序的工具。它通常用于编译源代码、链接对象文件以生成可执行文件或库文件。Makefile以文本文件的形式存在,其中包含了一系列规则和指令,用于描述程序的依赖关系以及构建步骤,指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译。核心思想是根据文件的最后修改时间来确定哪些部分需要重新编译,以及以什么顺序来执行编译步骤。每个规则由一个目标(target)和一组依赖项(dependencies)组成,以及执行指令(commands)。

​ makefile带来的好处就是自动化编译,一旦写好就只需要一个make指令来实现该文件。当执行make命令时,Makefile解析并执行其中的规则。它首先检查目标文件和依赖文件的时间戳,如果目标文件不存在或其依赖项的时间戳较新,则执行该规则所定义的指令来生成目标文件。这种方式可以避免不必要的重新编译,提高构建效率。Makefile中常见的指令包括编译源代码、链接对象文件、复制文件、清理生成的文件等。通过定义变量和规则,可以使Makefile更加灵活和可维护。

​ Makefile广泛应用于C、C++等编程语言的项目中,但也可以用于其他类型的项目。它是一种跨平台的工具,可以在不同的操作系统上使用,例如Linux、Unix和Windows等。

​ 总而言之,Makefile是一种用于自动化构建和管理程序的工具,通过描述依赖关系和构建步骤,可以有效地管理大型项目的编译过程,并提高开发效率。

1.1.1 安装

sudo apt install make
sudo apt install make-guile

1.1.2 简单Makefile

在这里插入图片描述
重点:

  1. 目标、依赖、命令
  2. all有什么意义
  3. all和test的顺序问题
  4. 空格符号的影响

1.1.3 Makefile三要素

在这里插入图片描述
假如说目标all有两个依赖test1和test2,那么先执行最底层依赖。

再例如这个:
在这里插入图片描述
对于simple的两个依赖,他是从左向右执行的。
在这里插入图片描述

1.1.4 Makefile工作

在这里插入图片描述
现在,我们需要写一个用于创建simple 可执行程序的 Makefile 了,这个 Makefile 需要如何去写?还记得目标、依赖关系和命令吗?
此时的目录:Makefile/simple
foo.c

#include <stdio.h>
void foo ()
{printf ("This is foo   ()!\n");
}

main.c

extern void foo();
int main ()
{foo();return 0;
}

写一个 Makefile 文件的第一步不是一个猛子扎进去试着写一个规则,而是先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么样的依赖关系,这一点非常的重要。通过不断的练习,我们最终能达到很自然的运用依赖关系去思考问题。到那时,你在写 Makefile 时,头脑会非常的清楚自己在写什么,以及后面要写什么。现在抛开 Makefile,我们先看一看 simple 程序的依赖关系是什么。

我想是第一个跃入我们脑海中的依赖关系图,其中 simple 可执行文件显然是通过main.c 和 foo.c 最后编译并连接生成的。通过这个依赖图,其实我们就可以写出一个 Makefile 来了,这个任务交给读者你。这样的依赖关系所写出来的 Makefile,在现实中不是很可行,就是你得将所有的源程序都放在一行中让 GCC 为我们编译。如果是这样,那我们希望什么样的依赖关系呢?让我们想想看下图中缺了什么。目标文件?对了!
在这里插入图片描述
下图是 simple 程序的依赖关系更为精确的表达,其中我们加入了目标文件。对于 simple 可执行程序来说, 表示的就是它的“依赖树”。接下来需要做的是将其中的每一个依赖关系,即其中的每一个带箭头的虚线,用 Makefile 中的规则来表示。
在这里插入图片描述
有了“依赖树”,写 Makefile 就会相对的轻松了。下面是所对应的 Makefile。
Makefile

Makefile
all: main.o foo.o
gcc -o simple main.o foo.o
main.o: main.c
gcc -o main.o -c main.c
foo.o: foo.c
gcc -o foo.o -c foo.c
clean:
rm simple main.o foo.o

而下图则是依赖关系与规则的映射图。在这个 Makefile 中,我还增加了一个 clean 目标用于删除所生成的文件,包括目标文件和 simple 可执行程序,这在现实的项目中很是常见。
在这里插入图片描述
编译结果:

$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$./simple
This is foo ()!$make clean
rm simple main.o foo.o

首先我们解释一下这个$make的输出结果:
gcc -c main.c -o main.o这个是将main.c编译成main.o
gcc -c foo.c -o foo.o这个是将foo.c编译成foo.o
gcc -o simple main.o foo.o输出到simple中,然后执行的时候就是./simple
这个如果不懂的话可以看看这个链接
在这里插入图片描述
如果我们在不改变代码的清况下再编译会出现什么现象呢?下面给出了结果,注意到了第二次编译并没有构建目标文件的动作吗?但为什么有构建simple可执行程序的动作呢?为了明白为什么,我们需要了解 make 是如何决定哪些目标(这里是文件)是需要重新编译的。为什么 make会知道我们并没有改变 main.c 和 foo.c 呢?答案很简单,通过文件的时间戳!当 make 在运行一个规则时,我们前面已经提到了目标和先决条件之间的依赖关系,make 在检查一个规则时,采用的方法是:如果先决条件中相关的文件的时间戳大于目标的时间戳,即先决条件中的文件比目标更新,则知道有变化,那么需要运行规则当中的命令重新构建目标。这条规则会运用到所有与我们在 make时指定的目标的依赖树中的每一个规则。比如,对于 simple 项目,其依赖树中包括三个规则,make 会检查所有三个规则当中的目标(文件)与先决条件(文件)之间的时间先后关系,从而来决定是否要重新创建规则中的目标。

$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
gcc -o simple main.o foo.o

知道了 make 是如何工作以后,我们不难想明白,为什么前面进行第二次 make 时,还会重新构建 simple 可执行文件,因为 simple 文件不存在。我们将 Makefile 做一点小小的改动,下面代码所示。其最后的运行结果则是这样的。为什么还是和以前一样呢?哦,因为 Makefile 中的第一条规则中的目标是 all,而 all 文件在我们的编译过程中并不生成,即 make 在第二次编译时找不到,所以又重新编译了一遍。
Makefile

all: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o

执行

$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
gcc -o simple main.o foo.o

再一次更改后的 Makefile 如图下面代码所示,其最终的运行结果,它的确是发现了不需要进行第二次的编译。这正是我们所希望的!
Makefile

simple: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o

执行

$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
make: 'simple' is up to date.

下面我们来验证一下如果对 foo.c 进行改动,是不是 make 能正确的发现并从新构建所需。对于make 工具,一个文件是否改动不是看文件大小,而是其时间戳。在我的环境中只需用 touch 命令来改变文件的时间戳就行了,这相当于模拟了对文件进行了一次编辑,而不需真正对其进行编辑。图1.28 列出了所有相关的命令操作,从最终的结果来看,make 发现了 foo.c 需要重新被编译,而这,最终也导致了 simple 需要重新被编译。
执行

$ls -l foo.c
-rw-r--r-- 1 Administrator None 70 Aug 14 07:38 foo.c$touch foo.c$ls -l foo.c
-rw-r--r-- 1 Administrator None 70 Aug 14 08:48 foo.c$make
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o

至此,你完全明白了什么是目标的依赖关系以及 make 选择哪些目标需要重新编译的工作原理。掌握如果在头脑中勾画(当然初学时,可以用纸画一画)出我们想让 make 做的事的“依赖树”是编写 Makefile 最为重要和关键的一步。后面我们需要做的是让 Makefile 更加的简单但却更加的强大。

1.4.5 编译程序-伪对象.PHONY

在这里插入图片描述

  1. 在程序所在的目录创建一个 clean 文件
    .PHONY: main clean
  2. 执行make clean
  3. 提示:make: ‘clean’ is up to date

在前面的 sample 项目中,现在假设在程序所在的目录下面有一个 clean 文件,这个文件也可以通过 touch 命令来创建。创建以后,运行 make clean 命令,你会发现 make 总是提示 clean 文件是最新的,而不是按我们所期望的那样进行文件删除操作,如图 1.29 所示。从原理上我们还是可以理解的,这是因为 make 将 clean 当作文件,且在当前目录找到了这个文件,加上 clean 目标没有任何先决条件,所以,当我们要求 make 为我们构建 clean 目标时,它就会认为 clean 是最新的。
执行

$ls -l clean
ls: cannot access clean: No such file or directory$touch clean$ls -l clean
-rw-r--r-- 1 Administrator None 70 Aug 13:01 clean$make clean
make: `clean' is up to date.

那对于这种情况,在现实中也难免存在所定义的目标与所存在的文件是同名的,采用 Makefile如何处理这种情况呢?Makefile 中的假目标(phony target)可以解决这个问题。假目标可以采用.PHONY 关键字来定义,需要注意的是其必须是大写字母。图 1.30 是将 clean 变为假目标后的Makefile,更改后运用 make clean 命令的结果。

Makefile

.PHONY: clean
simple: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o

执行

$make clean
rm simple main.o foo.o

正如你所看到的,采用.PHONY 关键字声明一个目标后,make 并不会将其当作一个文件来处理,而只是当作一个概念上的目标。对于假目标,我们可以想像的是由于并不与文件关联,所以每一次 make 这个假目标时,其所在的规则中的命令都会被执行。

1.5 变量

只要是从事程序开发的,对于变量都很熟悉,因为每一个编程语言都有变量的概念。为了方便使用,Makefile 中也有变量的概念,我们可以在 Makefile 中通过使用变量来使得它更简洁、更具可维护性。下面,我们来看一看如何通过使用变量来提高 simple 项目 Makefile 的可维护性,例如:

.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)$(CC) -o $(EXE) $(OBJS)
main.o: main.c$(CC) -o main.o -c main.c
foo.o: foo.c$(CC) -o foo.o -c foo.c
clean:$(RM) $(EXE) $(OBJS)

一个变量的定义很简单,就是一个名字(变量名)后面跟上一个等号,然后在等号的后面放这个变量所期望的值。对于变量的引用,则需要采用 ( 变量名 ) 或者 (变量名)或者 (变量名)或者{变量名}这种模式。在这个 Makefile 中,我们引入了 CC 和 RM 两个变量,一个用于保存编译器名,而另一个用于指示删除文件的命令是什么。还有就是引入了 EXE 和 OBJS 两个变量,一个用于存放可执行文件名,可另一个则用于放置所有的目标文件名。采用变量的话,当我们需要更改编译器时,只需更改变量赋值的地方,非常方便,如果不采用变量,那我们得更改每一个使用编译器的地方,很是麻烦。显然,变量的引入增加了 Makefile 的可维护性。你可能会问,既然定义了一个 CC 变量,那是不是要将-o 或是-c 命令参数也定义成为一个变量呢?好主意!的确,如果我们更改了一个编译器,那么很有可能其使用参数也得跟着改变。现在,我们不急着这么去做,为什么?因为后面我们还会对 Makefile 进行简化,到时再改变也来得及,现在我们只是将注意力放在变量的使用上。

1.5.1 自动变量

对于每一个规则,不知你是否觉得目标和先决条件的名字会在规则的命令中多次出,每一次出现都是一种麻烦,更为麻烦的是,如果改变了目标或是依赖的名,那得在命令中全部跟着改。有没有简化这种更改的方法呢?这我们需要用到 Makefile 中的自动变量,它们包括:
$@用于表示一个规则中的目标。当我们的一个规则中有多个目标时,$@所指的是其中任何造成命令被运行的目标。
$^则表示的是规则中的所有先择条件。
$<表示的是规则中的第一个先决条件。
除了上面的两个自动变量,在 Makefile 中还有其它的动变量,我们在需要的时候再提及,就simple 项目的 Makefile 而言,为了简化它,采用这些变量就足够了。下面是用于测试上面三个自动变量的值的 Makefile,其运行结果中可以找到。需要注意的是,在 Makefile 中$具有特殊的意思,因此,如果想采用 echo 输出$,则必需用两个连着的$。还有就是,$@对于 Shell 也有特殊的意思,我们需要在$$@之前再加一个脱字符\。如果你还有困惑,你可以通过改一改 Makefile 来验证它。
Makefile

.PHONY: all
all: first second third@echo "\$$@ = $@"@echo "$$^ = $^"@echo "$$< = $<"
first second third:

执行

$make
$@ = all
$^ = first second third
$< = first

采用自动变量后 simple 项目的 Makefile 可以被重写为下面所示,用了自动变量以后这个Makefile 看起来有点怪怪的,有些什么^@,等等。这就对了,你所看到的 Makefile 看起来不都很奇怪吗?我们要的就是这个“味”!
Makefile

.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)$(CC) -o $@ $^
main.o: main.c$(CC) -o $@ -c $^
foo.o: foo.c$(CC) -o $@ -c $^
clean:$(RM) $(EXE) $(OBJS)

自动变量在对它们还不熟悉时,看起来可能有那么一点吃力,但熟悉了你就会觉得其简捷(洁),那时也会觉得它们好用。这有点像我们用惯了 Windows 操作系统,刚开始用 Linux 时很不适应,比如在 Linux 中,ls 表示的是查看目录的文件,这名字就是有那么一点怪。但当我们对于 Linux 熟悉了,你会发现在 Linux 平台上工作,非常的自由和方便,远比 Windows 上灵活。从这个问题来看,设计是有一个平衡点的,Windows 容易上手,但它把用户当作“傻子”,对于高级用户来说却不方便;而 Linux 则更难上手,但一旦上手后,功能却更强大。
至此,你有没有觉得我们的 Makefile 更加的酷了呢?当然,我们写 Makefile 的目的不是为了让
其酷得不能理解,相反是为了更简单和易于维护,这里的酷是指其看起来更专业。
在这里插入图片描述

1.5.2 依赖第三方库

在这里插入图片描述
在这里插入图片描述
如果想更深入了解可以看看这个

2 cmake

CMake是一个开源、跨平台的工具系列,是用来构建、测试和打包软件。
CMake使用平台无关的配置文件来控制软件编译过程,并生成可在您选择的编译器环境中使用项目文件,比如可以生成vs项目文件或者makefile。CMake工具套件由Kitware公司创建,以满足ITK和VTK等开源项目对跨平台构建环境的需求。Kitware是一家从事医疗计算,高性能的可视化和计算,数据和分析,计算机视觉的公司。该公司成立于1998年。

2.1 CMake介绍

假如我们有一个深度学习框架的部分工程列表,里面有超过40个互相调用的工程共同组成,一些用于生成库文件,一些用于实现逻辑功能。他们之间的调用关系复杂而严格,如果我想在这样复杂的框架下进行二次开发,显然只拥有它的源码是远远不够的,还需要清楚的明白这几十个项目之间的复杂关系,在没有原作者的帮助下进行这项工作几乎是不可能的。
  即使是原作者给出了相关的结构文档,对新手来说建立工程的过程依旧是漫长而艰辛的,因此CMake的作用就凸显出来了。原作者只需要生成一份CMakeLists.txt文档,框架的使用者们只需要在下载源码的同时下载作者提供的CMakeLists.txt,就可以利用CMake,在”原作者的帮助下“进行工程的搭建。
  打个更通俗易懂的比喻,小利在路边捡瓶盖赚了500万准备买房,但是小利这一麻袋的5毛、一块、十块、五十、一百售楼处的小姐姐嫌麻烦不想收这些钱,那怎么办呢?小姐姐建议小利把钱拿到银行去换成一张银行卡,然后直接来刷卡就行啦!CMake这里就扮演银行的角色,帮你去整理你的资产。

2.1.1 CMake主要功能

配置和生成各大平台的工程(VS的vcxproj,Qt的Pro)
  比如设置输出目录,设置编译对象的debug后缀,设置源码在工程中的那个文件夹(Filter),配置需要依赖的第三方的头文件目录,库目录等等属性。

生成makefile文件
  计算机编译源文件的时候是一条指令一条指令的发送给编译器执行的,这样效率很低下,所以就产生了一种文件,把所有的命令写到一个文件中,这个文件就是makefile。CMake生成了这个makefile之后,各大平台的编译器都会拿到这个makefile然后解析它。将他的命令解析出来一条一条执行。

2.1.2 什么是makefile?

或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但要作一个好的和professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义。

特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。 make 是一个命令工具,是一个解释 makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Visual C++ 的 nmake,QtCreator 的 qmake 等。

make 工具在构造项目的时候需要加载一个叫做 makefile 的文件,makefile 关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile 定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile 就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。

makefile 带来的好处就是 ——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。

makefile 文件有两种命名方式 makefile 和 Makefile,(注意:makefile文件就叫这个名字,是前面这两种文件名,并且没有扩展名或者后缀),构建项目的时候在哪个目录下执行构建命令 make 这个目录下的 makefile 文件就会别加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中。

2.1.3 为什么要用makefile?

对于一个大型软件,其编译、维护是一个复杂而耗时的过程。它涉及到大量的文件、目录,这些文件可能是在不同的时间、由不同的人、在不同的地方分别写的,其中一些是程序,有些是数据,有些是文档,有些是衍生文件。

甚至参与开发的人员也不一定清楚所有文件的细节,包括如何处理它们。此外,构成软件的文件数目可能达到成百上千,甚至成千上万个,开发过程中当修改了少量几个文件后,往往只需要重新编译、生成少数几个文件。有效地描述这些文件之间的依赖关系以及处理命令,当个别文件改动后仅执行必要的处理,而不必重复整个编译过程,可以大大提高软件开发的效率。

2.1.4 CMakeLists.txt介绍

CMakelists.txt是Cmake的配置文件,用于描述编译方式和项目依赖。CMakeLists.txt文件是CMake编译系统编译软件包过程的输入文件,任何CMake兼容包都包含一个或多个CMakeLists.txt文件,这些文件描述了如何编译代码以及将其安装到哪里。在windows下CMake 会读取 CMakeLists.txt 中的设置,并生成项目的 makefile 或 Visual Studio 工程文件。

makefile文件的编写实在是个繁琐的事,于是,CMake出现了,使得这一切变得简单,CMake通过CMakeLists.txt读入所有源文件自动生成makefile,进而将源文件编译成可执行文件或库文件。

2.2 初试 cmake – cmake 的 helloworld

本节选择了一个最简单的例子Helloworld 来演练一下cmake 的完整构建过程,本节并不会深入的探讨cmake,仅仅展示一个简单的例子,并加以粗略的解释。

2.2.1 准备工作

首先,建立一个cmake 目录,用来放置我们学习过程中的所有练习。 mkdir -p cmake
以后我们所有的cmake练习都会放在cmake的子目录下(你也可以自行安排目录,这个并不是限制,仅仅是为了叙述的方便)
然后在cmake建立第一个练习目录t1

cd cmake
mkdir t1
cd t1

t1(hello-world) 目录建立main.cCMakeLists.txt(注意文件名大小写):

main.c 文件内容:
//main.c

#include <stdio.h>
int main()
{printf("Hello World from t1 Main!\n"); return 0;
}

CmakeLists.txt 文件内容:

PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
ADD_EXECUTABLE(hello2 ${SRC_LIST})

在这里插入图片描述

2.2.2 开始构建

所有的文件创建完成后,t1 目录中应该存在main.cCMakeLists.txt 两个文件接下来我们来构建这个工程,在这个目录运行:

cmake .(注意命令后面的点号,代表本目录)。

输出大概是这个样子:
在这里插入图片描述
再让我们看一下目录中的内容, 你会发现,系统自动生成了:
CMakeFiles, CMakeCache.txt, cmake_install.cmake等文件,并且生成了Makefile.
在这里插入图片描述现在不需要理会这些文件的作用,以后你也可以不去理会。最关键的是,它自动生成了Makefile.
然后进行工程的实际构建,在这个目录输入make命令,大概会得到如下的彩色输出:
Scanning dependencies of target hello
[100%] Building C object CMakeFiles/hello.dir/main.o Linking C executable hello
[100%] Built target hello
在这里插入图片描述

如果你需要看到make构建的详细过程,可以使用make VERBOSE=1或者VERBOSE=1 make命令来进行构建。
这时候,我们需要的目标文件hello 已经构建完成,
在这里插入图片描述

位于当前目录,尝试运行一下:
./hello
得到输出:
Hello World from Main
在这里插入图片描述
恭喜您,到这里为止您已经完全掌握了 cmake 的使用方法。

然后就是怎么清理工程
make clean
即可对构建结果进行清理。

2.3 简单的解释

我们来重新看一下CMakeLists.txt,这个文件是cmake的构建定义文件,文件名是大小写相关的,如果工程存在多个目录,需要确保每个要管理的目录都存在一个CMakeLists.txt。(关于多目录构建,后面我们会提到,这里不作过多解释)。

上面例子中的CMakeLists.txt文件内容如下:

PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})

2.3.1 PROJECT

PROJECT 指令的语法是:

PROJECT(projectname [CXX] [C] [Java])

你可以用这个指令定义工程名称,并可指定工程支持的语言,支持的语言列表是可以忽略的,默认情况表示支持所有语言。这个指令隐式的定义了两个cmake变量: <projectname>_BINARY_DIR以及<projectname>_SOURCE_DIR,这里就是HELLO_BINARY_DIRHELLO_SOURCE_DIR(所以CMakeLists.txt中两个MESSAGE
指令可以直接使用了这两个变量),因为采用的是内部编译,两个变量目前指的都是工程所在路cmake/t1,后面我们会讲到外部编译,两者所指代的内容会有所不同。

同时cmake系统也帮助我们预定义了PROJECT_BINARY_DIRPROJECT_SOURCE_DIR变量,他们的值分别跟HELLO_BINARY_DIRHELLO_SOURCE_DIR一致。

为了统一起见,建议以后直接使用PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。如果使用了
<projectname>_SOURCE_DIR,修改工程名称后,需要同时修改这些变量。

2.3.2 SET

SET指令的语法是:
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
现阶段,你只需要了解SET指令可以用来显式的定义变量即可。
比如我们用到的是SET(SRC_LIST main.c),如果有多个源文件,也可以定义成:
SET(SRC_LIST main.c t1.c t2.c)

2.3.3 MESSAGE

MESSAGE指令的语法是:
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)
这个指令用于向终端输出用户定义的信息,包含了三种类型:
SEND_ERROR,产生错误,生成过程被跳过。
SATUS,输出前缀为—的信息。
FATAL_ERROR,立即终止所有cmake过程.

我们在这里使用的是STATUS信息输出,演示了由PROJECT指令定义的两个隐式变量HELLO_BINARY_DIRHELLO_SOURCE_DIR

2.3.4 ADD_EXECUTABLE

ADD_EXECUTABLE(hello ${SRC_LIST})
定义了这个工程会生成一个文件名为hello的可执行文件,相关的源文件是SRC_LIST中定义的源文件列表, 本例中你也可以直接写成ADD_EXECUTABLE(hello main.c)

在本例我们使用了${}来引用变量,这是cmake的变量应用方式,但是,有一些例外,比如在IF控制语句,变量是直接使用变量名引用,而不需要${}。如果使用了${}去应用变量,其实IF会去判断名为${}所代表的值的变量,那当然是不存在的了。

将本例改写成一个最简化的CMakeLists.txt:

PROJECT(HELLO)
ADD_EXECUTABLE(hello main.c)

2.4 基本语法规则

前面提到过,cmake其实仍然要使用”cmake 语言和语法”去构建,上面的内容就是所谓的 ”cmake 语言和语法”,最简单的语法规则是:

  1. 变量使用${}方式取值,但是在IF控制语句中是直接使用变量名 ,

  2. 指令(参数1 参数 2...)
    a. 参数使用括弧括起,参数之间使用空格或分号分开。
    以上面的ADD_EXECUTABLE指令为例,如果存在另外一个func.c源文件,就要写成:
    ADD_EXECUTABLE(hello main.c func.c)或者ADD_EXECUTABLE(hello main.c;func.c)

  3. 指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。
    上面的MESSAGE指令我们已经用到了这条规则:
    MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR})
    也可以写成:
    MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)
    这里需要特别解释的是作为工程名的HELLO和生成的可执行文件hello是没有任何关系的。

  4. 工程名和执行文件
    hello定义了可执行文件的文件名,你完全可以写成:ADD_EXECUTABLE(t1 main.c)编译后会生成一个t1 可执行文件。

  5. 关于语法的疑惑
    cmake的语法还是比较灵活而且考虑到各种情况,比如
    SET(SRC_LIST main.c)也可以写成SET(SRC_LIST “main.c”)
    是没有区别的,但是假设一个源文件的文件名是 fu nc.c(文件名中间包含了空格)。
    这时候就必须使用双引号,如果写成了SET(SRC_LIST fu nc.c),就会出现错误,提示你找不到fu 文件和nc.c文件。这种情况,就必须写成:
    SET(SRC_LIST "fu nc.c")
    此外,你可以可以忽略掉source 列表中的源文件后缀,比如可以写成
    ADD_EXECUTABLE(t1 main)cmake会自动的在本目录查找main.c或者main.cpp等,当然,最好不要偷这个懒,以免这个目录确实存在一个main.c一个main.
    同时参数也可以使用分号来进行分割。
    下面的例子也是合法的:
    ADD_EXECUTABLE(t1 main.c t1.c)可以写成ADD_EXECUTABLE(t1 main.c;t1.c).
    我们只需要在编写CMakeLists.txt时注意形成统一的风格即可。

  6. 清理工程:
    跟经典的autotools 系列工具一样,运行:
    make clean
    即可对构建结果进行清理。

  7. 问题?问题!
    “我尝试运行了 make distclean,这个指令一般用来清理构建过程中产生的中间文件的,如果要发布代码,必然要清理掉所有的中间文件,但是为什么在 cmake 工程中这个命令是无效的?”
    是的,cmake 并不支持 make distclean,关于这一点,官方是有明确解释的:
    因为CMakeLists.txt 可以执行脚本并通过脚本生成一些临时文件,但是却没有办法来跟踪这些临时文件到底是哪些。因此,没有办法提供一个可靠的 make distclean 方案。
    Some build trees created with GNU autotools have a “make distclean” target that cleans the build and also removes Makefiles and other parts of the generated build system. CMake does not generate a “make distclean” target because CMakeLists.txt files can run scripts and arbitrary commands; CMake has no way of tracking exactly which files are generated as part of running
    CMake. Providing a distclean target would give users the false impression that it would work as expected. (CMake does generate a “make clean” target to remove files generated by the compiler and linker.)
    A “make distclean” target is only necessary if the user performs an in-source build. CMake supports in-source builds, but we strongly encourage users to adopt the notion of an out-of-source build. Using a build tree that is separate from the source tree will prevent CMake from generating any files in the source tree. Because CMake does not change the source tree, there is no need for a distclean target. One can start a fresh build by deleting the build tree or creating a separate build tree.
    同时,还有另外一个非常重要的提示,就是:我们刚才进行的是内部构建(in-source build),而 cmake 强烈推荐的是外部构建(out-of-source build)。

  8. 内部构建与外部构建:
    上面的例子展示的是“内部构建”,相信看到生成的临时文件比您的代码文件还要多的时候,估计这辈子你都不希望再使用内部构建。
    对于cmake,内部编译上面已经演示过了,它生成了一些无法自动删除的中间文件,所以,引出了我们对外部编译的探讨,外部编译的过程如下:
    1.首先,请清除t1目录中除main.c CmakeLists.txt之外的所有中间文件,最关键的CMakeCache.txt在这里插入图片描述
    2.在t1目录中建立build目录,当然你也可以在任何地方建立build目录,不一定必须在工程目录中。在这里插入图片描述
    3.进入build目录,运行cmake ..(注意,…代表父目录,因为父目录存在我们需要的CMakeLists.txt,如果你在其他地方建立了build目录,需要运行cmake <工程的全路径>,查看一下build 目录,就会发现了生成了编译需要的Makefile以及其他的中间文件.
    在这里插入图片描述
    4.运行make构建工程,就会在当前目录(build 目录)中获得目标文件hello
    在这里插入图片描述
    上述过程就是所谓的out-of-source外部编译,一个最大的好处是,对于原有的工程没有任何影响,所有动作全部发生在编译目录。通过这一点,也足以说服我们全部采用外部编译方式构建工程。
    这里需要特别注意的是:
    通过外部编译进行工程构建,HELLO_SOURCE_DIR仍然指代工程路径,即cmake/t1
    HELLO_BINARY_DIR则指代编译路径,即cmake/t1/build

  9. 小结:
    本小节描述了使用cmake构建Hello World程序的全部过程,并介绍了三个简单的指令:
    PROJECT/MESSAGE/ADD_EXECUTABLE以及变量调用的方法,同时提及了两个隐式变量 <projectname>_SOURCE_DIR<projectname>_BINARY_DIR,演示了变量调用的方法,从这个过程来看,有些开发者可能会想,这实在比我直接写 Makefile 要复杂多了,甚至我都可以不编写Makefile,直接使用gcc main.c即可生成需要的目标文件。是的,正如第一节提到的,如果工程只有几个文件,还是直接编写Makefile最简单。但是, kdelibs压缩包达到了50 多M,您认为使用什么方案会更容易一点呢?
    下一节,我们的任务是让HelloWorld 看起来更像一个工程。
    剩下的看这个链接

2.5 静态库与动态库构建

从本节开始,我们不再折腾Hello World 了,我们来折腾Hello World 的共享库。
本节的任务:

  1. 建立一个静态库和动态库,提供HelloFunc函数供其他程序编程使用,HelloFunc向终端输出Hello World 字符串。
  2. 安装头文件与共享库。

2.5.1 准备工作

在cmake 目录建立 t3 目录,用于存放本节涉及到的工程

2.5.2 建立共享库

cd make/t3
mkdir lib

t3目录下建立CMakeLists.txt,内容如下:

PROJECT(HELLOLIB) 
ADD_SUBDIRECTORY(lib)

lib目录下建立两个源文件hello.chello.h
hello.c 内容如下:

#include "hello.h"
void hello_func(void) {printf("Hello World!\n");return;
}

hello.h 内容如下:

#ifndef HELLO_H_
#define HELLO_H_ (1)
#include <stdio.h>
void hello_func(void);
#endif

在 lib 目录下建立CMakeLists.txt,内容如下:

SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC}

2.5.3 编译共享库

仍然采用out-of-source 编译的方式,按照习惯,我们建立一个build目录,在 build目录中
cmake ..
make
在这里插入图片描述
这时,你就可以在lib 目录得到一个libhello.so,这就是我们期望的共享库。
在这里插入图片描述
如果你要指定libhello.so生成的位置,可以通过在主工程文件CMakeLists.txt中修
ADD_SUBDIRECTORY(lib)指令来指定一个编译输出位置或者在lib/CMakeLists.txt中添加 SET(LIBRARY_OUTPUT_PATH <路径>)来指定一个新的位置。

这两者的区别我们上一节已经提到了,所以,这里不再赘述,下面,我们解释一下一个新的指令ADD_LIBRARY

ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL]
source1 source2 ... sourceN)

你不需要写全libhello.so,只需要填写hello即可,cmake 系统会自动为你生成libhello.X

类型有三种:

  • SHARED,动态库
  • STATIC,静态库
  • MODULE,在使用 dyld 的系统有效,如果不支持dyld,则被当作SHARED 对待。

EXCLUDE_FROM_ALL 参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建。

2.5.4 添加静态库

同样使用上面的指令,我们在支持动态库的基础上再为工程添加一个静态库,按照一般的习惯,静态库名字跟动态库名字应该是一致的,只不过后缀是.a罢了。

下面我们用这个指令再来添加静态库:

ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

然后再在build 目录进行外部编译,我们会发现,静态库根本没有被构建,仍然只生成了一个动态库。因为hello 作为一个target 是不能重名的,所以,静态库构建指令无效。

如果我们把上面的hello修改为hello_static:

ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

就可以构建一个libhello_static.a的静态库了。

这种结果显示不是我们想要的,我们需要的是名字相同的静态库和动态库,因为 target 名称是唯一的,所以,我们肯定不能通过 ADD_LIBRARY 指令来实现了。这时候我们需要用到另外一个指令:

SET_TARGET_PROPERTIES,其基本语法是:
SET_TARGET_PROPERTIES(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)

这条指令可以用来设置输出的名称,对于动态库,还可以用来指定动态库版本和 API 版本。

在本例中,我们需要作的是向lib/CMakeLists.txt中添加一条:
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")这样,我们就可以同时得到libhello.so/libhello.a 两个库了。
在这里插入图片描述
在这里插入图片描述

与他对应的指令是:

GET_TARGET_PROPERTY(VAR target property)

具体用法如下例,我们向lib/CMakeListst.txt中添加:

GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)
MESSAGE(STATUS “This is the hello_static OUTPUT_NAME:”${OUTPUT_VALUE})

如果没有这个属性定义,则返回NOTFOUND.

让我们来检查一下最终的构建结果,我们发现,libhello.a已经构建完成,位于 build/lib 目录中,但是libhello.so 去消失了。这个问题的原因是:cmake 在构建一个新的target 时,会尝试清理掉其他使用这个名字的库,因为,在构建libhello.a 时,就会清理掉libhello.so.
为了回避这个问题,比如再次使用SET_TARGET_PROPERTIES定义CLEAN_DIRECT_OUTPUT属性。
向 lib/CMakeLists.txt 中添加:

SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

这时候,我们再次进行构建,会发现build/lib 目录中同时生成了libhello.so 和 libhello.a

2.5.5 动态库版本号

按照规则,动态库是应该包含一个版本号的,我们可以看一下系统的动态库,一般情况是

libhello.so.1.2
libhello.so ->libhello.so.1
libhello.so.1->libhello.so.1.2

为了实现动态库版本号,我们仍然需要使用 SET_TARGET_PROPERTIES 指令。

具体使用方法如下:

SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1) VERSION 指代动态库版本,
SOVERSION 指代 API 版本。

在这里插入图片描述
将上述指令加入lib/CMakeLists.txt 中,重新构建看看结果。
在 build/lib 目录会生成: libhello.so.1.2 libhello.so.1->libhello.so.1.2 libhello.so ->libhello.so.1
在这里插入图片描述

2.5.6 安装共享库和头文件

以上面的例子,我们需要将libhello.a, libhello.so.x 以及hello.h安装到系统目录,才能真正让其他人开发使用,在本例中我们将hello的共享库安装到<prefix>/lib目录,将hello.h安装到<prefix>/include/hello目录。
利用上一节了解到的INSTALL指令,我们向lib/CMakeLists.txt中添加如下指令:

INSTALL(TARGETS hello hello_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
INSTALL(FILES hello.h DESTINATION include/hello)

注意,静态库要使用ARCHIVE 关键字

通过:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
make install

在这里插入图片描述

我们就可以将头文件和共享库安装到系统目录/usr/lib/usr/include/hello中了。
在这里插入图片描述
在这里插入图片描述

如果报错
CMake Error: cmake_symlink_library: System Error: Operation not supported
CMake Error: cmake_symlink_library: System Error: Operation not supported
make[2]: *** [lib/CMakeFiles/hello_dynamic.dir/build.make:85: lib/libhello.so.1.2] Error 1
make[2]: *** Deleting file ‘lib/libhello.so.1.2’
make[1]: *** [CMakeFiles/Makefile2:130: lib/CMakeFiles/hello_dynamic.dir/all] Error 2
make: *** [Makefile:130: all] Error 2
则说明你你可能是通过hgfs共享的Windows目录下进行make,生成so时需要在纯linux环境运行,比如把t3拷贝到~/0voice/makefile/t3, 并且先把对应的build目录里面的内容清空,重新cmake … 再make

2.5.7 小结

本小节,我们谈到了:
如何通过ADD_LIBRARY指令构建动态库和静态库。
如何通过SET_TARGET_PROPERTIES同时构建同名的动态库和静态库。如何通过SET_TARGET_PROPERTIES控制动态库版本
最终使用上一节谈到的INSTALL指令来安装头文件和动态、静态库。
在下一节,我们需要编写另一个高级一点的 Hello World 来演示怎么使用我们已经构建的构建的共享库libhello 和外部头文件。

2.6 如何使用外部共享库和头文件

抱歉,本节仍然继续折腾Hello World.
上一节我们已经完成了libhello 动态库的构建以及安装,本节我们的任务很简单:
编写一个程序使用我们上一节构建的共享库。

2.6.1 准备工作

请在cmake 目录建立 t4 目录,本节所有资源将存储在t4 目录。

2.6.2 构建工程

重复以前的步骤,建立src 目录,编写源文件main.c,内容如下:

#include "hello.h"
int main(void) {hello_func();return 0;
}

编写工程主文件CMakeLists.txt

PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)

编写src/CMakeLists.txt

ADD_EXECUTABLE(main main.c)

上述工作已经严格按照我们前面几节提到的内容完成了。

2.6.3 外部构建

按照习惯,仍然建立build目录,使用cmake ..方式构建。
过程:

cmake ..
make

构建失败,如果需要查看细节,可以使用第一节提到的方法
make VERBOSE=1来构建
错误输出为是:
cmake/t4/src/main.c:1:19: error: hello.h: 没有那个文件或目录
在这里插入图片描述

2.6.4 引入头文件搜索路径

hello.h 位于/usr/include/hello目录中,并没有位于系统标准的头文件路径,
(有人会说了,白痴啊,你就不会include <hello/hello.h>,同志,要这么干,我这一节就没什么可写了,只能选择一个glib或者libX11来写了,这些代码写出来很多同志是看不懂的)
为了让我们的工程能够找到hello.h头文件,我们需要引入一个新的指令
INCLUDE_DIRECTORIES,其完整语法为:
INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面,你可以通过两种方式来进行控制搜索路径添加的方式:

  1. CMAKE_INCLUDE_DIRECTORIES_BEFORE,通过SET这个cmake变量为on,可以将添加的头文件搜索路径放在已有路径的前面。
  2. 通过AFTER或者BEFORE参数,也可以控制是追加还是置前。

现在我们在src/CMakeLists.txt中添加一个头文件搜索路径,方式很简单,加入:

INCLUDE_DIRECTORIES(/usr/include/hello)

在这里插入图片描述
进入build 目录,重新进行构建,这是找不到hello.h的错误已经消失,但是出现了一个新的错误:
main.c:(.text+0x12): undefined reference to "HelloFunc'
因为我们并没有link到共享库libhello上。
在这里插入图片描述

2.6.5 为 target 添加共享库

我们现在需要完成的任务是将目标文件链接到 libhello,这里我们需要引入两个新的指令
LINK_DIRECTORIESTARGET_LINK_LIBRARIES
LINK_DIRECTORIES的全部语法是:
LINK_DIRECTORIES(directory1 directory2 ...)
这个指令非常简单,添加非标准的共享库搜索路径,比如,在工程内部同时存在共享库和可执行二进制,在编译时就需要指定一下这些共享库的路径。这个例子中我们没有用到这个指令。
TARGET_LINK_LIBRARIES 的全部语法是:

TARGET_LINK_LIBRARIES(target library1
<debug | optimized> library2
...)

这个指令可以用来为target 添加需要链接的共享库,本例中是一个可执行文件,但是同样可以用于为自己编写的共享库添加共享库链接。

为了解决我们前面遇到的HelloFunc未定义错误,我们需要作的是向 src/CMakeLists.txt 中添加如下指令:

TARGET_LINK_LIBRARIES(main hello)
也可以写成
TARGET_LINK_LIBRARIES(main libhello.so)

在这里插入图片描述

这里的hello指的是我们上一节构建的共享库libhello.
进入build 目录重新进行构建。

cmake ..
make

这是我们就得到了一个连接到libhello的可执行程序main,位于build/bin目录,运
main的结果是输出:
Hello World
在这里插入图片描述

让我们来检查一下main 的链接情况:
ldd main

linux-vdso.so.1 (0x00007ffd43bf7000)
libhello.so.1 => /lib/libhello.so.1 (0x00007f8e13140000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e12f4e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8e1315b000)

在这里插入图片描述
可以清楚的看到main确实链接了共享库libhello,而且链接的是动态库libhello.so.1
那如何链接到静态库呢?
方法很简单:
将 TARGET_LINK_LIBRRARIES 指令修改为: TARGET_LINK_LIBRARIES(main libhello.a)
重新构建后再来看一下main 的链接情况
ldd main

 linux-vdso.so.1 (0x00007ffc42575000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcaade3e000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcaae046000)

在这里插入图片描述

说明,main 确实链接到了静态库libhello.a

2.6.6 特殊的环境变量CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH

务必注意,这两个是环境变量而不是cmake变量。
使用方法是要在bash中用export或者在csh中使用set命令设置或者 CMAKE_INCLUDE_PATH=/home/include cmake ..等方式。

这两个变量主要是用来解决以前autotools工程中
--extra-include-dir等参数的支持的。

也就是,如果头文件没有存放在常规路径(/usr/include, /usr/local/include等),则可以通过这些变量就行弥补。
我们以本例中的hello.h为例,它存放在/usr/include/hello目录,所以直接查找肯定是找不到的。

前面我们直接使用了绝对路径INCLUDE_DIRECTORIES(/usr/include/hello)告诉工程这个头文件目录。

为了将程序更智能一点,我们可以使用CMAKE_INCLUDE_PATH来进行,使用bash的方法如下:
export CMAKE_INCLUDE_PATH=/usr/include/hello

然后在头文件中将INCLUDE_DIRECTORIES(/usr/include/hello)替换为:

FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)

上述的一些指令我们在后面会介绍。

这里简单说明一下,FIND_PATH用来在指定路径中搜索文件名,比如:
FIND_PATH(myHeader NAMES hello.h PATHS /usr/include /usr/include/hello)
这里我们没有指定路径,但是,cmake 仍然可以帮我们找到hello.h 存放的路径,就是因为我们设置了环境变量CMAKE_INCLUDE_PATH

如果你不使用FIND_PATHCMAKE_INCLUDE_PATH 变量的设置是没有作用的,你不能指望它会直接为编译器命令添加参数-I<CMAKE_INCLUDE_PATH>

以此为例,CMAKE_LIBRARY_PATH可以用在FIND_LIBRARY中。

同样,因为这些变量直接为FIND_指令所使用,所以所有使用FIND_指令的cmake模块都会受益。

2.6.7 小节

本节我们探讨了:
如何通过INCLUDE_DIRECTORIES指令加入非标准的头文件搜索路径。
如何通过LINK_DIRECTORIES指令加入非标准的库文件搜索路径。
如果通过TARGET_LINK_LIBRARIES为库或可执行二进制加入库链接。

并解释了如果链接到静态库。

到这里为止,您应该基本可以使用cmake 工作了,但是还有很多高级的话题没有探讨,比如编译条件检查、编译器定义、平台判断、如何跟 pkgconfig 配合使用等等。

到这里,或许你可以理解前面讲到的“cmake 的使用过程其实就是学习cmake 语言并编写 cmake 程序的过程”,既然是“cmake 语言”,自然涉及到变量、语法等.

下一节,我们将抛开程序的话题,看看常用的 CMAKE 变量以及一些基本的控制语法规则。

2.7 CMake 常用指令

前面我们讲到了cmake 常用的变量,相信“cmake 即编程”的感觉会越来越明显,无论如何,我们仍然可以看到cmake 比autotools 要简单很多。接下来我们就要集中的看一看 cmake 所提供的常用指令。在前面的章节我们已经讨论了很多指令的用法,如
PROJECT,ADD_EXECUTABLE,INSTALL,ADD_SUBDIRECTORY,SUBDIRS,INCLUDE _DIRECTORIES,LINK_DIRECTORIES,TARGET_LINK_LIBRARIES,SET 等。

本节会引入更多的cmake 指令,为了编写的方便,我们将按照 cmake man page 的顺序来介绍各种指令,不再推荐使用的指令将不再介绍,INSTALL 系列指令在安装部分已经做了非常详细的说明,本节也不在提及。(你可以将本章理解成选择性翻译,但是会加入更多的个人理解)

2.7.1 基本指令

  1. ADD_DEFINITIONS
    向C/C++编译器添加-D 定义,比如:
    ADD_DEFINITIONS(-DENABLE_DEBUG -DABC),参数之间用空格分割。
    如果你的代码中定义了#ifdef ENABLE_DEBUG #endif,这个代码块就会生效。
    如果要添加其他的编译器开关,可以通过CMAKE_C_FLAGS变量和CMAKE_CXX_FLAGS变量设置。

  2. ADD_DEPENDENCIES
    定义target 依赖的其他target,确保在编译本target 之前,其他的target 已经被构建。
    ADD_DEPENDENCIES(target-name depend-target1 depend-target2 ...)

  3. ADD_EXECUTABLEADD_LIBRARYADD_SUBDIRECTORY 前面已经介绍过了,这里不再罗唆。

  4. ADD_TESTENABLE_TESTING指令。
    ENABLE_TESTING指令用来控制Makefile是否构建test目标,涉及工程所有目录。语
    法很简单,没有任何参数,ENABLE_TESTING(),一般情况这个指令放在工程的主CMakeLists.txt中.
    ADD_TEST指令的语法是:
    ADD_TEST(testname Exename arg1 arg2 ...)
    testname是自定义的test名称,Exename 可以是构建的目标文件也可以是外部脚本等等。后面连接传递给可执行文件的参数。如果没有在同一个 CMakeLists.txt 中打开
    ENABLE_TESTING()指令,任何ADD_TEST都是无效的。
    比如我们前面的Helloworld例子,可以在工程主CMakeLists.txt中添加ADD_TEST(mytest ${PROJECT_BINARY_DIR}/bin/main) ENABLE_TESTING()
    生成Makefile后,就可以运行make test来执行测试了。

  5. AUX_SOURCE_DIRECTORY
    基本语法是:
    AUX_SOURCE_DIRECTORY(dir VARIABLE)
    作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表。因为目前cmake还不能自动发现新添加的源文件。
    比如

    AUX_SOURCE_DIRECTORY(. SRC_LIST)
    ADD_EXECUTABLE(main ${SRC_LIST})
    

    你也可以通过后面提到的FOREACH指令来处理这个LIST

  6. CMAKE_MINIMUM_REQUIRED
    其语法为CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])
    比如CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR)
    如果cmake版本小与2.5,则出现严重错误,整个过程中止。

  7. EXEC_PROGRAM
    CMakeLists.txt处理过程中执行命令,并不会在生成的 Makefile 中执行。具体语法为:

EXEC_PROGRAM(Executable [directory in which to run]
[ARGS <arguments to executable>]
[OUTPUT_VARIABLE <var>]
[RETURN_VALUE <var>])

用于在指定的目录运行某个程序,通过ARGS添加参数,如果要获取输出和返回值,可通过 OUTPUT_VARIABLERETURN_VALUE分别定义两个变量.
这个指令可以帮助你在CMakeLists.txt处理过程中支持任何命令,比如根据系统情况去修改代码文件等等。
举个简单的例子,我们要在src目录执行ls命令,并把结果和返回值存下来。
可以直接在src/CMakeLists.txt中添加:

EXEC_PROGRAM(ls ARGS "*.c" OUTPUT_VARIABLE LS_OUTPUT RETURN_VALUE LS_RVALUE)
IF(not LS_RVALUE)
MESSAGE(STATUS "ls result: " ${LS_OUTPUT})
ENDIF(not LS_RVALUE)

在 cmake 生成 Makefile 的过程中,就会执行ls 命令,如果返回0,则说明成功执行,那么就输出ls *.c 的结果。关于IF 语句,后面的控制指令会提到。

  1. FILE指令
    文件操作指令,基本语法为:
FILE(WRITE filename "message to write"... )FILE(APPEND filename "message to write"... )FILE(READ filename variable)FILE(GLOB variable [RELATIVE path] [globbing expressions]...)FILE(GLOB_RECURSE variable [RELATIVE path] [globbing expressions]...)FILE(REMOVE [directory]...)FILE(REMOVE_RECURSE [directory]...)FILE(MAKE_DIRECTORY [directory]...)FILE(RELATIVE_PATH variable directory file)FILE(TO_CMAKE_PATH path result)FILE(TO_NATIVE_PATH path result)

这里的语法都比较简单,不在展开介绍了。

  1. INCLUDE指令,用来载入CMakeLists.txt文件,也用于载入预定义的cmake模块. INCLUDE(file1 [OPTIONAL])
    INCLUDE(module [OPTIONAL])
    OPTIONAL参数的作用是文件不存在也不会产生错误。
    你可以指定载入一个文件,如果定义的是一个模块,那么将在CMAKE_MODULE_PATH中搜索这个模块并载入。
    载入的内容将在处理到INCLUDE语句是直接执行。

2.8.2 INSTALL 指令

INSTALL 系列指令已经在前面的章节有非常详细的说明,这里不在赘述,可参考前面的安装部分。

2.8.2 FIND_指令

FIND_系列指令主要包含一下指令:
FIND_FILE(<VAR> name1 path1 path2 ...)
VAR 变量代表找到的文件全路径,包含文件名

FIND_LIBRARY(<VAR> name1 path1 path2 ...)
VAR 变量表示找到的库全路径,包含库文件名

FIND_PATH(<VAR> name1 path1 path2 ...)
VAR 变量代表包含这个文件的路径。

FIND_PROGRAM(<VAR> name1 path1 path2 ...)
VAR 变量代表包含这个程序的全路径。

FIND_PACKAGE(<name> [major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS] [componets...]])
用来调用预定义在CMAKE_MODULE_PATH 下的Find.cmake 模块,你也可以自己定义Find模块,通过SET(CMAKE_MODULE_PATH dir)将其放入工程的某个目录中供工程使用,我们在后面的章节会详细介绍 FIND_PACKAGE 的使用方法和 Find 模块的
编写。

FIND_LIBRARY 示例:
FIND_LIBRARY(libX X11 /usr/lib)
IF(NOT libX)
MESSAGE(FATAL_ERROR “libX not found”)
ENDIF(NOT libX)

2.8.3 控制指令

  1. IF 指令,基本语法为:
IF(expression)
#  THEN section. COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
ELSE(expression)
#  ELSE section. COMMAND1(ARGS ...) COMMAND2(ARGS ...)
...
ENDIF(expression)

另外一个指令是ELSEIF,总体把握一个原则,凡是出现IF 的地方一定要有对应的ENDIF.出现ELSEIF 的地方,ENDIF是可选的。
表达式的使用方法如下:

IF(var),如果变量不是:空,0,N, NO, OFF, FALSE, NOTFOUND 或 <var>_NOTFOUND 时,表达式为真。IF(NOT var ),与上述条件相反。IF(var1 AND var2),当两个变量都为真是为真。IF(var1 OR var2),当两个变量其中一个为真时为真。IF(COMMAND cmd),当给定的 cmd 确实是命令并可以调用是为真。IF(EXISTS dir)或者IF(EXISTS file),当目录名或者文件名存在时为真。IF(file1 IS_NEWER_THAN file2),当file1 比file2 新,或者file1/file2 其中有一个不存在时为真,文件名请使用完整路径。IF(IS_DIRECTORY dirname),当 dirname 是目录时,为真。IF(variable MATCHES regex)IF(string MATCHES regex)

当给定的变量或者字符串能够匹配正则表达式 regex 时为真。比如:

IF("hello" MATCHES "ell")MESSAGE("true")ENDIF("hello" MATCHES "ell")IF(variable LESS number)IF(string LESS number)IF(variable GREATER number)IF(string GREATER number)IF(variable EQUAL number)IF(string EQUAL number)

数字比较表达式

IF(variable STRLESS string)IF(string STRLESS string)IF(variable STRGREATER string)IF(string STRGREATER string)IF(variable STREQUAL string)IF(string STREQUAL string)

按照字母序的排列进行比较.

IF(DEFINED variable),如果变量被定义,为真。 

一个小例子,用来判断平台差异:

IF(WIN32)
MESSAGE(STATUS “This is windows.”)
#作一些Windows 相关的操作
ELSE(WIN32)
MESSAGE(STATUS “This is not windows”)
#作一些非Windows 相关的操作
ENDIF(WIN32)

上述代码用来控制在不同的平台进行不同的控制,但是,阅读起来却并不是那么舒服,ELSE(WIN32)之类的语句很容易引起歧义。

这就用到了我们在“常用变量”一节提到的CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS开关。

可以SET(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)

这时候就可以写成:

IF(WIN32)
ELSE()
ENDIF()

如果配合ELSEIF使用,可能的写法是这样:

IF(WIN32)
#do something related to WIN32
ELSEIF(UNIX)
#do something related to UNIX
ELSEIF(APPLE)
#do something related to APPLE
ENDIF(WIN32)
  1. WHILE
    WHILE 指令的语法是:
WHILE(condition)
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
ENDWHILE(condition)

其真假判断条件可以参考IF 指令。

  1. FOREACH
    FOREACH指令的使用方法有三种形式:
    1.列表
    FOREACH(loop_var arg1 arg2 ...)
    COMMAND1(ARGS ...)
    COMMAND2(ARGS ...)
    ...
    ENDFOREACH(loop_var)
    
    像我们前面使用的AUX_SOURCE_DIRECTORY的例子AUX_SOURCE_DIRECTORY(. SRC_LIST)
    FOREACH(F ${SRC_LIST})
    MESSAGE(${F})
    ENDFOREACH(F)
    
    2.范围
    FOREACH(loop_var RANGE total)
    ENDFOREACH(loop_var)
    
    从 0 到 total 以1为步进
    举例如下:
    FOREACH(VAR RANGE 10)
    MESSAGE(${VAR})
    ENDFOREACH(VAR)
    
    最终得到的输出是:
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    3.范围和步进
    FOREACH(loop_var RANGE start stop [step])
    ENDFOREACH(loop_var)
    从 start 开始到stop 结束,以step 为步进,

举例如下
FOREACH(A RANGE 5 15 3)
MESSAGE(${A})
ENDFOREACH(A)
最终得到的结果是:
5
8
11
14

这个指令需要注意的是,知道遇到ENDFOREACH 指令,整个语句块才会得到真正的执行。

2.7 单个文件目录实现

2.7.1 单个文件目录实现1

在这里插入图片描述

2.7.1 单个文件目录实现2

在这里插入图片描述
这个是需要执行子目录src中的CMakeLists.txt的。
我们打印一下:在这里插入图片描述

2.8 多个目录实现

2.8.1 子目录编译成库文件

在这里插入图片描述

语法:INCLUDE_DIRECTORIES 找头文件
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/dir1")语法:ADD_SUBDIRECTORY 添加子目录
ADD_SUBDIRECTORY("${CMAKE_CURRENT_SOURCE_DIR}/dir1")语法:ADD_LIBRARY 生成库文件
ADD_LIBRARY( hello_shared SHARED libHelloSLAM.cpp ) 生成动态库
ADD_LIBRARY( hello_static STATIC libHelloSLAM.cpp ) 生成静态库语法:TARGET_LINK_LIBRARIES链接库到执行文件上,生成最后目标
TARGET_LINK_LIBRARIES(darren dir1 dir2)

2.8.2 子目录使用源码编译

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/11642.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python数据分析——seaborn绘图1

参考资料&#xff1a;活用pandas库 matplotlib库是python的和兴绘图工具&#xff0c;而seaborn基于matplotlib创建&#xff0c;它为绘制统计图提供了更高级的接口&#xff0c;使得只用少量代码就能生成更美观、更复杂的可视化效果。 seaborn库和pandas以及其他pydata库&#xf…

OpenHarmony 实战开发——移植通信子系统

通信子系统目前涉及Wi-Fi和蓝牙适配&#xff0c;厂商应当根据芯片自身情况进行适配。 移植指导 Wi-Fi编译文件内容如下&#xff1a; 路径&#xff1a;“foundation/communication/wifi_lite/BUILD.gn” group("wifi") {deps [ "$ohos_board_adapter_dir/ha…

C++基础与深度解析 | 数组 | vector | string

文章目录 一、数组1.一维数组2.多维数组 二、vector三、string 一、数组 1.一维数组 在C中&#xff0c;数组用于存储具有相同类型和特定大小的元素集合。数组在内存中是连续存储的&#xff0c;并且支持通过索引快速访问元素。 数组的声明&#xff1a; 数组的声明指定了元素的…

【数据结构】数组循环队列的实现

队列&#xff08;Queue&#xff09;是一种特殊的线性数据结构&#xff0c;它遵循FIFO&#xff08;First In First Out&#xff0c;先入先出&#xff09;的原则。队列只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#…

python下载及安装

1、python下载地址&#xff1a; Python Releases for Windows | Python.orgThe official home of the Python Programming Languagehttps://www.python.org/downloads/windows/ 2、python安装 &#xff08;1&#xff09; 直接点击下载后的可执行文件.exe &#xff08;2&…

Spring Boot项目怎么集成Gitee登录

一、背景 现在的越来越多的项目&#xff0c;需要集成第三方系统进行登录。今天我们以Spring Boot项目集成Gitee为例&#xff0c;演示一下怎么使用Oauth2协议&#xff0c;集成第三方系统登录。 不了解oauth2的&#xff0c;可以看我之前的文章。Ouath2是怎么实现在第三方应用认…

计算机毕业设计Python+Spark知识图谱课程推荐系统 课程预测系统 课程大数据 课程数据分析 课程大屏 mooc慕课推荐系统 大数据毕业设计

1 绪 论 1.1 课题研究背景 在线教育学习平台是学生用来进行校内或校外拓展课程学习的平台&#xff0c;平台需要具备在线视频观看&#xff0c;作业提交&#xff0c;形成性考核等功能。在学生学习的过程中&#xff0c;学校的管理者或负责教师需要了解学生的学习情况和学习状态&…

WWW服务器搭建(2)——Apache服务器配置与管理

一、Apache简介 1.1 关于Apache Apache HTTP Server&#xff08;简称Apache&#xff09;是Apache软件基金会的一个开放源码的Web服务器&#xff0c;可以在大多数计算机操作系统中运行&#xff0c;由于其跨平台和安全性被广泛使用&#xff0c;是最流行的Web服务器端软件之一。…

01-02-5

1、单链表中按位置查找 a.原理 通过传递的位置&#xff0c;返回该位置对应的地址&#xff0c;放到主函数定义的指针变量中。 我们认为位置从&#xff1a;有数据的节点开始计数 即如下结构&#xff1a; 查找位置&#xff0c;就是返回该位置对应的空间地址。 b.代码说明 Ⅰ…

H5嵌入原生----兼容安卓与ios

主要分UI展示&#xff0c;键盘&#xff0c;输入框等等。解决bug最苦恼的问题不是没有解决方案&#xff0c;而是你没有找到真正的原因。再就是现象难以重现&#xff0c;每次都要发布代码&#xff0c;然后到手机app中去测试&#xff0c;模拟。这些地方会耗费大量的精力。 一、UI…

【软设】常见易错题汇总

目录 计算机系统基础 程序语言基础 数据结构 算法设计与分析 计算机网络与信息安全 软件工程基础 开发方法&#xff08;结构化与面向对象&#xff09; 数据库 操作系统 知识产权相关的法律法规 &#x1f92f;&#x1f92f;&#x1f92f;&#x1f92f;&#x1f92f;&#x1f9…

《系统架构设计师教程(第2版)》第10章-软件架构的演化和维护-07-软件架构维护

文章目录 1. 软件架构知识管理1.1 概念1.2 架构知识的获取1.3 作用1.4 架构知识管理的现状 2 软件架构修改管理3 软件架构版本管理4. 示例4.1 背景4.2 数据获取4.3 数据计算4.4 结果分析4.4.1 圈复杂度 (CCN)4.4.2 扇入扇出度 (FFC)4.4.3 模块间耦合度 (CBO)4.4.4 模块的响应 (…

mysql group by 细节介绍

mysql中group by的用法是配合聚合函数&#xff0c;利用分组信息进行统计&#xff0c;语句如“select name,sum(id) from test group by name,number”。 先来看下表1&#xff0c;表名为test&#xff1a; 执行如下SQL语句&#xff1a; SELECT name FROM test GROUP BY name 你…

OFDM802.11a的FPGA实现(十四)data域的设计优化,挤掉axi协议传输中的气泡

原文链接&#xff08;相关文章合集&#xff09;&#xff1a;OFDM 802.11a的xilinx FPGA实现 目录 1.前言 2.data域的时序要求 3.Debug 1.前言 前面12篇文章详细讲述了&#xff0c;OFDM 802.11a发射部分data域的FPGA实现和验证&#xff0c;今天对data域的设计做一个总结。在…

springboot306基于Java的民宿管理系统(源码+包运行+配套LW+技术指导)

项目描述 临近学期结束&#xff0c;开始毕业设计制作&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉的困难吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。今天给大家介绍一篇基于Java的民宿管理…

CoSeg: Cognitively Inspired Unsupervised Generic Event Segmentation

名词解释 1.特征重建 特征重建是一种机器学习中常用的技术&#xff0c;通常用于自监督学习或无监督学习任务。在特征重建中&#xff0c;模型被要求将输入数据经过编码器&#xff08;encoder&#xff09;转换成某种表示&#xff0c;然后再经过解码器&#xff08;decoder&#x…

升级Microsoft 365后,SAP GUI中无法打开Excel的解决方案

最近&#xff0c;我们遇到了一个棘手的问题&#xff0c;一位客户在升级到Microsoft 365后&#xff0c;无法在SAP GUI中打开Excel。这个问题不仅影响了工作效率&#xff0c;也给用户的日常操作带来了不便。在本文中&#xff0c;我们将探讨问题的成因&#xff0c;并提供一种解决方…

泛微E9开发 添加多个多选框,实现单选框的效果

利用多个多选框实现单选框的效果 1、功能背景2、展示效果3、实现效果 1、功能背景 如下图所示&#xff0c;在表单中新增四个“选择框-复选框”类型的字段&#xff0c;并且设置其中的选项&#xff0c;每个多选框都只有一个选项&#xff0c;通过代码块实现单选框的效果 1.显示模…

邓闲小——生存、生活、生命|真北写作

人生有三个层次∶生存、生活、生命。 生存就是做必须做的事。生存的模式是邓&#xff0c;是交易&#xff0c;是买卖。别人需要的东西&#xff0c;你生产出来&#xff0c;卖给他。哪怕这个东西没啥用&#xff0c;也可以卖&#xff0c;情绪也可以卖。你需要的东西&#xff0c;你花…

分布式与一致性协议之POW算法

POW算法 概述 谈起比特币&#xff0c;你应该并不陌生。比特币是基于区块链实现的&#xff0c;而区块链运行在因特网上&#xff0c;这就存在有人试图作恶的情况。有些读者可能已经发现了&#xff0c;口信消息型拜占庭问题之解、PBFT算法虽然能防止坏人作恶&#xff0c;但只能防…