0%

cmake 迷你系列(三)分离你的模块

引言

我们会创建静态库以及动态库之后,接下要分离我们的模块,既然工具包不仅仅是提供给 main 程序的,我们就没有别要它们放到一起了。

包就要规整

我们创建出另外一个包 utils,结构如下:

1
2
3
4
5
6
7
8
9
test
|---CMakeLists.txt
|---src
|---CMakeLists.txt
|---main.c
|---utils
|---CMakeLists.txt
|---utils.c
|---utils.h

以上我们做了以下改动:

  1. 新建 utils 包,叫模块应该更加合理。
  2. 将 utils.c、utils.h 放到 utils 模块下。
  3. 同时,我们发现每个模块下都有一个 CMakeLists.txt 文件,这是必须的,如果说你的模块需要参与编译。

整理 CMakeLists.txt

既然每个包下有一个 CMakeLists.txt 文件,我们接下来要做的就是把根路径里的 CMakeLists.txt 内容提取到各自模块中的 CMakeLists.txt 里。我们先回顾一下根中 CMakeLists.txt 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

set(MAIN_SOURCE
${PROJECT_SOURCE_DIR}/src/main.c
)

set(UTILS_SOURCE
utils/utils.c
)
unset(UTILS_LIB CACHE)
find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build)
#add_library(utils SHARED ${UTILS_SOURCE})

add_executable(test ${MAIN_SOURCE})
target_link_libraries(test ${UTILS_LIB})

我们要做的就是把 main 下相关的编译信息放到 src 包中的 CMakeLists.txt:

1
2
3
4
5
6
set(MAIN_SOURCE
main.c
)

add_executable(test ${MAIN_SOURCE})
target_link_libraries(test ${UTILS_LIB})

然后把 utils 模块相关信息放到 utils 模块中的 CMakeLists.txt 文件中:

1
2
3
4
5
set(UTILS_SOURCE
utils.c
)

add_library(utils SHARED ${UTILS_SOURCE})

最后我们根路径中的 CMakeLists.txt 中就剩下如下内容:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

unset(UTILS_LIB CACHE)
find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build)

是不是精简了很多?而且各自模块的功能就要放到各自的模块,根包中的就只是单纯的做通用处理。到了这里我们还是没有办法启动我们的项目的,因为我们需要为根中的 CMakeLists.txt 指定它的子。本示例中,我们的子有两个,一个是 src 下的,一个是 utils 下的,所以我们要加进来:

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

unset(UTILS_LIB CACHE)
find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build/utils)

add_subdirectory(src)
add_subdirectory(utils)

9 行、10 行我们通过 add_subdirectory 指令,将 src、utils 两个模块加入进来。同时我们还要注意第 7 行路径改为:/build/utils,因为添加 utils 模块之后,就会在 build 里添加对应的包了,后续我们会讲解如何打包到指定位置,这样就不用修改我们的程序了。

安装你的库

由于编译之后,我们需要修改我们的库路径,所以这里我们指定我们的库安装路径。这样方便查找。
我们修改 utils 模块中的 CMakeLists.txt 内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
set(UTILS_SOURCE
utils.c
)

add_library(utils STATIC ${UTILS_SOURCE})
add_library(utils-share SHARED ${UTILS_SOURCE})

file(STRINGS VERSION version)

set_target_properties(utils-share
PROPERTIES
OUTPUT_NAME utils
PUBLIC_HEADER utils.h
VERSION ${version}
)

install(TARGETS utils utils-share
ARCHIVE DESTINATION ${LIB_INSTALL_DIR}
LIBRARY DESTINATION ${LIB_INSTALL_DIR}
PUBLIC_HEADER DESTINATION ${LIB_INCLUDE_DIR}
)

内容比较多,不过没有关系,我们一步一步分析:

  1. 5 行、6 行,我们这次同时生成动态库与静态库,由于变量明不能重复,所以动态库我们叫 utils-share。
  2. 我们生成库的时候,一般都会定义一个版本号,所以我们就用 file 指令读取当前路径下的 VERSION 文件中的版本号,并存于 version 变量中。
  3. 由于变量不能重名,导致动态库叫 utils-share,这显然不是我们想要的,所以我们需要给它重新命名,这也是第 10 行的作用。我们修改 utils-share 的属性,OUTPUT_NAME 用来指定动态库生成的名字;PUBLIC_HEADER 用来将我们的头文件暴露出来,毕竟库的使用方通过该头文件才能知道库提供的功能有哪些。VERSION 用于指定动态库的版本号。
  4. 我们在第 17 行安装我们的库,TARGETS 指定我们要安装的目标对象,这里是 utils 静态库,utils-share 动态库两个。接下来的 ARCHIVE 指定我们静态库安装路径,LIBRARY 指定我们动态库安装路径,PUBLIC_HEADER 指定我们头文件安装路径。
  5. 指令中的 LIB_INSTALL_DIR 以及 LIB_INCLUDE_DIR 则是在根路径定义的安装路径。

以上命令会生成三个库文件:libutils.a、libutils.1.0.0.dylib、libutils.dylib。其中 1.0.0 就是 VERSION 文件中定义的版本号,而 libutils.dylib 是一个软链,指向了 libutils.1.0.0.dylib。

最终根路径中的 CMakeLists.txt 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

set(LIB_INSTALL_DIR ${PROJECT_SOURCE_DIR}/libs/lib)
set(LIB_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/libs/include)

unset(UTILS_LIB CACHE)
find_library(UTILS_LIB NAMES utils PATHS ${LIB_INSTALL_DIR})
find_path(UTILS_HEADER NAMES utils.h PATHS ${LIB_INCLUDE_DIR})

#include_directories(${UTILS_HEADER})

add_subdirectory(src)
add_subdirectory(utils)
  1. 6 行、7 行指定了静态库以及头文件的安装路径。
  2. 10 行指定我们的库文件路径。
  3. 11 行我们这里指定了头文件的安装路径。
  4. 13 行将我们找到的头文件包含进我们的程序中,有读者可能会有疑问,为什么不直接使用 include_directories 把头文件加进来,非要在用个 find_path 呢?其实这里主要是介绍一下 find_path 的用法,一方面是由于 include_directories 的范围有点大,所以我们更推荐的做法是采用 target_include_directories() 指令。除非该头文件是全局都需要使用的。这里我们就直接采用后者的方式,将头文件仅仅包括到当前编译文件中。

src 下的 CMakeLists.txt 为:

1
2
3
4
5
6
7
set(MAIN_SOURCE
main.c
)

add_executable(test ${MAIN_SOURCE})
target_include_directories(test PRIVATE ${UTILS_HEADER})
target_link_libraries(test ${UTILS_LIB})
  1. 6 行,我们将头文件编译到 test 目标对象中。这里有个 PRIVATE 关键字,相似的还有 PUBLIC、INTERFACE,那它们分别是什么意思呢?

    • PRIVATE
      私有,指被链接的库仅仅被目标使用,目标不对外暴露这个被链接的库的接口;
    • PUBLIC
      公有,指被链接的库不仅被目标使用,目标还对外暴露这个被链接的库的接口;
    • INTERFACE
      接口,指被链接的库不被目标使用,但目标对外暴露这个被链接的库的接口;

    如果理解起来比较费劲,没有关系,在后续的文章中,我会单独开一篇,详细讲解这些特性。

  2. 7 行,将生成的库链接到 test 中。