当项目比较大的时候,往往需要将代码划分为几个模块,可能还会分离出部分通用模块,在多个项目之间同时使用;当然,也可能是依赖开源的第三方库,在项目中包含第三方源代码或者编译好的库文件。本文将会介绍CMake中如何模块化地执行编译,以及指定目标对相应库文件的依赖。
在上一篇文章中,笔者介绍了一个比较完备的CMakeists.txt
该如何书写。往期文章可以关注本号的话题:CMake,文章列表如下(文末可连续阅读):
但是上一篇文章介绍的CMakeLists.txt
一般是在项目初期的样子,随着项目代码原来越多,或者功能越来越多,代码可能会分化出不同的功能模块,并且有一些可能是多个项目通用的模块,这时为了更好地管理各个模块,可以为每个模块都编写一个CMakeLists.txt
文件,然后在父级目录中对不同编译目标按需添加依赖。
本文着重介绍下面的内容:
-
模块化管理构建系统(add_subdirectory)
-
导入编译好的目标文件
-
添加库依赖
一 模块化构建
在前面的文章中介绍过,CMakeLists.txt
是定义一个目录(Source Tree)的构建系统的,所以对于模块化构建,其实就是分别为每一个子模块目录编写一个CMakeLists.txt
,在其父目录中“导入”子目录的构建系统生成对应的目标,以便在父目录中使用。
下面仍以开源项目:https://gitee.com/RealCoolEngineer/cmake-template为例,基于上一篇文章的状态进行修改,本文对应的commit id为:4bfb85b
。
假设项目目录结构如下:
./cmake-template
├── CMakeLists.txt
├── src
│ └── c
│ ├── cmake_template_version.h
│ ├── cmake_template_version.h.in
│ ├── main.c
│ └── math
│ ├── add.c
│ ├── add.h
│ ├── minus.c
│ └── minus.h
└── test
└── c
├── test_add.c
└── test_minus.c
现在的编译任务为:
-
将math目录视为子模块,为其单独定义构建系统
-
整个项目依赖math模块的编译结果,生成其他目标文件
1 定义子目录的构建系统
只要是定义目录的构建系统,都是在此目录下创建一个CMakeLists.txt
文件,其结构和语法在上一篇文章已经介绍的比较详细。
因为主要进行模块的编译工作,所以一般只需要编译构建库文件(静态库或者动态库),以及针对该库对外提供接口的一些单元测试即可,所以可以写的比较简单一些。
在src/math
目录下新建CMakeLists.txt
文件,内容如下:
cmake_minimum_required(VERSION 3.12)
project(CMakeTemplateMath VERSION 0.0.1 LANGUAGES C CXX)
aux_source_directory(. MATH_SRC)
message("MATH_SRC: ${MATH_SRC}")
add_library(math STATIC ${MATH_SRC})
如上代码所示,对于子目录(模块),一般也有自己的project
命令,同时如果有需要,也可以指定自己的版本号。
这里使用了一个此前没有提到的命令:aux_source_directory
,该命令可以搜索指定目录(第一个参数)下的所有源文件,将源文件的列表保存到指定的变量(第二个参数)。
2 包含子目录
通过命令add_subdirectory
包含一个子目录的构建系统,其命令格式如下:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
其中source_dir
就是要包含的目标目录,该目录下必须存在一个CMakeLists.txt
文件,一般为相对于当前CMakeLists.txt
的目录路径,当然也可以是绝对路径;
binary_dir
是可选的参数,用于指定子构建系统输出文件的路径,相对于当前的Binary tree
,同样也可以是绝对路径。一般情况下,source_dir
是当前目录的子目录,那么binary_dir
的值为不做任何相对路径展开的source_dir
;但是如果source_dir
不是当前目录的子目录,则必须指定binary_dir
,这样CMake才知道要将子构建系统的相关文件生成在哪个目录下。
如果指定了EXCLUDE_FROM_ALL
选项,在子路径下的目标默认不会被包含到父路径的ALL
目标里,并且也会被排除在IDE工程文件之外。但是,如果在父级项目显式声明依赖子目录的目标文件,那么对应的目标文件还是会被构建以满足父级项目的依赖需求。
综上,可以修改cmake-template
项目根目录下的CMakeLists.txt
文件,将原来的如下内容:
# Build math lib
add_library(math STATIC ${MATH_LIB_SRC})
修改为:
add_subdirectory(src/c/math)
构建的静态库的名字依旧是math
,所以在编译demo
目标时,链接的库的名字不用修改:
# Build demo executable
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
此时构建和编译的命令没有任何改变:
➜ cmake-template # cmake -B cmake-build
➜ cmake-template # cmake --build cmake-build
上面的命令指定父项目的生成路径(Binary tree)为cmake-build
,那么子模块(math)的生成路径为cmake-build/src/c/math
,也就是说binary_dir
为src/c/math
,等同于source_dir
。
二 导入编译好的目标文件
在前面介绍的命令add_subdirectory
其实是相当于通过源文件来构建项目所依赖的目标文件,但是CMake也可以通过命令来导入已经编译好的目标文件。
1 导入库文件
使用add_library
命令,通过指定IMPORTED
选项表明这是一个导入的库文件,通过设置其属性指明其路径:
add_library(math STATIC IMPORTED)
set_property(TARGET math PROPERTY
IMPORTED_LOCATION "./lib/libmath.a")
对于库文件的路径,也可以使用find_library
命令来查找,比如在lib
目录下查找math
的Realse和Debug版本:
find_library(LIB_MATH_DEBUG mathd HINTS "./lib")
find_library(LIB_MATH_RELEASE math HINTS "./lib")
对于不同的编译类型,可以通过IMPORTED_LOCATION_<CONFIG>
来指明不同编译类型对应的库文件路径:
add_library(math STATIC IMPORTED GLOBAL)
set_target_properties(math PROPERTIES
IMPORTED_LOCATION "${LIB_MATH_RELEASE}"
IMPORTED_LOCATION_DEBUG "${LIB_MATH_DEBUG}"
IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
)
导入成功以后,就可以将该库链接到其他目标上,但是导入的目标不可以被install
。
这里以导入静态库为例,导入动态库或其他类型也是类似的操作,只需要将文件类型STATIC
修改成对应的文件类型即可。
2 导入可执行文件
这个不是那么常用,为了文章完整性,顺便提一下。是和导入库文件类似的:
add_executable(demo IMPORTED)
set_property(TARGET demo PROPERTY
IMPORTED_LOCATION "./bin/demo")
三 库依赖
这里主要着重介绍一下target_link_libraries
命令的几个关键字:
-
PRIVATE
-
INTERFACE
-
PUBLIC
这三个关键字的主要作用是指定的是目标文件依赖项的使用范围(scope),所以可以专门了解一下。
假设某个项目中存在两个动态链接库:动态链接库liball.so
、动态链接库libsub.so
。
对于PRIVATE关键字,使用的情形为:liball.so
使用了libsub.so
,但是liball.so
并不对外暴露libsub.so
的接口:
target_link_libraries(all PRIVATE sub)
target_include_directories(all PRIVATE sub)
对于INTERFACE关键字,使用的情形为:liball.so
没有使用libsub.so
,但是liball.so
对外暴露libsub.so
的接口,也就是liball.so
的头文件包含了libsub.so
的头文件,在其它目标使用liball.so
的功能的时候,可能必须要使用libsub.so
的功能:
target_link_libraries(all INTERFACE sub)
target_include_directories(all INTERFACE sub)
对于PUBLIC关键字(PUBLIC=PRIVATE+INTERFACE),使用的情形为:liball.so
使用了libsub.so
,并且liball.so
对外暴露了libsub.so
的接口:
target_link_libraries(all PUBLIC sub)
target_include_directories(all PUBLIC sub)
这里的内容可以有个大概了解即可,随着后续深入使用,自然会水到渠成。