学习自https://subingwen.cn/cmake/CMake-primer/。
https://www.bilibili.com/video/BV14s4y1g7Zj/?p=3&spm_id_from=pageDriver&vd_source=a65f19543c0eb665983d7177bd50730e

!!!!!!编译过程中如果需要同时产出静态库和可执行程序或者动态库,一定要一步一步来,不要想着一次性调通,血泪

第一步

本次用c++简单测试

1
2
3
4
5
6
7
8
$ tree结构
.
├── add.cpp
├── div.cpp
├── head.h
├── main.cpp
├── mult.cpp
└── sub.cpp

1、 add.cpp

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
return a+b;
}

2、sub.cpp

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int subtract(int a, int b)
{
return a-b;
}

3、mult.cpp

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
return a*b;
}

4、div.cpp

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
return (double)a/b;
}

5、head.h

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

6、main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}
1
2
cmake_minimum_required(VERSION 3.0.0)  
#最低版本号的意思,可以不写
1
2
3
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>...])

PROJECT-NAME,项目的名字。
VERSION [.[.[.项目的版本。
DESCRIPTION 项目的描述。
HOMEPAGE_URL 地址。可以是自己网站地址。
LANGUAGES 项目是用什么语言写的

add_executable:定义工程会生成一个可执行程序

1
2
3
4
5
add_executable(可执行程序名 源文件名称)
#第一个参数是要生成的参数 第二个是要编译的文件
这里的可执行程序名和project中的项目名没有任何关系

源文件名可以是一个也可以是多个,如有多个可用空格或;间隔
1
2
3
4
5

# 样式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)

cmake写法

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0.0)

project (app)

add_executable(app add.c div.c main.c mult.c sub.c )

指向cmake 命令时,单独创建个目录存放cmake文件

第二步

通过add_executable设置的文件太多,可以通过设置变量的方法。
通过set()设置,set设置的值是字符串类型。
set有多种用法

1、取别名

1
2
3
4
5
# 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})

取出值必须用${}

2、指定c++编译标准

C++标准对应有一宏叫做DCMAKE_CXX_STANDARD。在CMake中想要指定C++标准有两种方式:

第一种:

1
2
3
4
5
6
7

#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)

第二种: 在执行 cmake 命令的时候指定出这个宏的值

#增加-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

3、指定输出的路径

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

set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
第一行:定义一个变量用于存储一个绝对路径
第二行:将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏
如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录。

练习

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

project (app)
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) //编译路经
set(app app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
add_executable(${app} ) //

第三步

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件罗列出来,
这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory命令或者file命令。

两种方式:

方式一

在 CMake 中使用aux_source_directory 命令可以查找某个路径下的所有源文件,命令格式为:

1
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})

方式二

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在CMake中为我们提供了搜索文件的命令,他就是file(当然,除了搜索以外通过 file 还可以做其他事情)。

1
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)

GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。

1
2
3
4
5
搜索当前目录的src目录下所有的源文件,并存储到变量中
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
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
1
2
3
4
5
6
7
8
9
10
11
第一种方式代码
cmake_minimum_required(VERSION 3.0.0)

project (app)
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
aux_source_directory(${PROJECT_SOURCE_DIR} SRC ) # //这个路径和cmake ../ 路径一样的意思
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
add_executable(${SRC} ) #

PORJECT_SOURCE_DIR这个路径和cmake ../ 路径一样的意思。

1
2
3
4
5
6
7
8
9
10
11
12
第二种
cmake_minimum_required(VERSION 3.0.0)

project (app)
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.c *.h ) #这个路径与CMakeLisits.txt 路径一致
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
add_executable(app ${SRC} ) #

第四步

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories:
include_directories(headpath)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
└── src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

3 directories, 7 files

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.0.0)

project (app)
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c *.h ) #这个路径与CMakeLisits.txt 路径一致
file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/*.c * )
set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
add_executable(app ${app} ) #

其中,第六行指定就是头文件的路径,PROJECT_SOURCE_DIR宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。

第五步

有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,
下面来讲解在cmake中生成这两类库文件的方法。

制作静态库

在cmake中,如果要制作静态库,需要使用的命令如下:

1
add_library(库名称 STATIC 源文件1 [源文件2] ...) 

在Linux中,静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。

在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

下面有一个目录,需要将src目录中的源文件编译成静态库,然后再使用:

1
2
3
4
5
6
7
8
9
10
11
.
├── build
├── CMakeLists.txt
├── include # 头文件目录
│ └── head.h
├── main.cpp # 用于测试的源文件
└── src # 源文件目录
├── add.cpp
├── div.cpp
├── mult.cpp
└── sub.cpp

根据上面的目录结构,可以这样编写CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST “${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp”)
add_library(calc STATIC ${SRC_LIST})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.0.0)

project (app)
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c *.h ) #这个路径与CMakeLisits.txt 路径一致
#file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/*.c * )
#set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) #添加头文件
#add_executable(app ${app} ) #编译成可执行程序

#add_library(mush SHARED ${SRC} ) #制作成静态库
add_library(mush STATIC ${SRC} ) #制作成动态库

set(LIBRARY_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib) #LIBRARY_OUTPUT_PATH生成可执行文件的路径

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

LIBRARY_OUTPUT_PATH生成可执行库文件的路径

制作动态库

在Linux中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。

在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

根据上面的目录结构,可以这样编写CMakeLists.txt文件:

1
add_library(库名称 SHARED 源文件1 [源文件2] ...)
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_library(calc SHARED ${SRC_LIST})

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

指定输出的路径

方式1 - 适用于动态库

对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:

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")
# 设置动态库生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(calc SHARED ${SRC_LIST})

对于这种方式来说,其实就是通过set命令给EXECUTABLE_OUTPUT_PATH宏设置了一个路径,这个路径就是可执行文件生成的路径。

方式2 - 都适用

由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH宏了,而应该使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用。

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

第六步

包含库文件

在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。

链接静态库

1
2
3
4
5
6
src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

现在我们把上面src目录中的add.cpp、div.cpp、mult.cpp、sub.cpp编译成一个静态库文件libcalc.a。
测试目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree 
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
├── lib
│ └── libcalc.a # 制作出的静态库的名字
└── src
└── main.cpp

4 directories, 4 files

在cmake中,链接静态库的命令如下:
link_libraries(<static lib> [<static lib>...])
参数1:指定出要链接的静态库的名字
可以是全名 libxxx.a
也可以是掐头(lib)去尾(.a)之后的名字 xxx
参数2-N:要链接的其它静态库的名字
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:

link_directories(<lib path>)
这样,修改之后的CMakeLists.txt文件内容如下:

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

添加了第8行的代码,就可以根据参数指定的路径找到这个静态库了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实战
cmake_minimum_required(VERSION 3.0.0)

project (app)
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
aux_source_directory(${PROJECT_SOURCE_DIR} SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
file(GLOB SR ${CMAKE_CURRENT_SOURCE_DIR}/lib/*.a ) #这个路径与CMakeLisits.txt 路径一致
#file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/*.c )
#set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) #添加头文件
#add_executable(app ${SRC} ) #编译成可执行程序
link_libraries(${SR}) #需要链接的库
link_directories( ${CMAKE_CURRENT_SOURCE_DIR}/lib)
#add_library(mush SHARED ${BL} ) #制作成静态库
#add_library(mush STATIC ${SRC} ) #制作成动态库

#set(LIBRARY_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib) #LIBRARY_OUTPUT_PATH生成可执行库文件的路径

链接动态库

在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的。
在cmake中链接动态库的命令如下:

1
2
3
4
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

target:指定要加载动态库的文件的名字

该文件可能是一个源文件
该文件可能是一个动态库文件
该文件可能是一个可执行文件

PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为PUBLIC
如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
动态库的链接具有传递性,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。

1
2
target_link_libraries(A B C)
target_link_libraries(D A)

PUBLIC:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。
PRIVATE:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库
INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号。

链接系统动态库

动态库的链接和静态库是完全不同的:
静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存,因此,在cmake中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app pthread)

在target_link_libraries(app pthread)中:

app: 对应的是最终生成的可执行程序的名字
pthread:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so,在指定的时候一般会掐头(lib)去尾(.so)。
动态库会被共享,共享一块内存,其他进程也会操作动态库的数据。

链接第三方动态库

现在,自己生成了一个动态库,对应的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
$ tree 
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h # 动态库对应的头文件
├── lib
│ └── libcalc.so # 自己制作的动态库文件
└── main.cpp # 测试用的源文件

3 directories, 4 files

假设在测试文件main.cpp中既使用了自己制作的动态库libcalc.so又使用了系统提供的线程库,此时CMakeLists.txt文件可以这样写:

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(app ${SRC_LIST})
target_link_libraries(app pthread calc)

在第六行中,pthread、calc都是可执行程序app要链接的动态库的名字。当可执行程序app生成之后并执行该文件,会提示有如下错误信息:

1
2
$ ./app 
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

这是因为可执行程序启动之后,去加载calc这个动态库,但是不知道这个动态库被放到了什么位置解决动态库无法加载的问题,所以就加载失败了,在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,指定静态库位置使用的也是这个命令:

1
link_directories(path)

所以修改之后的CMakeLists.txt文件应该是这样的:

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)

通过link_directories指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。
温馨提示:使用 target_link_libraries 命令就可以链接动态库,也可以链接静态库文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
综合之前的
cmake_minimum_required(VERSION 3.0.0)

project (app)
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
aux_source_directory(${PROJECT_SOURCE_DIR} SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
#file(GLOB SR ${CMAKE_CURRENT_SOURCE_DIR}/lib/*.a ) #这个路径与CMakeLisits.txt 路径一致
file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/lib1/*.so )
#set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) #添加头文件

add_executable(app ${SRC} ) #编译成可执行程序
#link_libraries(${SR}) #需要链接的库
target_link_libraries(app ${BL}) #链接动态库 这个命令需要写到最后,涉及程序的允许顺序
link_directories(${SRC})
#link_directories( ${CMAKE_CURRENT_SOURCE_DIR}/lib)
#add_library(mush SHARED ${BL} ) #制作成静态库
#add_library(mush SHARED ${SRC} ) #制作成动态库
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01)
#set(LIBRARY_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行库文件的路径
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行动态库文件的路径

第七步

日志
在CMake中可以用用户显示一条消息,该命令的名字为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 错误, 终止所有处理过程
CMake的命令行工具会在stdout上显示STATUS消息,在stderr上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。

CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。

1
2
3
4
5
6
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")

第八步

变量操作

追加

有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set命令也可以使用list命令。

使用set拼接

如果使用set进行字符串拼接,对应的命令格式如下:

1
set(变量名1 ${变量名1} ${变量名2} ...)

关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。

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

使用list拼接

如果使用list进行字符串拼接,对应的命令格式如下:

1
list(APPEND <list> [<element> ...])

list命令的功能比set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND表示进行数据追加,后边的参数和set就一样了。

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

在CMake中,使用set命令可以创建一个list。一个在list内部是一个由分号;分割的一组字符串。例如,set(var a b c d e)命令将会创建一个list:a;b;c;d;e,但是最终打印变量值的时候得到的是abcde。

1
2
3
4
set(tmp1 a;b;c;d;e)
set(tmp2 a b c d e)
message(${tmp1})
message(${tmp2})
1
2
3
4
输出的结果:

abcde
abcde

字符串移除

我们在通过file搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:

1
2
3
4
5
6
7
8
9
$ tree
.
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

0 directories, 5 files

在当前这么目录有五个源文件,其中main.cpp是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要add.cpp、div.cp、mult.cpp、sub.cpp这四个源文件就可以了。此时,就需要将main.cpp从搜索到的数据中剔除出去,想要实现这个功能,也可以使用list

1
list(REMOVE_ITEM <list> <value> [<value> ...])

通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM。

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}")
1
2
3
可以看到,在第8行把将要移除的文件的名字指定给list就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。

关于list命令还有其它功能,但是并不常用,在此就不一一进行举例介绍了。
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
cmake_minimum_required(VERSION 3.0.0)

project (app)
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
#file(GLOB SR ${CMAKE_CURRENT_SOURCE_DIR}/lib/*.a ) #这个路径与CMakeLisits.txt 路径一致
file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/lib1/*.so )
#set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) #添加头文件

add_executable(app ${SRC} ) #编译成可执行程序
#link_libraries(${SR}) #需要链接的库
#target_link_libraries(app ${BL}) #链接动态库 这个命令需要写到最后,涉及程序的允许顺序
#link_directories(${SRC})
#link_directories( ${CMAKE_CURRENT_SOURCE_DIR}/lib)
#add_library(mush SHARED ${BL} ) #制作成静态库
#add_library(mush SHARED ${SRC} ) #制作成动态库
message("${SRC}xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
list(APPEND SRC main.c) #字符串添加
message(${SRC})
list(REMOVE_ITEM SRC main.c )
list(REMOVE_ITEM SRC ${SRC}/main.c ) #注意在删除时,在地层维护的字符串带分号
message(${SRC}) #字符串可以带""也可不带
list(LENDTH SRC )
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01)
#set(LIBRARY_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行库文件的路径
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行动态库文件的路径

list其他操作

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
获取 list 的长度。

list(LENGTH <list> <output variable>)
LENGTH:子命令LENGTH用于读取列表长度
<list>:当前操作的列表
<output variable>:新创建的变量,用于存储列表的长度。
读取列表中指定索引的的元素,可以指定多个索引

list(GET <list> <element index> [<element index> ...] <output variable>)
<list>:当前操作的列表
<element index>:列表元素的索引
从0开始编号,索引0的元素为列表中的第一个元素;
索引也可以是负数,-1表示列表的最后一个元素,-2表示列表倒数第二个元素,以此类推
当索引(不管是正还是负)超过列表的长度,运行会报错
<output variable>:新创建的变量,存储指定索引元素的返回结果,也是一个列表。
将列表中的元素用连接符(字符串)连接起来组成一个字符串

list (JOIN <list> <glue> <output variable>)
<list>:当前操作的列表
<glue>:指定的连接符(字符串)
<output variable>:新创建的变量,存储返回的字符串
查找列表是否存在指定的元素,若果未找到,返回-1

list(FIND <list> <value> <output variable>)
<list>:当前操作的列表
<value>:需要再列表中搜索的元素
<output variable>:新创建的变量
如果列表<list>中存在<value>,那么返回<value>在列表中的索引
如果未找到则返回-1。
将元素追加到列表中

list (APPEND <list> [<element> ...])
在list中指定的位置插入若干元素

list(INSERT <list> <element_index> <element> [<element> ...])
将元素插入到列表的0索引位置

list (PREPEND <list> [<element> ...])
将列表中最后元素移除

list (POP_BACK <list> [<out-var>...])
将列表中第一个元素移除

list (POP_FRONT <list> [<out-var>...])
将指定的元素从列表中移除

list (REMOVE_ITEM <list> <value> [<value> ...])
将指定索引的元素从列表中移除

list (REMOVE_AT <list> <index> [<index> ...])
移除列表中的重复元素

list (REMOVE_DUPLICATES <list>)
列表翻转

list(REVERSE <list>)
列表排序

list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
COMPARE:指定排序方法。有如下几种值可选:
STRING:按照字母顺序进行排序,为默认的排序方法
FILE_BASENAME:如果是一系列路径名,会使用basename进行排序
NATURAL:使用自然数顺序排序
CASE:指明是否大小写敏感。有如下几种值可选:
SENSITIVE: 按照大小写敏感的方式进行排序,为默认值
INSENSITIVE:按照大小写不敏感方式进行排序
ORDER:指明排序的顺序。有如下几种值可选:
ASCENDING:按照升序排列,为默认值
DESCENDING:按照降序排列

第九步

宏定义

在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:

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

在程序的第七行对DEBUG宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,第八行就相当于被注释掉了,因此最终无法看到日志输入出(上述代码中并没有定义这个宏)。

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

1
$ gcc test.c -DDEBUG -o app

在gcc/g++命令中通过参数 -D指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏,其名字为DEBUG。

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

add_definitions(-D宏名称)
针对于上面的源文件编写一个CMakeLists.txt,内容如下:

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

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
综合所有
cmake_minimum_required(VERSION 3.0.0)

project (app)
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01 ) # //编译路经
#set(app add.c div.c main.c mult.c sub.c) //设置变量
#set(CMAKE_CXX_STANDARD )
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC) # //PORJECT_SOURCE_DIR这个路径和执行cmake ../ 路径一样的
#file(GLOB SR ${CMAKE_CURRENT_SOURCE_DIR}/lib/*.a ) #这个路径与CMakeLisits.txt 路径一致
file(GLOB BL ${CMAKE_CURRENT_SOURCE_DIR}/lib1/*.so )
#set(app ${SRC} ${BL})
#假设有两个目录里有源文件,就是用搜索的方式,搜索到以后使用set进行合并
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) #添加头文件

add_executable(app ${SRC} ) #编译成可执行程序
#link_libraries(${SR}) #需要链接的库
#target_link_libraries(app ${BL}) #链接动态库 这个命令需要写到最后,涉及程序的允许顺序
#link_directories(${SRC})
#link_directories( ${CMAKE_CURRENT_SOURCE_DIR}/lib)
#add_library(mush SHARED ${BL} ) #制作成静态库
#add_library(mush SHARED ${SRC} ) #制作成动态库
message("${SRC}xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
list(APPEND SRC main.c) #字符串添加
message(${SRC})
list(REMOVE_ITEM SRC main.c )
list(REMOVE_ITEM SRC ${SRC}/main.c ) #注意在删除时,在地层维护的字符串带分号
message(${SRC}) #字符串可以带""也可不带
add_definitions(-DDEBUG) #定义一个宏,起到一个开关的作用,很常用
set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01)
#set(LIBRARY_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行库文件的路径
#set(EXECUTABLE_OUTPUT_PATH /home/ygc/Desktop/stady_cmake/01/lib1) #LIBRARY_OUTPUT_PATH生成可执行动态库文件的路径

通过这种方式,上述代码中的第八行日志就能够被输出出来了。

预定义宏

1
2
3
4
5
6
7
8
9
10
11
宏	                                                功能
PROJECT_SOURCE_DIR 使用cmake命令后紧跟的目录,一般是工程的根目录
PROJECT_BINARY_DIR 执行cmake命令的目录
CMAKE_CURRENT_SOURCE_DIR 当前处理的CMakeLists.txt所在的路径
CMAKE_CURRENT_BINARY_DIR target 编译目录
EXECUTABLE_OUTPUT_PATH 重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH 重新定义目标链接库文件的存放位置
PROJECT_NAME 返回通过PROJECT指令定义的项目名称
CMAKE_BINARY_DIR 项目实际构建路径,假设在build目录进行的构建,那么得到的就是这个目录的路径


第十步

嵌套的CMake

如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个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
26
27
$ 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


include 目录:头文件目录
calc 目录:目录中的四个源文件对应的加、减、乘、除算法
对应的头文件是include中的calc.h
sort 目录 :目录中的两个源文件对应的是插入排序和选择排序算法
对应的头文件是include中的sort.h
test1 目录:测试目录,对加、减、乘、除算法进行测试
test2 目录:测试目录,对排序算法进行测试
可以看到各个源文件目录所需要的CMakeLists.txt文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。

准备工作

节点关系
众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:

根节点CMakeLists.txt中的变量全局有效
父节点CMakeLists.txt中的变量可以在子节点中使用
子节点CMakeLists.txt中的变量只能在当前节点中使用

添加子目录
接下来我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:
dd_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir:指定了CMakeLists.txt源文件和代码文件的位置,其实就是指定子目录
binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。
EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的ALL目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式CMakeLists.txt文件之间的父子关系就被构建出来了。

解决问题

在上面的目录中我们要做如下事情:

通过 test1 目录中的测试文件进行计算器相关的测试
通过 test2 目录中的测试文件进行排序相关的测试
现在相当于是要进行模块化测试,对于calc和sort目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。

根目录
根目录中的 CMakeLists.txt文件内容如下:

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

在根节点对应的文件中主要做了两件事情:定义全局变量和添加子目录。

定义的全局变量主要是给子节点使用,目的是为了提高子节点中的CMakeLists.txt文件的可读性和可维护性,避免冗余并降低出差的概率。
一共添加了四个子目录,每个子目录中都有一个CMakeLists.txt文件,这样它们的父子关系就被确定下来了。

calc 目录
calc 目录中的 CMakeLists.txt文件内容如下:

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

第3行aux_source_directory:搜索当前目录(calc目录)下的所有源文件
第4行include_directories:包含头文件路径,HEAD_PATH是在根节点文件中定义的
第5行set:设置库的生成的路径,LIB_PATH是在根节点文件中定义的
第6行add_library:生成静态库,静态库名字CALC_LIB是在根节点文件中定义的

sort 目录
sort 目录中的 CMakeLists.txt文件内容如下:

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

第6行add_library:生成动态库,动态库名字SORT_LIB是在根节点文件中定义的
这个文件中的内容和calc节点文件中的内容类似,只不过这次生成的是动态库。
在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库。

test1 目录
test1 目录中的 CMakeLists.txt文件内容如下:

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
# include_directories(${HEAD_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})

第4行include_directories:指定头文件路径,HEAD_PATH变量是在根节点文件中定义的
第6行link_libraries:指定可执行程序要链接的静态库,CALC_LIB变量是在根节点文件中定义的
第7行set:指定可执行程序生成的路径,EXEC_PATH变量是在根节点文件中定义的
第8行add_executable:生成可执行程序,APP_NAME_1变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。

test2 目录
test2 目录中的 CMakeLists.txt文件内容如下:

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

第四行include_directories:包含头文件路径,HEAD_PATH变量是在根节点文件中定义的
第五行set:指定可执行程序生成的路径,EXEC_PATH变量是在根节点文件中定义的
第六行link_directories:指定可执行程序要链接的动态库的路径,LIB_PATH变量是在根节点文件中定义的
第七行add_executable:生成可执行程序,APP_NAME_2变量是在根节点文件中定义的
第八行target_link_libraries:指定可执行程序要链接的动态库的名字
在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库。

构建项目
一切准备就绪之后,开始构建项目,进入到根节点目录的build 目录中,执行cmake 命令,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/cmake/calc/build

可以看到在build目录中生成了一些文件和目录,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
$ tree build -L 1     
build
├── calc # 目录
├── CMakeCache.txt # 文件
├── CMakeFiles # 目录
├── cmake_install.cmake # 文件
├── Makefile # 文件
├── sort # 目录
├── test1 # 目录
└── test2 # 目录


然后在build 目录下执行make 命令:

通过上图可以得到如下信息:

在项目根目录的lib目录中生成了静态库libcalc.a
在项目根目录的lib目录中生成了动态库libsort.so
在项目根目录的bin目录中生成了可执行程序test1
在项目根目录的bin目录中生成了可执行程序test2
最后再来看一下上面提到的这些文件是否真的被生成到对应的目录中了:

1
2
3
4
5
6
7
$ tree bin/ lib/
bin/
├── test1
└── test2
lib/
├── libcalc.a
└── libsort.so

由此可见,真实不虚,至此,项目构建完毕。

写在最后:
在项目中,如果将程序中的某个模块制作成了动态库或者静态库并且在CMakeLists.txt 中指定了库的输出目录,而后其它模块又需要加载这个生成的库文件,此时直接使用就可以了,如果没有指定库的输出路径或者需要直接加载外部提供的库文件,此时就需要使用 link_directories 将库文件路径指定出来。

第十一步

流程控制

在 CMake 的 CMakeLists.txt 中也可以进行流程控制,也就是说可以像写 shell 脚本那样进行条件判断和循环。

条件判断

条件判断取变量不需要${},直接变量名就可以
关于条件判断其语法格式如下:

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

在进行条件判断的时候,如果有多个条件,那么可以写多个elseif,最后一个条件可以使用else,但是开始和结束是必须要成对出现的,分别为:if和endif。
基本表达式

1
if(<expression>)

如果是基本表达式,expression 有以下三种情况:常量、变量、字符串。

如果是1, ON, YES, TRUE, Y, 非零值,非空字符串时,条件判断返回True
如果是 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND,空字符串时,条件判断返回False

逻辑判断

1
if(NOT <condition>)

其实这就是一个取反操作,如果条件condition为True将返回False,如果条件condition为False将返回True。

1
if(<cond1> AND <cond2>)

如果cond1和cond2同时为True,返回True否则返回False。

1
if(<cond1> OR <cond2>)

如果cond1和cond2两个条件中至少有一个为True,返回True,如果两个条件都为False则返回False。

比较

基于数值的比较

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

LESS:如果左侧数值小于右侧,返回True
GREATER:如果左侧数值大于右侧,返回True
EQUAL:如果左侧数值等于右侧,返回True
LESS_EQUAL:如果左侧数值小于等于右侧,返回True
GREATER_EQUAL:如果左侧数值大于等于右侧,返回True

基于字符串的比较

1
2
3
4
5
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)

STRLESS:如果左侧字符串小于右侧,返回True
STRGREATER:如果左侧字符串大于右侧,返回True
STREQUAL:如果左侧字符串等于右侧,返回True
STRLESS_EQUAL:如果左侧字符串小于等于右侧,返回True
STRGREATER_EQUAL:如果左侧字符串大于等于右侧,返回True

文件操作

判断文件或者目录是否存在

1
2
if(EXISTS path-to-file-or-directory)
如果文件或者目录存在返回True,否则返回False。

判断是不是目录

1
2
3
if(IS_DIRECTORY path)
此处目录的 path 必须是绝对路径
如果目录存在返回True,目录不存在返回False。

判断是不是软连接

1
2
3
4
if(IS_SYMLINK file-name)
此处的 file-name 对应的路径必须是绝对路径
如果软链接存在返回True,软链接不存在返回False。
软链接相当于 Windows 里的快捷方式

判断是不是绝对路径

1
2
3
4
5
if(IS_ABSOLUTE path)
关于绝对路径:
如果是Linux,该路径需要从根目录开始描述
如果是Windows,该路径需要从盘符开始描述
如果是绝对路径返回True,如果不是绝对路径返回False

其他操作(不常用)

判断某个元素是否在列表中

if(<variable|string> IN_LIST )
CMake 版本要求:大于等于3.3
如果这个元素在列表中返回True,否则返回False。
比较两个路径是否相等

if(<variable|string> PATH_EQUAL <variable|string>)
CMake 版本要求:大于等于3.24
如果这个元素在列表中返回True,否则返回False。
关于路径的比较其实就是另个字符串的比较,如果路径格式书写没有问题也可以通过下面这种方式进行比较:

if(<variable|string> STREQUAL <variable|string>)
我们在书写某个路径的时候,可能由于误操作会多写几个分隔符,比如把/a/b/c写成/a//b///c,此时通过STREQUAL对这两个字符串进行比较肯定是不相等的,但是通过PATH_EQUAL去比较两个路径,得到的结果确实相等的,可以看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.26)
project(test)

if("/home//robin///Linux" PATH_EQUAL "/home/robin/Linux")
message("路径相等")
else()
message("路径不相等")
endif()

message(STATUS "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")

if("/home//robin///Linux" STREQUAL "/home/robin/Linux")
message("路径相等")
else()
message("路径不相等")
endif()

输出的日志信息如下:

路径相等
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
路径不相等
通过得到的结果我们可以得到一个结论:在进行路径比较的时候,如果使用 PATH_EQUAL 可以自动剔除路径中多余的分割线然后再进行路径的对比,使用 STREQUAL 则只能进行字符串比较。

循环

在 CMake 中循环有两种方式,分别是:foreach和while。
另外一种foreach不做介绍了

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
while
除了使用foreach也可以使用 while 进行循环,关于循环结束对应的条件判断的书写格式和if/elseif 是一样的。while的语法格式如下:

while(<condition>)
<commands>
endwhile()
while循环比较简单,只需要指定出循环结束的条件即可:

cmake_minimum_required(VERSION 3.5)
project(test)
# 创建一个列表 NAME
set(NAME luffy sanji zoro nami robin)
# 得到列表长度
list(LENGTH NAME LEN)
# 循环
while(${LEN} GREATER 0)
message(STATUS "names = ${NAME}")
# 弹出列表头部元素
list(POP_FRONT NAME)
# 更新列表长度
list(LENGTH NAME LEN)
endwhile()
输出的结果如下:

$ cd build/
$ cmake ..
-- names = luffy;sanji;zoro;nami;robin
-- names = sanji;zoro;nami;robin
-- names = zoro;nami;robin
-- names = nami;robin
-- names = robin
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/robin/abc/a/build
可以看到当列表中的元素全部被弹出之后,列表的长度变成了0,此时while循环也就退出了。

扩展

添加选项 option(变量 “形容” OFF/ONM)

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(SORTTEST)
option(ADD_TEST "option for test" OFF)
if(ADD_TEST)
add_definitions(-DTEST)
message("----------")
endif()
add_executable(test main.cpp)

以上来自于rk课程第二章第一节,。rkaiq这个宏来自于哪里