CMake应用:模块化及库依赖

来源:公众号【很酷的程序员/RealCoolEngineer】

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

在上一篇文章中,笔者介绍了一个比较完备的CMakeists.txt该如何书写。往期文章可以关注文集:CMake,部分文章如下:

  1. CMake应用:基础篇
  2. CMake应用:核心语法篇
  3. CMake应用:CMakeists.txt完全指南

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

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

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

一 模块化构建

在前面的文章中介绍过,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

现在的编译任务为:

  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_dirsrc/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命令的几个关键字:

  1. PRIVATE
  2. INTERFACE
  3. 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)

这里的内容可以有个大概了解即可,随着后续深入使用,自然会水到渠成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章