CMake用于自动生成Makefile文件

img

Makefile通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。

CMake的优点:跨平台、能够管理大型项目、简化编译构建过程和编译过程、可扩展(可以为 cmake 编写特定功能的模块,扩充 cmake 功能)

1、Cmake的使用

前言:Cmake不区分大小写,在编写CMakelists.txt随缘

1.1注释

1
2
3
4
5
6
#这是要注释的内容
cmake_minimum_required(VERSION 3.0.0)
#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
cmake_minimum_required(VERSION 3.0.0)

1.2源文件

假设在某处存在如下几个源文件

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
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
return a+b;
}
#include <stdio.h>
#include "head.h"

// 你好
int subtract(int a, int b)
{
return a-b;
}
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
return a*b;
}
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
return (double)a/b;
}
#include <stdio.h>
#include "head.h"

int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif

目录结构如下:

img
1.2.1编写CMakeLists.txt

在同级目录创建CMakeLists.txt文件

内容如下:

1
2
3
cmake_minimum_required(VERSION 3.0)	#指定使用的 cmake 的最低版本,可选非必须,不加可能会有警告
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)#定义工程会生成一个可执行程序

project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。

1
2
3
4
5
6
7
# PROJECT 指令的语法是:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])

add_executable:生成可执行程序

1
2
3
4
5
6
7
add_executable(可执行程序名 源文件名称)

可以是一个或者多个源文件,如果有多个之间用空格或“;”隔开
# 样式1
add_executable(app add.c div.c main.c mult.c sub.c)
# 样式2
add_executable(app add.c;div.c;main.c;mult.c;sub.c)

注意:可执行程序名和project中的项目名没有任何关系

1.2.2执行cmake
1
cmake CMakeLists.txt文件所在路径
img

执行完成后,可以看到当前目录多了许多文件,其中就包括Makefile文件

img

执行make即可得到目标执行程序文件

img
1.2.3独立CMake相关文件

如果在CMakeLists.txt文件所在目录执行了cmake命令之后就会生成一些目录和文件(包括 makefile 文件),如果再基于makefile文件执行make命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为build:

1
2
3
makedir build
cd build
cmake ..

当命令执行完毕之后,在build目录中会生成一个makefile文件

1.3定义变量

在上面的例子中一共提供了5个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在cmake里定义变量需要使用set。

1
2
3
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
  • VAR:变量名
  • VALUE:变量值
1
2
3
4
5
6
# 方式1: 各个源文件之间使用空格间隔
set(SRC_LIST add.c div.c main.c mult.c sub.c)

# 方式2: 各个源文件之间使用分号 ; 间隔
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app ${SRC_LIST})

1.4指定C++版本

1
$ g++ *.cpp -std=c++11 -o app

上面的例子中通过参数-std=c++11指定出要使用c++11标准编译程序

C++标准对应有一宏叫做DCMAKE_CXX_STANDARD

在CMake中想要指定C++标准有两种方式:

  1. 在 CMakeLists.txt 中通过 set 命令指定
1
2
3
4
5
6
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)
  1. 在执行 cmake 命令的时候指定出这个宏的值
1
2
3
4
5
6
#增加-std=c++11
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
#增加-std=c++14
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=14
#增加-std=c++17
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=17

1.5指定输出路径

在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值通过set命令进行设置:

1
2
set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)

如果这个路径中的子目录不存在,会自动生成,无需自己手动创建

1.7搜索文件

在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件罗列出来,这样太麻烦也不现实。

所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory命令或者file命令。

1.7.1 aux_source_directory方式

命令格式:aux_source_directory(< dir > < variable >)

  • dir:要搜索的目录
  • variable:将从dir目录下搜索到的源文件列表存储到该变量中
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索 src 目录下的源文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(app ${SRC_LIST})
1.7.2 file方式

命令格式:file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)

  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
  • GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
1
2
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)

注释:CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径。

1.8头文件

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake通过include_directories即可指定

1
2
3
4
基本格式:include_directories(headpath)
例:
include_directories(${PROJECT_SOURCE_DIR}/include)
#PROJECT_SOURCE_DIR宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录

1.9制作静态库和动态库

命令格式:add_library(库名称 STATIC 源文件1 [源文件2] ...)

静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库名字即可

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
#制作静态库,生成libcalc.a
add_library(calc STATIC ${SRC_LIST})
#制作动态库,生成libcalc.so
add_library(calc SHARED ${SRC_LIST})

这样最终就会生成对应的静态库文件libcalc.alibcalc.so

1.9.1指定静态库和动态库的输出路径:

使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用。

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 生成动态库
#add_library(calc SHARED ${SRC_LIST})
# 生成静态库
add_library(calc STATIC ${SRC_LIST})

1.10链接静态库和动态库

1.10.1链接静态库

命令格式:link_libraries(<static lib> [<static lib>...])

  • 可以是全名 libxxx.a
  • 也可以是掐头(lib)去尾(.a)之后的名字 xxx

如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:link_directories(<lib path>)

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库
link_libraries(calc)
add_executable(app ${SRC_LIST})
1.10.2链接动态库
1
2
3
4
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • target:动态库名;
  • PRIVATE|PUBLIC|INTERFACE:动态库可视权限,默认为PUBLIC

动态库的链接具有传递性,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了B、C

1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 指定源文件或者动态库对应的头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 指定要链接的动态库的路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 添加并生成一个可执行程序
add_executable(app ${SRC_LIST})
# 指定要链接的动态库
target_link_libraries(app pthread calc)

2、日志message

message函数可以为用户打印一条消息

1
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
  • (无) :重要消息
  • STATUS :非重要消息
  • WARNING:CMake 警告, 会继续执行
  • AUTHOR_WARNING:CMake 警告 (dev), 会继续执行
  • SEND_ERROR:CMake 错误, 继续执行,但是会跳过生成的步骤
  • FATAL_ERROR:CMake 错误, 终止所有处理过程

3、变量操作

3.1拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
set(SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
list(APPEND SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")

3.2字符串移除

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/*.cpp)
# 移除前日志
message(STATUS "message: ${SRC_1}")
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/main.cpp)
# 移除后日志
message(STATUS "message: ${SRC_1}")

4、宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#define NUMBER 3
int main()
{
int a = 10;
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif
for(int i=0; i<NUMBER; ++i)
{
printf("hello, GCC!!!\n");
}
return 0;
}

为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++命令中去指定,如下:

1
$ gcc test.c -DDEBUG -o app

在gcc/g++命令中通过参数 -D指定出要定义的宏的名字,其名字为DEBUG

在CMake中我们也可以做类似的事情,对应的命令叫做add_definitions

1
2
3
4
5
cmake_minimum_required(VERSION 3.0)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)

5、预定义宏

image-20240306212818441

6、嵌套的CMake

如果项目很大,或者项目中有很多的源码目录,就得给每个源码目录都添加一个CMakeLists.txt文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。

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
$ tree
.
├── build
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── CMakeLists.txt
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert.cpp
│ └── select.cpp
├── test1
│ ├── calc.cpp
│ └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp

6 directories, 15 files

可以通过add_subdirectory建立父子节点关系

1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

注:子节点可以使用读取父节点中的变量

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
cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)
cmake_minimum_required(VERSION 3.0)
project(CALCLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${CALC_LIB} STATIC ${SRC})
cmake_minimum_required(VERSION 3.0)
project(SORTLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${SORT_LIB} SHARED ${SRC})
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
link_directories(${LIB_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})
cmake_minimum_required(VERSION 3.0)
project(SORTTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
link_directories(${LIB_PATH})
add_executable(${APP_NAME_2} ${SRC})
target_link_libraries(${APP_NAME_2} ${SORT_LIB})

7、流程控制

7.1条件判断

7.1.1基本表达式
1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>) # 可选快, 可以重复
<commands>
else() # 可选快
<commands>
endif()

ifendif必须成对出现

7.1.2逻辑判断
1
2
3
if(<cond1> AND <cond2>)且
if(<cond1> OR <cond2>)或
if(NOT <conditon>)非
7.1.3比较
1
2
3
4
5
小于:if(<variable|string> LESS <variable|string>)
大于:if(<variable|string> GREATER <variable|string>)
等于:if(<variable|string> EQUAL <variable|string>)
小于等于:if(<variable|string> LESS_EQUAL <variable|string>)
大于等于:if(<variable|string> GREATER_EQUAL <variable|string>)
7.1.4文件操作
1
2
3
4
5
6
7
8
#判断文件或目录是否存在
if(EXISTS path-to-file-or-directory)
#判断是否是目录
if(IS_DIRECTORY path)
#判断是否是软链接
if(IS_SYMLINK file-name)
#判断是否为绝对路径
if(IS_ABSOLUTE path)

7.2循环