CMake应用:模块化及库依赖

CMake应用:模块化及库依赖

当项目比较大的时候,往往需要将代码划分为几个模块,可能还会分离出部分通用模块,在多个项目之间同时使用;当然,也可能是依赖开源的第三方库,在项目中包含第三方源代码或者编译好的库文件。本文将会介绍CMake中如何模块化地执行编译,以及指定目标对相应库文件的依赖。

在上一篇文章中,笔者介绍了一个比较完备的 CMakeists.txt 该如何书写。往期文章可以查看专栏: CMake实践应用专题 ,上一篇文章链接如下:

但是上一篇文章介绍的 CMakeLists.txt 一般是在项目初期的样子,随着项目代码原来越多,或者功能越来越多,代码可能会分化出不同的功能模块,并且有一些可能是多个项目通用的模块,这时为了更好地管理各个模块,可以为每个模块都编写一个 CMakeLists.txt 文件,然后在父级目录中对不同编译目标按需添加依赖。

本文着重介绍下面的内容:

  1. 模块化管理构建系统(add_subdirectory)
  2. 导入编译好的目标文件
  3. 添加库依赖

一 模块化构建

在前面的文章中介绍过, CMakeLists.txt 是定义一个目录(Source Tree)的构建系统的,所以对于模块化构建,其实就是分别为每一个子模块目录编写一个 CMakeLists.txt ,在其父目录中“导入”子目录的构建系统生成对应的目标,以便在父目录中使用。

下面仍以开源项目: gitee.com/RealCoolEngin 为例,基于上一篇文章的状态进行修改,本文对应的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

现在的编译任务为:

  1. 将math目录视为子模块,为其单独定义构建系统
  2. 整个项目依赖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"