博客参考自:爱编程的大丙: https://subingwen.cn/cmake/CMake-primer/ ,仅供学习分享使用
如果项目很大
,或者项目中有很多的源码目录,在通过 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 目录
:测试目录,对calc中实现的加、减、乘、除
算法进行测试test2 目录
:测试目录,对sort中实现排序算法进行测试
从目录结构可以看出,最终会生成两个可执行文件,一个是计算器相关的,一个是排序相关的。如果把所有操作都写在顶层的CMakeLists.txt
中,那么这个CMakeLists.txt
的内容就会比较复杂了。有些时候为了让这些脚本文件更加容易维护,我们就可以通过逐个击破的方式,把文件的内容拆分,每个源文件中单独的CMakeLists.txt
进行处理
calc目录中有若干个源文件,我们在该目录中添加一个CMakeLists
去管理这些源文件。sort目录中有两个排序的源文件,我们可以在sort
目录中也添加一个CMakeLists.txt
;对于test
和test2
中分别有一个测试文件,我们在test1
和test2
中分别添加一个CMakeLists.txt
把这些文件添加好后,需要通过顶层的CMakeLists.txt
来让定义若干个CMakeLists.txt形成父子关系
。怎么让父节点绑定这些子节点呢?使用add_subdirectory
来添加这些子目录。这样的话,顶层的CMakeLists就知道它的各个子节点。
在每个CMakeLists.txt中我们都可以定一些变量
,当我们在父节点定义了某些变量后,父节点中定义的变量是可以被子节点使用的(父节点定义的这些变量是全局变量
),在子节点中定义的变量称为局部变量
。父节点无法使用子节点的变量,但子节点中可以使用父节点定义的变量。
1 准备工作
1.1 节点关系
众所周知,Linux 的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层
的 CMakeLists.txt 是根节点
,其次
都是子节点
。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:
根节点
CMakeLists.txt 中的变量全局有效父节点
CMakeLists.txt 中的变量可以在子节点中使用子节点
CMakeLists.txt 中的变量只能在当前节点中使用
1.2 添加子目录
接下来我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
注意source_dir
指定的是子节点CMakeLists.txt所在的目录
source_dir
:指定了 CMakeLists.txt 源文件和代码文件的位置,其实就是指定子目录binary_dir
:指定了输出文件的路径,一般不需要指定,忽略即可。EXCLUDE_FROM_ALL
:在子路径下的目标默认不会被包含到父路径的 ALL 目标里,并且也会被排除在 IDE 工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式 CMakeLists.txt 文件之间的父子关系就被构建出来了
2 示例说明
项目目录结构图像
├── CMakeLists.txt
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert.cpp
│ └── select.cpp
├── test1
│ ├── calc.cpp
│ └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp
- 首先有一个根节点,在根节点中有一个
CMakeList.txt
, 然后有4
个子节点,分别为calc
,sort
,test1
,test2
,这些子节点中都有各自的CMakeLists.txt
分析:
在test1
和test2
中的CMakeLists作用其实就是分别生成一个可执行程序
,假设分别为app1和app2;对于calc
目录中的4个源文件add.cpp
,div.cpp
,mult.cpp
,sub.cpp
,其实它是提供接口供test1中的calc.cpp使用的,所以我们需要把它生成对应的库文件
(动/静态库)。对于sort目录中的源文件insert.cpp
和select.cpp
也是一样的,它是为test2中的sort.cpp提供接口,我们同样把它生成库文件(动/静态库)库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式
。
如何选择生成的是静态库还是动态库?
- 到底是生成动态库还是静态库呢?其实这两种都是可以的,
推荐
: 在源文件非常多,推荐生成动态库;在源文件比较少的情况,推荐生成静态库
。 - 如果生成
动态库
,它所对应的源代码是不会打包的可执行文件中的,如果是静态库
则会打包
到可执行文件的内部。如果我们最终要求,生成的可执行文件非常小,那么我们就可以把这些源文件生成一个动态库,然后再发布应用程序时,一定要把该动态库提供给程序的使用者;如果我们对生成可执行文件的大小没有要求,并且要求使用比较简便,我们就可以把这些源文件生成静态库,生成静态库之后这些源文件就会打包到可执行文件中,所以在发布的时候只要发布一个可执行程序就可以了,不需要发布额外的库文件了。 - 使用
静态库的缺点
是最终生成的可执行文件会比较大
,另外如果启动多个可执行程序,它占用的物理内存
也比动态库要大
。因为如果程序使用的是动态库
,不管启动多少个可执行程序,在内存中动态库有且只有一份
,是共享的
。
2.1 根目录
根目录
中的 CMakeLists.txt 可以定义一些全局变量
,定义的变量可以给子节点使用,比如:定义头文件的路径
,生成可执行文件的名字
,以及子节点生成库文件的存储路径
等,这些都可以在根目录的CMakeLists.txt
中先定义出来。后面在子节点中直接使用根节点定义的变量,就可以找到对应的路径。
根目录中的 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib) #lib子目录不需要自己去创建,如果lib目录没有,会自动创建
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin) #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
文件,这样它们的父子关系就被确定下来了。
2.2 子目录中的CMakeList.txt
写完了顶层的CMakeList.txt,现在可以写4个子目录中的CMakeList.txt
文件。
2.2.1 calc 目录
calc 目录中的 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALCLIB)
aux_source_directory(./ SRC) # ./ 表示当前 cmakelist.txt所在目录下
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
是在根节点文件中定义的
2.2.2 sort 目录
sort目录也是生成库文件,和calc中CMakelists.txt的写法其实是差不多的。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 节点文件中的内容类似,只不过这次生成的是动态库。
在生成
库文件
的时候,这个库可以是静态库
也可以是动态库
,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库
。
2.2.3 test1 目录
test1 生成可执行文件
,可执行文件依赖于上面生成的libcalc.a
静态库。需要链接静态库
编译得到当前的可执行程序。test1目录中的 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC) # 搜索的还是当前cmakelist目录下的源文件
include_directories(${HEAD_PATH})
# 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不知道该静态库的存放路径,所以还需要指定需要链接的静态库的路径:link_directories
- 第 4 行
include_directories
:指定头文件路径,HEAD_PATH 变量是在根节点文件中定义的 - 第 6 行
link_libraries
:指定可执行程序要链接的静态库
,CALC_LIB 变量是在根节点文件中定义的 - 第 7 行
set
:指定可执行程序生成的路径,EXEC_PATH 变量是在根节点文件中定义的 - 第 8 行
add_executable
:生成可执行程序,APP_NAME_1 变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。
2.2.4 test2 目录
与test1一样,test2目录的CMakeLists也是生成一个可执行文件,CMakelists.txt中的指令基本差不多。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
:指定可执行程序要链接的动态库的名字,target_link_libraries
需要在add_executable
命令之后,因为先必须生成可执行文件,然后target_link_libraries才能将库文件链接到可执行文件中
在生成可执行程序的时候,
动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库
。
2.2 构建项目
一切准备就绪之后,开始构建项目,进入到根节点目录的 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 中指定了库的输出目录
,而后其它模块又需要加载这个生成的库文件,此时直接使用就可以了,如果没有指定库的输出路径或者需要直接加载外部提供的库文件,此时就需要使用 link_directories 将库文件路径指定出来
。