在 C\C++ 项目中,通常会有不止一个可执行程序,或者需要将一些调用封装成动态或静态库,来实现项目的模块化,本文就讲解一种支持这种项目布局的方法。

CMake 支持子目录构建(add_subdirectory 指令),本文介绍的布局方法就是基于这个功能来实现。

项目

项目根目录编写一个 CMakeLists.txt,主要描述项目名称,也可以将一些项目级别的配置写入,来影响所有项目构建目标。比如编译的警告级别设置,如果要在项目级别上统一,可以写入,如果要针对不同构建目标来设置,可以留到构建目标 CMakeLists.txt 文件中写入。

项目的各个构建目标作为子目录存放在项目根目录下,每个子目录均包含相应的构建 CMakeLists.txt 文件。使用 add_subdirectory 指令将构建目标子目录添加到项目 CMakeLists.txt 中,构建目标间可以相互引用,只要在添加子目录到项目 CMakeLists.txt 时保持依赖顺序即可。

项目 CMakeLists.txt 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cmake_minimum_required(VERSION 2.6)

# 项目名称
set(PROJECT_NAME "TestApps")
project(${PROJECT_NAME})

# 统一构建目标输出目录
# 将所有构建目标输出目录定位在构建目录下,按类型分类子目录
#     静态库输出目录 => 构建目录/lib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
#     动态库输出目录 => 构建目录/bin
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
#     可执行程序输出目录 => 构建目录/bin
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)

# ---------- 库 ----------
# staticlib 库
add_subdirectory(staticlib)
# 添加头文件搜索路径,这样其他构建目标可以引用其头文件
include_directories(staticlib)

# sharelib 库
add_subdirectory(sharelib)
# 添加头文件搜索路径,这样其他构建目标可以引用其头文件
include_directories(sharelib)

# ---------- 应用程序 ----------
# testapp 程序
add_subdirectory(testapp)

# 配置文件类
add_subdirectory(config)

项目 TestApps 中包含4个构建目标目录,2 个库 staticlibsharelib,依赖库的 1 个应用程序 testapp,以及配置文件类目标 config。4 个目标的输出文件均统一到了构建目录下,按类型分类,这样便于构建结果的归档处理。

通常将构建目标子目录名与目标名一致,这样便于项目管理。

目标

每个构建目标对应在项目根目录下的各个子目录,每个子目录中都有一个 CMakeLists.txt 文件,记录着对应的构建配置。这些构建配置均会继承项目 CMakeLists.txt 文件中的配置。

以下针对示例项目,讲解各个子目录的 CMakeLists.txt ,用以理解如何进行具体的项目布局。

目标 staticlib(静态库编译示例):

1
2
3
4
5
6
7
8
9
# 编译选项
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(SLIB "staticlib")
aux_source_directory("." SLIB_SRC)
add_library(${SLIB} STATIC ${SLIB_SRC})
# 依赖库
target_link_libraries(${SLIB} m)

add_library 指令添加一个库目标,参数 STATIC 表示编译成静态库,该目标链接时需要链接数学库。

目标 sharelib(动态库编译示例):

1
2
3
4
5
6
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(DLIB "sharelib")
aux_source_directory("." DLIB_SRC)
add_library(${DLIB} SHARED ${DLIB_SRC})

与目标 staticlib 差不多,只是add_library 参数 SHARED 表示编译成动态库。

目标 testapp(执行程序编译示例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 编译选项
add_definitions("-std=gnu99")
add_definitions("-Wall")

set(APP "testapp")
aux_source_directory("." APP_SRC)
add_executable(${APP} ${APP_SRC})
target_link_libraries(${APP}
        staticlib
        sharelib)

add_executable 指令添加一个可执行目标,其依赖于静态库 staticlib 和动态库 sharelib

目标 config(配置文件示例):

1
2
3
4
5
6
7
# 拷贝文件到输出目录,便于后续打包安装文件
file(COPY .
    DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
    # 忽略文件匹配模式
    PATTERN "CMakeLists.txt" EXCLUDE
    PATTERN "README.md" EXCLUDE
    )

config 目标是一个特殊目标,其不构建库或可执行程序,而是执行一个文件拷贝工作,在此示例中,将 config 目录下的文件(除 CMakeLists.txtREADME.md)都拷贝到可执行输出目录中。这个用法一般用于拷贝程序的默认配置文件。这样做的好处在于可以将默认配置文件纳入版本管理,也方便打包程序以及测试部署,既能将代码与输出分离,又能实现自动化整合。

结语

CMake 是一个很便利的跨平台构建工具,使用模块化布局方法可以更契合多人共享开发模式,促进项目复用代码,方便上下游库协同管理。如果与 Git 的 submodule 配合,就更能体现出模块化。