【CMake】大型项目中CMake的使用技巧
目录
1 大型项目的一般架构
在实际程序开发过程中,会经常使用到第三方库,而这些第三方库通常是一些大型的项目,例如计算机视觉库OpenCV,视觉伺服平台visp等。这些库为了满足多人同时开发的功能,在代码逻辑架构、文件架构上都有比较深的讲究。
1.1 文件架构
一般来说,源代码的文件夹会被命名为src
,若是 C/C++ 的大型项目,还会存在以 include
为头文件的文件夹。除此之外,一些测试文件均被放在 test
文件夹下,并且在这些均被放在同级目录之下,在该目录下还可能存在 CMakeLists.txt
。在项目最高级目录下还会存在 samples
文件夹,很多测试 demo 被放入其中,最后还会给用户提供一个 README.md
描述文件,就拿 OpenCV 的部分文件来举例子:
.
├── cmake
│ └── ...
├── CMakeLists.txt
├── include
│ ├── CMakeLists.txt
│ └── ...
├── modules
│ └── core
│ ├── CMakeLists.txt
│ ├── include
│ │ └── ...
│ ├── src
│ │ └── ...
│ └── test
│ └── ...
├── README.md
└── samples
1.2 CMakeLists 在其中的作用
很显然,每个模块的文件夹下都会存在一个CMakeLists.txt
。实际上,最底层的CMakeLists.txt
是为了管理该模块功能的编译、测试文件编译,以及提供指定的接口(可以是库,也可以是若干 CMake 变量)给外部。上一级的CMakeLists.txt
则可以根据需求,选择需要编译的一个或若干功能模块,并且在某些联合编译的情况下链接需要的库。最高一级CMakeLists.txt
一般是对于整个库编译选项的设置,以及对系统、平台、编译器等编译环境信息的兼容处理。
2 CMake 变更项目信息技巧
2.1 按需求配置编译选项
2.1.1 使用介绍
往往一个项目会涉及到几个彼此独立的模块,一般情况下,可手动配置所需要的部分,在实现根据选项配置内容时需要用到 option()
,例如在CMakeLists.txt
中写入以下内容:
option(A "a" OFF)
if (A)
# ...
endif ()
此时,我们为 CMake 提供了一个选项 A,并且默认情况下为关闭状态(OFF)。若在命令行中输入以下内容(假设CMakeLists.txt
在当前目录的上一级):
cmake -D A=ON ..
则可以在 cmake
过程中将 A 选项开启,则可以使 if
语句块中的内容参与编译内容的制作。
2.1.2 案例
现设计一个识别模块 detector ,具有一个抽象的识别器头文件 detector.h
,以及两个具体的识别器派生类 A_detector、B_detector,其代码结构如下:
若用户想在该模块上添加一个测试文件 test.cpp
,并且仅用到了 A_detector 识别器,因此需要在编译时选择编译 A_detector 的功能,而不编译 A_detector 的功能。
为此我们需要在该模块的 CMakeLists.txt
中写入:
# ...
if (A)
aux_source_directory(src/A_detector A_DIRS)
# 此处用 SHARED 也可换成 STATIC,代码仅做展示
add_library(A_detector SHARED ${A_DIRS})
target_include_directories(A_detector PUBLIC include/A_detector)
target_link_libraries(A_detector xxx)
elseif (B)
aux_source_directory(src/B_detector B_DIRS)
add_library(B_detector SHARED ${B_DIRS})
target_include_directories(B_detector PUBLIC include/B_detector)
target_link_libraries(B_detector xxx)
endif ()
# ...
并且在命令行中输入以下内容,即可实现仅对 A_detetcor 的使用:
mkdir build
cd build
cmake -D A=ON ..
2.2 按需求配置宏与常量
若需要在代码中根据编译选项来实时添加某些宏定义或者变量,可使用 configure_file
或 add_definitions
2.2.1 configure_file
基本用法:
configure_file(xxx.h.in xxx.h)
例如先构建一个项目,其文件结构如下:
在使用 configure_file
之前需要创建一个 xxx.h.in
文件,用于生成 xxx.h
,其中 xxx.h.in
文件的编写与普通C++文件极为相似,例如:
#cmakedefine CONFIG_DIR "@CONFIG_DIR@"
#define CONFIG_DIR_1 "@CONFIG_DIR@"
constexpr auto CONFIG_DIR_2 = "@CONFIG_DIR@";
#cmakedefine CONFIG_DIR_3 "@CONFIG_DIR_2@"
#define CONFIG_DIR_4 "@CONFIG_DIR_3@"
在 CMakeLists.txt
中写入:
set(CONFIG_DIR /usr/local/include/opencv4)
configure_file(
config.h.in
${CMAKE_SOURCE_DIR}/config.h
)
在经过 cmake 后,会在 CMakeLists.txt
当前路径下生成 config.h
文件,其内容如下
#define CONFIG_DIR "/usr/local/include/opencv4"
#define CONFIG_DIR_1 "/usr/local/include/opencv4"
constexpr auto CONFIG_DIR_2 = "/usr/local/include/opencv4";
/* #undef CONFIG_DIR_3 */
#define CONFIG_DIR_4 ""
可以看到,第 4 行和第 5 行的区别就在于使用的是 #cmakedefine
还是 #define
除此之外,还可以通过 option()
选项来制作条件编译的宏,例如:在 config.h.in
中写入:
#cmakedefine A
#cmakedefine B
在 CMakeLists.txt
中写入:
option(A "a" ON)
option(B "b" OFF)
configure_file(
config.h.in
${CMAKE_SOURCE_DIR}/config.h
)
生成的 config.h
文件内容如下:
#define A
/* #undef B */
2.2.2 add_definitions
有时候单纯是为了得到条件编译的标签,例如 #define A
这种,使用 configure_file
需要单独维护一个 .in 文件,因此不够方便。而使用 add_definitions
可以很好的解决这一点问题。还是本文【2.2.1】中的示例文件架构,但此时不再需要任何的 .in 文件,仅需在 cmake 时使用 add_definitions
即可。例如,在 CMakeLists.txt
中写入:
add_definitions(-DA)
在 main.cpp 中写入
#include <iostream>
using namespace std;
#ifdef A
inline void foo() { cout << "this is A" <<endl; }
#endif // !A
#ifdef B
inline void foo() { cout << "this is B" <<endl; }
#endif // !A
int main(int argc, char *argv[])
{
foo();
return 0;
}
经过cmake、编译后,运行结果如下
this is A
注意事项:
在使用add_definitions
语句时,其生成的对象(例如上述例子中的A)具有作用域的属性。即子模块(由add_subdirectory
语句生成)中使用add_definitions
生成的对象无法在父模块中使用,也无法跨模块使用(子模块除外)。
2.3 跨文件链接
在大型项目中,多人合作开发是不可避免的,假如此时两个人设计了彼此独立的两个库A和B,文件结构如下:
A 库只有若干头文件,B 库有若干头文件、cpp源文件,因此二者在 CMakeLists.txt
的书写上略有差别,首先看 B 库,参考cmake代码如下:
aux_source_directory(src B_DIRS)
add_library(B SHARED ${B_DIRS})
target_include_directories(B PUBLIC include)
可以利用源文件直接构建一个库B,并且将这些头文件绑定在目标(B库)上,因此,本项目在使用时只需链接该库B即可。
- 不过某些大型项目在编译安装时会将这些头文件(.h)与库文件(Linux下通常是.so,Windows下通常是.dll)彼此分开,并且安装到本地的默认路径下(Linux下一般是/usr/local/xxx,Windows下一般是C:\Program Files)为此需要提供一个能直接获取头文件以及库文件的参数集合,例如计算机视觉库 OpenCV 提供的参数集合是
S{OpenCV_INCLUDE_DIRS}
和${OpenCV_LIBS}
。
话说回来,如果有的库仅在本项目中使用,那如何构建出只有头文件的参数集合供本项目的其他库使用呢?本案例中的 A 库可以回答这一点。
本案例中由 main.cpp
生成的可执行程序会用到 A 库的内容,但 CMake 语法生成库必须要有源文件(.cpp),因此不能通过 add_library()
语法来生成能被该可执行文件链接的库。不过,可以提供一个长期存在的参数集保存该头文件,并且在生成可执行文件之后包含该参数集即可。A 库中 CMakeLists.txt
参考代码如下:
set(A_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/include
CACHE PATH "A")
其中,CACHE PATH "A"
一句是将集合 A_INCLUDE_DIRS
存储到文件 CMakeCache.txt
中,因此不加这部分内容的话 A_INCLUDE_DIRS
只能被本 CMakeLists.txt
访问,因此在 cmake 中也存在局部变量的说法。