CMake应用:模块化及库依赖
当项目比较大的时候,往往需要将代码划分为几个模块,可能还会分离出部分通用模块,在多个项目之间同时使用;当然,也可能是依赖开源的第三方库,在项目中包含第三方源代码或者编译好的库文件。本文将会介绍CMake中如何模块化地执行编译,以及指定目标对相应库文件的依赖。
在上一篇文章中,笔者介绍了一个比较完备的
CMakeists.txt
该如何书写。往期文章可以查看专栏:
CMake实践应用专题
,上一篇文章链接如下:
但是上一篇文章介绍的
CMakeLists.txt
一般是在项目初期的样子,随着项目代码原来越多,或者功能越来越多,代码可能会分化出不同的功能模块,并且有一些可能是多个项目通用的模块,这时为了更好地管理各个模块,可以为每个模块都编写一个
CMakeLists.txt
文件,然后在父级目录中对不同编译目标按需添加依赖。
本文着重介绍下面的内容:
- 模块化管理构建系统(add_subdirectory)
- 导入编译好的目标文件
- 添加库依赖
一 模块化构建
在前面的文章中介绍过,
CMakeLists.txt
是定义一个目录(Source Tree)的构建系统的,所以对于模块化构建,其实就是分别为每一个子模块目录编写一个
CMakeLists.txt
,在其父目录中“导入”子目录的构建系统生成对应的目标,以便在父目录中使用。
下面仍以开源项目:
https://
gitee.com/RealCoolEngin
eer/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"