Cmake总结

总结Cmake的使用

背景

之前我学习了,Linux中autotools工具链的使用,但是在学习的过程中我发现还有比autotools更加方便,更加好用的制作Makefile文件的工具——Cmake

Autotools和Cmake的对比

首先我们已经知道了Autotools工具链的使用非常繁琐:

而Cmake的使用只需要编写Cmake.txt文件,将自己的需求告诉Cmake就可以了。其实Cmake就是一种语言,我们通过Cmake的语法编写Cmake.txt文件内容。

安装Cmake

注意:由于Cmake是一个版本控制工具,所以我们使用的Cmake最好比我们使用的编译器的版本的发行时间要晚,所以我们尽量使用新版本的Cmake。

常用的函数以及操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
cmake_minimum_required(VERSION 3.10)
# 指定运行此配置文件所需的 CMake 的最低版本。

project(Hello VERSION 1.0)
# 设置项目的属性,包括(名称、版本等待)。同时会自动生成PROJECT_NAME 变量,使用 ${PROJECT_NAME} 即可访问到 hello_cmake。

add_executable(Hello hello.c)
# 第一个参数是可执行文件名,第二个参数是要编译的源文件列表。这里将hello.c文件编译成hello可执行文件,在有源函数的文件夹中的CMakeTlists中使用。

install(TARGETS Hello)
# 指定安装的目标

aux_source_directory(. DIR_SRCS)
# 查询当前目录下的所有源文件,将其存储在DIR_SRCE变量下

ADD_LIBRARY(libname [SHARED|STATIC|MODULE]
[EXCLUDE_FROM_ALL]
source1 source2 ... sourceN)
# add_library:用于从某些源文件创建一个库,默认生成在构建文件夹。第一个参数为库名(不需要 lib 前缀,会自动添加),第二个参数用于指定 SHARED(动态库),STATIC(静态库)(如果不写,则通过全局BUILD_SHARED_LIBS 的 FALSE 或 TRUE 来指定)。第三个参数即为源文件列表。

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

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

target_link_libraries(Demo MathFunctions)
# target_link_libraries:该命令用于指明可执行文件 Demo 需要链接 MathFunctions 库。第一个参数为可执行文件名,第二个参数为访问权限(PUBLIC、PRIVATE、INTERFACE,默认为 PUBLIC),第三个参数为库名(这两个参数可以为多个)。

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

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


ADD_SUBDIRECTORY 指令
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
# 这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL 参数的含义是将这个目录从编译过程中排除,比如,工程的 example,可能就需要工程构建完成后,再进入 example 目录单独进行构建(当然,你也可以通过定义依赖来解决此类问题)。在主CMakeLists中使用。


通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH 变量
来指定最终的目标二进制的位置(指最终生成的 hello 或者最终的共享库,不包含编译生成
的中间文件)

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

<projectname>_BINARY_DIR 和 PROJECT_BINARY_DIR 变量,他们指的编译发生的当前目录,如果是内部编译,就相当于 PROJECT_SOURCE_DIR 也就是
工程代码所在目录,如果是外部编译,指的是外部编译所在目录,也就是本例中的build目录。

# INSTALL函数,其中大写字母都是固定的,我们只需要更改小写字母的。

# 安装目标文件
INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)
上面的例子会将:
可执行二进制 myrun 安装到${CMAKE_INSTALL_PREFIX}/bin 目录
动态库 libmylib 安装到${CMAKE_INSTALL_PREFIX}/lib 目录
静态库 libmystaticlib 安装到${CMAKE_INSTALL_PREFIX}/libstatic 目录

# 安装普通文件
INSTALL(FILES files... DESTINATION <dir>
[PERMISSIONS permissions...] # 权限
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])
# 可用于安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。如果默认不定义权限 PERMISSIONS,安装后的权限为:OWNER_WRITE, OWNER_READ, GROUP_READ,和 WORLD_READ,即 644 权限。


# 安装非目标文件的可执行文件(脚本等等)
INSTALL(PROGRAMS files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])
# 跟上面的 FILES 指令使用方法一样,唯一的不同是安装后权限为:
# OWNER_EXECUTE, GROUP_EXECUTE, 和 WORLD_EXECUTE,即 755 权限

# 安装函数的注意点:
# 1. 如果没有cmake -DCMAKE_INSTALL_PREFIX=/tmp/t2/usr .. 也就是说没有指定CMAKE_INSTALL_PREFIX,则默认路径是/usr/local
# 2. 在安装目录(属于上述的普通文件)时 如果是 x/ 则是安装x目录下的文件,不包括x;如果是 x 则是包括了x。

#生成一个动态库或者是静态库,也可以使用
ADD_LIBRARY
ADD_LIBRARY(libname [SHARED|STATIC|MODULE]
[EXCLUDE_FROM_ALL]
source1 source2 ... sourceN)

在Cmake中使用变量的值的一般方法是%{},但是在if语句中使用变量只需直接使用变量名即可,无需使用%{}

语法规则小总结

最简单的语法规则是:

  1. 变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名
  2. 指令(参数 1 参数 2…)

参数使用括弧括起,参数之间使用空格或分号分开。

以上面的 ADD_EXECUTABLE 指令为例,如果存在另外一个 func.c 源文件,就要写成:

ADD_EXECUTABLE(hello main.c func.c)或者

ADD_EXECUTABLE(hello main.c;func.c)

  1. 指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。

需要注意的是Cmake的语法是比较灵活的,比如说:在包含文件名的时候可以使用””将文件名包裹,但是不使用也是可以的,但如果文件名中有空格的话就必须使用””。同时在函数中分隔参数可以使用空格分隔,也可以使用,分隔。注意这里不是所有的参数都是用,分隔。而是同类型的参数之间,比如说文件名之间。

构建过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 将规则写在CMakeList.txt文件中,需要注意大小写。

mkdir build
# 创建一个build文件夹,将Cmake生成的文件放在此文件下
cd ./build
# 切换到build文件夹下
cmake ..
# 生成Makefile等文件
make
# 执行make程序,你可以用 cmake --build . 替换 make 这一行。

# 上面的步骤也可以使用一下两条命令替代
cmake -S . -B build
cmake --build build

# 一下的任意一条指令都可以完成安装
# From the build directory (pick one)
make install
cmake --build . --target install
cmake --install . # CMake 3.15+ only
# From the source directory (pick one)
make -C build install
cmake --build build --target install
cmake --install build # CMake 3.15+ only

构建动态库和静态库

动态库

我们知道可以直接使用:

1
2
3
4
gcc -fPLC -share -o libtest.so 1.c 2.c 3.c或者是:

gcc -fPIC -c func.c -o func.o
gcc -shared func.o -o libfunc.so

Cmake方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#首先创建一个新的文件夹t3,t3中mkdir lib 。在t3目录中touch CMakeLists.txt中添加:
ROJECT(HELLOLIB) #创建一个新的项目
ADD_SUBDIRECTORY(lib) #引入lib子目录

#在lib中vim CMakeLists.txt添加:
SET(LIBHELLO hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO})

#使用外部编译,mkdir Build cd BUild

此时可以在lib中找到HELLOLIB.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

#设置共享库的版本号:
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

制作静态库

我们知道可以直接使用gcc命令制作静态库

1
2
3
4
5
6
7
8
9
10
11
12
		gcc -c 1.c 2.c 
ar rsc test.a ./1.o ./2.o

ar 是 Linux 的一个备份压缩命令,它可以将多个文件打包成一个备份文件(也叫归档文件),也可以从备份文件中提取成员文件,最常见的用法是将目标文件打包为静态链接库。

对参数的说明:

参数 r 用来替换库中已有的目标文件,或者加入新的目标文件。
参数 c 表示创建一个库。不管库否存在,都将创建。
参数 s 用来创建目标文件索引,这在创建较大的库时能提高速度。

不管是静态库或者是动态库都是由.o文件构成的。

但是当有很多源文件的时候,我们在使用这种方法就很不方便了。于是cmake支持创建静态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#使用到的指令:
使用指令和制作动态库是一致的,只是需要将动态参数换成静态参数。唯一需要注意的是两种库的同名问题,如果我们使用函数ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})来构建动态库,会造成一个同名的报错(因为函数的target是唯一的)。解决方法有将某一个库的名字改成另一个。如果要求两种库同名的话可以使用函数:

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.

使用动态库&静态库

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#表示注释   
#cmake file for project association

#cmake 最低版本要求,低于2.8 构建过程会被终止。
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

#定义工程名称
PROJECT(association)

#打印相关消息消息
#MESSAGE(STATUS "Project: ${PROJECT_NAME}")
#MESSAGE(STATUS "Project Directory: ${PROJECT_SOURCE_DIR}")

#指定编译类型debug版
SET(CMAKE_BUILE_TYPE DEBUG)
#发行版
#SET(CMAKE_BUILE_TYPE RELEASE)

#SET(CMAKE_C_FLAGS_DEBUG "-g -Wall") #C
#SET(CMAKE_CXX_FLAGS_DEBUG "-g -Wall") #C++

#设置C++ 编译
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -s -Wall -W -O3")

#添加子目录
ADD_SUBDIRECTORY(src/include)

#设置变量,表示所有的源文件
SET(SOURCE_FILES
src/main.cpp
)


#配置相关库文件的目录,
LINK_DIRECTORIES(
/usr/local/lib
)

#找BZip2
FIND_PACKAGE(BZip2)
if (BZIP2_FOUND)
MESSAGE(STATUS "${BZIP_INCLUDE_DIRS}")
MESSAGE(STATUS " ${BZIP2_LIBRARIES}")
endif (BZIP2_FOUND)
if (NOT BZIP2_FOUND)
MESSAGE(STATUS "NOT BZIP2_FOUND")
endif (NOT BZIP2_FOUND)


#相关头文件的目录
INCLUDE_DIRECTORIES(
/usr/local/include
${PROJECT_SOURCE_DIR}/utility_inc
${BZIP_INCLUDE_DIRS}
)

#链接库
LINK_LIBRARIES(
${PROJECT_SOURCE_DIR}/static_libs/libSentinelKeys64.a
${BZIP2_LIBRARIES}
)

#生成可执行文件
ADD_EXECUTABLE(${PROJECT_NAME} ${SOURCE_FILES})

#依赖的库文件
TARGET_LINK_LIBRARIES(${PROJECT_NAME} eventloop)

基础知识

本地变量

注意,Cmake本身就制定了一些变量,这里变量都是大写的。

https://modern-cmake-cn.github.io/Modern-CMake-zh_CN/chapters/basics/variables.html#fn_1

小总结:

  1. CMake具有作用域的概念

内部编译和外部编译

这两者的区别就是,内部编译是在源文件目录中直接make,而外部编译是在其他目录中(一般是Build)目录下 Cmake的。这两者的主要区别就是在内部编译中,编译所生成的中间文件都会留在源文件中,而外部编译不会。

安装

CMake中的安装需要使用一个函数:INSTALL。安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。这个函数一般和CMAKE_INSTALL_PREFIX变量一起使用。

GNU Make用于控制如何从程序的源代码文件编译并链接为可执行文件,通过make命令从名称为makefile的文件中获取构建信息,该文件定义了一系列规则来指定源文件的编译先后顺序、是否需要重新编译、甚至于进行更为复杂的操作。通过makefile文件可以方便的实现工程的自动化编译,只需要执行make命令即可完成编译动作,从而极大的提高了开发人员的工作效率。

CMake 3.17是一款源代码构建管理工具,最初作为各种 Makefile 方言的生成器,后来逐步发展为现代化的构建系统,广泛用于 C 和 C++ 工程源代码的构建。官方提供的《CMake Tutorial》 为开发人员提供了一个循序渐进的指南,涵盖了 CMake 构建过程中常见问题的解决方案。如果需要构建从第三方发布的源代码包,则可以参考《User Interaction Guide》。而《Using Dependencies Guide》则主要针对需要使用第三方库的开发人员。

GNU Make

make是一款用于解释makefile文件当中命令的工具,而makefile关系到整个工程的编译规则。许多 IDE 集成开发环境都整合了该命令,例如:Visual C++ 里的nmake,Linux 里的 GNU make,本章节主要讲解 GNU make 相关的内容。开始进一步讲解之前,需要先了解一下 C/C++ 源代码的编译过程,具体内容可参见笔者的《基于 Linux 的 GCC 与 GDB 应用调试》 - 编译步骤一文:

  1. 预处理 Preprocessing:解析各种预处理命令,包括头文件包含、宏定义的扩展、条件编译的选择等;
  2. 编译 Compiling:对预处理之后的源文件进行翻译转换,产生由机器语言描述的汇编文件;
  3. 汇编 Assembly:将汇编代码转译成为机器码;
  4. 链接 Link:将机器码中的各种符号引用与定义转换为可执行文件内的相应信息(例如虚拟地址);

makefile 文件

基本规则

执行make命令时,实际会解析当前目录下的makefile文件,该文件用于告知make命令如何对源代码进行编译与链接,一个 makefile 的基本编写规则如下所示:

1
2
3
4
target ... : prerequisites ...
command
...
...
  • target:即可以是 1 个目标文件,也可以是 1 个执行文件,甚至还可以是 1 个标签;
  • prerequisites:生成该target 所依赖的文件或者其它target
  • command:该target所要执行的 Shell 命令,需要保持 1 个【Tab】的缩进;

上述的基本编写规则最终会形成一套依赖关系,其中**target依赖于prerequisites,而生成规则定义在command;如果prerequisites中的文件比target上的文件要新,则command所定义的命令就会被执行**。

观察下面的例子,其中的反斜杠\表示换行,将其保存为一个makefile或者Makefile文件,然后在当前目录执行make命令,就可以生成可执行文件app。如果需要删除可执行文件以及中间生成的目标文件,则执行make clean命令即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
gcc -o app main.o kbd.o command.o display.o insert.o search.o files.o utils.o

main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c
clean :
rm app main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

输入make命令之后,就会开始执行上述的makefile文件,具体执行流程如下所示:

  1. make会在当前目录下查找Makefile或者makefile文件;
  2. 找到后将当中定义的第 1 个target作为最终的目标文件;
  3. 如果app文件不存在,或者其依赖的.o文件修改时间要比app执行文件更新。那么,他就会执行command定义的命令来生成app文件;
  4. 如果app依赖的.o文件也不存在,那么查找.o文件对应的依赖规则生成.o文件;
  5. 最后,基于工程中.c.h源文件生成.o依赖文件,然后再基于这些.o文件生成app执行文件;

定义变量

上面示例中app生成规则中的一系列.o文件反复出现,这里我们可以将其声明为一个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

app : $(objects)
cc -o app $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm app $(objects)

自动推导

GNU Make 可以自动识别并推导目标与依赖关系之后的command命令,只要make发现 1 个.o文件,就会自动将对应的.c文件添加至依赖关系当中,同时也会将对应的gcc -c命令推导出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

app : $(objects)
cc -o app $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
rm app $(objects)

这种方法被称为make隐含规则,上述代码中.PHONY表示clean是一个伪目标文件,关于隐晦规则和伪目标文件的内容后续将会进行更为详细的介绍。

通过隐含规则可以进一步简化上面的makefile,这样虽然可以最大幅度减少代码,但是文件的依赖关系显得较为凌乱,所以这种风格较少被采用。

1
2
3
4
5
6
7
8
9
10
11
12
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

app : $(objects)
cc -o app $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
rm app $(objects)

清理中间文件

习惯上,每个makefile文件都应该编写一个用于清理中间文件的规则,这样不仅便于重新编译,也有利于保持工程的整洁。

1
2
clean:
rm edit $(objects)

之前代码采用了上面较为简单粗暴的方式,但是更为稳健的方法是采用下面这样的风格:

1
2
3
.PHONY : clean
clean :
-rm edit $(objects)

.PHONY关键字用于表标识clean是一个伪目标rm命令前的小减号-表示忽略操作出现问题的文件,习惯上会将clean放置在makefile的最后。

Makefile 组成

Makefile 文件主要包含显式规则隐式规则变量定义文件指示注释

  • 显式规则:由Makefile编写者明确指定,用于描述如何生成target
  • 隐式规则:利用make命令的自动推导功能,简略书写Makefile
  • 变量的定义:通常为字符串,当Makefile被执行时,其中的变量会扩散到相应的引用位置;
  • 文件指示:在Makefile当中引用另外的Makefile,类似于 C 语言里的#include。或者根据条件指定Makefile的有效部分,类似于 C 语言中的#if。除此之外,还可以用于定义一条拥有多行的命令;
  • 注释:注释采用#字符,需要时可以采用反斜杠进行转义\#

注意Makefile中的命令command必须以【Tab】键开始。

引用其它 Makefile

使用include关键字可以将其它Makefile包含进来,类似于 C 语言中的#include预处理语句,被包含的文件会自动替换至包含位置。

1
include <filename>

filename可以是当前操作系统 Shell 命令或者文件(可以包含路径和通配符),include关键字之前可以存在空字符,但是绝不允许出现【Tab】键。

例如:存在 4 个 Makefilea.mkb.mkc.mkfoo.make以及 1 个变量$(bar)(包含e.mkf.mk) ,那么下面 2 条语句就是等价的:

1
2
3
include foo.make *.mk $(bar)

include foo.make a.mk b.mk c.mk e.mk f.mk

make命令开始执行时,会查找include的其它Makefile,如果没有指定绝对或者相对路径的话,make会首先在当前目录下查找,如果没有查询到则会进入如下目录:

  • 如果make命令执行时,带有-I或者--include-dir参数,那么make就会在该参数指定的目录下查找;
  • 此外,make还会去查找<prefix>/include目录(通常为/usr/local/bin或者/usr/include);

最后,如果文件未能找到,make将会生成警告信息,然后继续载入其它文件,一旦makefile读取完成,make会再次进行查询,如果依然未能找到,则报出一条致命错误信息。如果想让make忽略读取错误,则可以在include前添加减号-

1
-include <filename>

注意:其它版本make采用的兼容命令是sinclude,其作用与-include相同。

这里,重新再来总结一下 GNU Make 的工作步骤:

  1. 读取所有Makefile文件;
  2. 查找被include的其它Makefile
  3. 初始化Makefile文件当中定义的变量;
  4. 分析并且推导隐式规则;
  5. 创建target目标文件的依赖关系;
  6. 根据依赖关系,决定哪些target需要重新生成;

MAKEFILES 环境变量

如果当前定义了MAKEFILES环境变量,其值为采用空格分隔的其它Makefile,执行make时会将这个该环境变量的值include进来。但是与include所不同的是,该环境变量引入的Makefiletarget不会生效,其定义的文件如果发现错误,make也会不理会。

日常开发环境,不建议使用MAKEFILES环境变量,因为定义后会影响到所有make命令的执行。反而是在makefile文件出现一些莫名其妙错误的时候,需要检查当前是否定义了这个环境变量。

规则

规则描述了Makefile文件的依赖关系以及如何生成目标文件。定义在 Makefile 中的target可以有很多,但是第 1 条规则中的target会被确立为最终的目标。

1
2
3
4
5
6
7
8
9

targets : prerequisites
command
...


targets : prerequisites ; command
command
...
  • targets目标文件名称,以空格分隔,可以使用通配符;
  • prerequisites目标文件的依赖,如果某个依赖文件比目标文件要新,那么就会重新进行生成;
  • commandShell 命令行,如果不与target : prerequisites在一行,那么必须以【Tab】开头;如果保持在一行,则可以采用分号;进行分隔;

注意:如果prerequisitescommand过长,可以使用反斜杠\进行换行。通常make会以 Bash Shell 也就是/bin/sh来执行命令。

通配符

make支持*?~三个通配符。~字符在 Linux 下表示当前用户的$HOME目录,在 Windows 下则根据环境变量HOME设置而定。

通配符可以应用在command当中,下面代码会在清除所有.o文件之前,查看一下main.c文件。

1
2
3
clean:
cat main.c
rm -f *.o

通配符还可以应用于prerequisites,下面代码中的print目标依赖于所有.c文件,其中的$?是后续将会讲到的自动化变量

1
2
3
print: *.c
lpr -p $?
touch print

通配符同样可以应用在变量中,但是并不会因此而自动展开,下面代码里变量objects的值就是*.o

1
objects = *.o

如果需要让通配符在变量当中展开,即让objects的值是所有.o文件名的集合。

1
objects := $(wildcard *.o)

Autoconf

autoconf

Automake

automake

CMake

CMake 教程提供了一个循序渐进的指南,涵盖了常见的构建系统问题。本文涉及的示例代码可以在 CMake 源码树Help/guide/tutorial目录下找到,每个步骤都拥有其相应的子目录,循序渐进直至提供完整的解决方案。

基本出发点

最为基础的项目是从源代码构建可执行文件,这样只需要一个 3 行的CMakeLists.txt文件,这将是整个教程的起点。在【Step1】目录当中创建如下CMakeLists.txt文件:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.10)


project(Tutorial)


add_executable(Tutorial tutorial.cxx)

CMake 支持大写小写混合大小写的命令,上面的CMakeLists.txt文件使用了小写命令。教程源代码Step1目录中提供了用于执行数字平方根计算的cxx文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>

int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

const double inputValue = atof(argv[1]);
const double outputValue = sqrt(inputValue);
std::cout << "The square root of " << inputValue << " is " << outputValue
<< std::endl;
return 0;
}

添加版本号和配置头文件

我们要添加的第一个特性是为项目提供 1 个版本号。虽然源代码中也可以完成这件事,但是使用CMakeLists.txt可以提供更好的灵活性。首先,修改CMakeLists.txt文件,使用project()命令设置项目名称和版本号。

1
2
3
4
cmake_minimum_required(VERSION 3.10)


project(Tutorial VERSION 1.0)

然后,继续编写配置,把一个头文件上保存的版本号传递到源代码:

1
configure_file(TutorialConfig.h.in TutorialConfig.h)

由于配置文件将会被写入到二叉树,所以必须将该目录添加至搜索包含文件的路径列表当中,在CMakeLists.txt文件的末尾添加以下行:

1
2
3
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)

在当前目录下创建TutorialConfig.h文件,并且包含如下内容:

1
2
3
/* 配置主、副版本号 */


当 CMake 配置该头文件以后,上述的@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@的值将会被替换。

接下来修改tutorial.cxx来包含上面的TutorialConfig.h头文件,并最终通过修改后的tutorial.cxx打印版本号。

1
2
3
4
5
6
7
8
9
#include "TutorialConfig.h"

if (argc < 2) {

std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

指定 C++ 标准

接下来,将tutorial.cxx文件中的atof替换为std::stod,从而为项目添加一些 C++11 特性。同时,删除#include <cstdlib>

1
const double inputValue = std::stod(argv[1]);

CMake 中启用特定 C++ 标准支持的最简单方法是使用CMAKE_CXX_STANDARD变量,这里将CMakeLists.txt文件里的CMAKE_CXX_STANDARD变量设置为11,并将CMAKE_CXX_STANDARD_REQUIRED设置为True

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.10)

# 设置项目名称与版本
project(Tutorial VERSION 1.0)

# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

编译与测试

从命令行导航到 CMake 源代码树的 Help/guide/tutorial 目录,并运行以下命令:

参考资料

中文翻译教程

中文教程

http://file.ncnynl.com/ros/CMake%20Practice.pdf

官方教程

优秀博客

自动生成Makefile

Cmake保姆级教程上

Cmake保姆级教程上

Autotool和Cmake对比

Github上的个人教程


Cmake总结
https://ysc2.github.io/ysc2.github.io/2023/12/06/Cmake总结/
作者
Ysc
发布于
2023年12月6日
许可协议