1. CMake介绍
CMake一般用来构建大型项目,为了弄清整个项目的结构,源码在哪里,生成的产物在哪里等等,了解CMake以后以上问题都会迎刃而解。
1.1 构建
一般来说,为了方便管理,避免弄乱项目结构,我们会新一个build目录去存放生成的产物。如果不新建build会生成很多杂乱的文件,相关文件,可以参考第二节或者Linux静态库和动态库 | 就是要摸鱼 (wjdmyby98k.github.io)里面的c文件。
上面执行的过程如下
- 书写CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)
cmake_minimum_required:指定使用的 cmake 的最低版本 可选,非必须,如果不加可能会有警告
project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。可选项很多, 我们一般写一个工程名就行了
add_executable:定义工程会生成一个可执行程序
add_executable(可执行程序名 源文件名称)
这里的可执行程序名和project中的项目名没有任何关系,有多个源文件,可以用空格或;间隔
执行cmake命令,一般就是
cmake .
cmake ..
# cmake 命令原型 $ cmake CMakeLists.txt文件所在路径
执行make
当执行cmake命令以后,会根据CMakeLists.txt生成一些文件,其中就有makefile文件,然后再执行make命令进行构建,就能得到可执行程序。
根据前面的测试可以发现,生成的文件跟源码混在一起,比较混乱,所以为了方便管理,一般都新建一个build目录去存放生成产物。这时候在build目录下执行cmake ..
1.2 CMake常用命令
定义变量
# SET 指令的语法是:
# VAR表示变量名称,VALUE表示变量的值,可以为多个,后面一些可选项,用到的时候查文档即可
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
# 方式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})
指定使用的C++标准
gcc/g++编译的时候我们一般这样写
g++ *.cpp -std=c++11 -o app
上面的例子中通过参数-std=c++11
指定出要使用c++11
标准编译程序,C++标准对应有一宏叫做DCMAKE_CXX_STANDARD
。在CMake中想要指定C++标准有两种方式:
- 在CMakeLists.txt中通过set指定:
set(CMAKE_CXX_STANDARD 11)
- 在执行cmake的时候设置宏的值:
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
指定输出路径
同上面一样,在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值还是通过set命令进行设置:
set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
搜索文件
一般来说有两种方式,aux_source_directory
命令或者file
命令
1、aux_source_directory
aux_source_directory(< dir > < variable >)
# 搜索 src 目录下的文件,存在SRC_LIST变量中
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
2、file
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
- GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
- GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
# 搜索当前目录的src目录下所有的源文件,并存储到变量中
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
# 关于要搜索的文件路径和类型可加双引号,也可不加:
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
包含头文件
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories:
include_directories(${PROJECT_SOURCE_DIR}/include)
PROJECT_SOURCE_DIR
宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。
1.3 制作静态库或动态库
静态库
add_library(库名称 STATIC 源文件1 [源文件2] ...)
动态库
add_library(库名称 SHARED 源文件1 [源文件2] ...)
1.4 指定输出路径
在Linux中生成的动态库,默认是有可执行权限的,静态库默认不具有可执行权限。
针对动态库
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
宏设置了一个路径,这个路径就是可执行文件生成的路径都适用,设置
LIBRARY_OUTPUT_PATH
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})
如果只是单纯的生成库,这两种方式足以,但其实还有几个宏,可以指定生成路径,可参考2.2 存在的问题
1.5 链接库文件
静态库
link_libraries(<static lib> [<static lib>...]) # 链接静态库 link_libraries(calc)
如果是自定义的库,一般还需指明一下库的路径
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})
动态库
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中定义的方法。
target_link_libraries(A B C) target_link_libraries(D A)
- PUBLIC:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用
- PRIVATE:在private后面的库仅被Link到前面的target中,并且终结掉,第三方不能感知你调了啥库
- INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号
这里需要注意的是,动态库的加载和静态库的加载是不同的,在Linux静态库和动态库 | 就是要摸鱼 (wjdmyby98k.github.io)已经详细解释过。静态库在生成可执行程序的时候,就被打包到了可执行程序中,可执行程序一启动就被加载到内存中,而动态库只有在库函数被调用的时候,才会加载到内存中。
因此我们链接动态库是先生成可执行程序,再链接,而静态库则是先链接!
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)
在上面的例子中,用link_libraries链接静态库,使用target_link_libraries命令链接动态库,但事实上,target_link_libraries也可以链接静态库文件。
1.6 常见的宏⭐️
CMAKE_BINARY_DIR
:项目实际构建路径,也就是cmake命令执行的路径,比如/build目录CMAKE_ARCHIVE_OUTPUT_DIRECTORY
:默认存放静态库的文件夹位置;CMAKE_LIBRARY_OUTPUT_DIRECTORY
:默认存放动态库的文件夹位置;LIBRARY_OUTPUT_PATH
:默认存放库文件的位置,如果产生的是静态库并且没有指定CMAKE_ARCHIVE_OUTPUT_DIRECTORY
则存放在该目录下,动态库也类似;CMAKE_RUNTIME_OUTPUT_DIRECTORY
:存放可执行软件的目录;PROJECT_SOURCE_DIR
:在使用cmake命令时,后面紧跟的目录,一般是工程的根目录;CMAKE_SOURCE_DIR
: 最外层CMakeLists.txt所在目录;CMAKE_CURRENT_SOURCE_DIR
:当前正在处理的源目录的路径;
2. 嵌套的CMake⭐️
如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
先来看一下下面的这个的目录结构:
$ 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文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。
2.1 准备工作
节点关系
众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:
- 根节点CMakeLists.txt中的变量全局有效
- 父节点CMakeLists.txt中的变量可以在子节点中使用
- 子节点CMakeLists.txt中的变量只能在当前节点中使用
添加子目录
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
- source_dir:指定了CMakeLists.txt源文件和代码文件的位置,其实就是指定子目录
- binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。
- EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的ALL目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式CMakeLists.txt文件之间的父子关系就被构建出来了。
2.2 解决问题
在上面的目录中我们要做如下事情:
- 通过 test1 目录中的测试文件进行计算器相关的测试
- 通过 test2 目录中的测试文件进行排序相关的测试
现在相当于是要进行模块化测试,对于calc和sort目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。
一般来说,这里要么是把源文件编译成一个可执行文件,要么就是编译成一个库,然后在搞一个test目录!
根目录🚀
根目录中的 CMakeLists.txt文件内容如下:
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文件内容如下:
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文件内容如下:
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文件内容如下:
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})
- 第4行include_directories:指定头文件路径,HEAD_PATH变量是在根节点文件中定义的
- 第6行link_libraries:指定可执行程序要链接的静态库,CALC_LIB变量是在根节点文件中定义的
- 第7行set:指定可执行程序生成的路径,EXEC_PATH变量是在根节点文件中定义的
- 第8行add_executable:生成可执行程序,APP_NAME_1变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。
test2目录
test2 目录中的 CMakeLists.txt文件内容如下:
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 命令,如下:
$ 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目录中生成了一些文件和目录,如下所示:
$ 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
最后再来看一下上面提到的这些文件是否真的被生成到对应的目录中了:
$ tree bin/ lib/
bin/
├── test1
└── test2
lib/
├── libcalc.a
└── libsort.so
存在的问题⭐️
但是上面的方式还是有一点问题,因为我们在根目录的CMakeLists.txt中定义了
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
所以最终生成的库和执行文件是在根目录下的lib和bin目录,有没有什么方式可以让生成的文件全部在build目录下的?
CMAKE_BINARY_DIR
:项目实际构建路径,也就是cmake命令执行的路径,比如/build目录CMAKE_ARCHIVE_OUTPUT_DIRECTORY
:默认存放静态库的文件夹位置;CMAKE_LIBRARY_OUTPUT_DIRECTORY
:默认存放动态库的文件夹位置;LIBRARY_OUTPUT_PATH
:默认存放库文件的位置,如果产生的是静态库并且没有指定CMAKE_ARCHIVE_OUTPUT_DIRECTORY
则存放在该目录下,动态库也类似;CMAKE_RUNTIME_OUTPUT_DIRECTORY
:存放可执行软件的目录;
了解了上面五个宏以后,我们就可以这样修改,删除根目录下定义的库文件和可执行文件的定义,子目录也相应删掉。根目录添加
# 设置可执行文件和库的输出目录
# 可执行文件
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 动态库
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 库
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib)
这样整个目录结构就很清晰了!