编写 CMakeLists.txt 最常用的功能,就是调用其他 .h 头文件和 .so / .a 库文件,将 .cpp.c.cc 文件编译成可执行文件,或者编译成新的库文件。


CMakeLists.txt 内容

set

# CMake 最低版本要求
cmake_minimum_required(VERSION 3.1)

# 指定 C++11 版本
set(CMAKE_CXX_STANDARD 11)

# 要求编译器必须支持指定的 C++ 标准
# 如果不设置,或者设置为 FALSE,当指定版本不可用时,CMake 可能会回退到上一版本
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)

# 设置构建类型
set(CMAKE_BUILD_TYPE Debug)

# 如果希望生成的可执行文件保留符号表,方便使用 gdb 调试,可以加上这句
# -g 表示保留调试符号,-Wall 表示开启常用警告
add_definitions("-Wall -g")

CMake 中的变量本质上是字符串类型,可以使用 set() 声明或修改变量,也可以使用 unset() 移除变量。

CMAKE_BUILD_TYPE 常见的构建方式有 4 种:

  • Release:发布版本,不适合断点调试,体积较小。编译器通常会启用优化,例如 -O3 -DNDEBUG
  • Debug:调试版本,体积较大。编译器通常会保留调试信息,例如 -g
  • MinSizeRel:最小体积版本。编译器通常会启用体积优化,例如 -Os -DNDEBUG
  • RelWithDebInfo:既优化又保留调试信息。编译器通常会使用类似 -O2 -g -DNDEBUG 的选项。

find_package

# 项目名称和版本
project(v8pkg VERSION 1.0)

# 找到工程需要引用的头文件(.h)和库文件(.so)
find_package(CUDA 10.2 REQUIRED)

# 变量的引用使用 ${变量名}
if(${CUDA_FOUND})
    # 使用 message 函数打印信息
    # 常见消息类型包括 FATAL_ERROR(致命错误)、WARNING(警告)、STATUS(状态信息)
    message(STATUS "CUDA information:")
    message(STATUS "   CUDA_INCLUDE_DIRS: ${CUDA_INCLUDE_DIRS}")
    message(STATUS "   CUDA_LIBRARIES: ${CUDA_LIBRARIES}")
    message(STATUS "   CUDA_LIBRARY_DIRS: ${CUDA_LINK_DIRECTORIES}")
else()
    message(FATAL_ERROR "CUDA 10.2 is not found in the system.")
endif()

找到 CUDA(其他库同理)后,CMake 会生成一些相关变量,例如 CUDA_FOUNDCUDA_INCLUDE_DIRSCUDA_LIBRARIES 等。

# 设定 OpenCV 的路径
set(OpenCV_DIR "/media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/opencv_4_sourcecode/build")

# 找到 OpenCV 库的位置,版本为 4
find_package(OpenCV 4 REQUIRED)

message(STATUS "OpenCV library status:")
message(STATUS "    config: ${OpenCV_DIR}")
message(STATUS "    version: ${OpenCV_VERSION}")
message(STATUS "    libraries: ${OpenCV_LIBS}")
message(STATUS "    include path: ${OpenCV_INCLUDE_DIRS}")

关于 find_package 的接口:

find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])
  • PackageName:必填参数,表示待查找包的名称。它还会影响两种搜索模式下的 .cmake 文件名称。例如模块模式下通常查找Find<PackageName>.cmake,配置模式下通常查找 <PackageName>Config.cmake<lowercasePackageName>-config.cmake,需要注意大小写。
  • version:可选参数。如果指定版本,CMake 会检查找到的包是否与该版本兼容。
  • EXACT:要求待查找包的版本必须与指定版本精确匹配。
  • QUIET:查找失败时尽量不输出提示信息。但是如果同时指定了 REQUIRED,查找失败仍然会停止配置流程。
  • REQUIRED:表示必须找到这个包。找不到时,CMake 会立即报错并停止。
  • MODULE:指定 find_package 只使用模块模式查找。如果不指定该选项,CMake 通常会先尝试模块模式,再尝试配置模式,具体行为也会受 CMake 版本和相关变量影响。
  • COMPONENTS:指定要查找的组件。一个包通常可能包含多个组件。

Module 模式查找顺序

Module 模式会查找名为 Find<PackageName>.cmake 的配置文件。

它会先在 CMAKE_MODULE_PATH 变量对应的路径中查找。如果路径为空,或者路径中没有找到,就会继续在 CMake 安装目录,也就是 CMAKE_ROOT 变量对应目录下的 Modules 目录中查找。这两个变量可以在 CMakeLists.txt 文件中打印查看:

# CMAKE_MODULE_PATH 默认为空,可以使用 set 命令赋值
message(STATUS "CMAKE_MODULE_PATH = ${CMAKE_MODULE_PATH}")

# CMAKE_ROOT 通常类似 /usr/share/cmake-X.XX
message(STATUS "CMAKE_ROOT = ${CMAKE_ROOT}")

安装 CMake 时,CMake 会自带很多 FindXXX.cmake 模块文件,可以通过命令查询:

funnywii@funnywii-3020S:~$ cmake --help-module-list | grep -E ^Find
FindALSA
FindASPELL
FindAVIFile
FindArmadillo
FindBISON
FindBLAS
FindBZip2
FindBacktrace
FindBoost
FindBullet
FindCABLE
FindCUDA
FindCURL

上面只是一部分输出结果。可以看到,CMake 自带了很多 FindXXX.cmake 模块,但并不是所有第三方库都一定有内置的 FindXXX.cmake。例如 OpenCV 更常见的方式,是安装时提供 OpenCVConfig.cmake,然后通过配置模式被 find_package(OpenCV) 找到。

如果 CMake 不能自动找到库,也可以自定义某个库的路径,例如设置 [PackageName]_DIR。上面的 OpenCV 示例就是通过下面这句指定路径:

set(OpenCV_DIR "/media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/opencv_4_sourcecode/build")

add_library

用于将指定源文件打包成静态库、动态库或模块库。第一个参数是生成库的目标名称,第二个参数是库类型,后面的参数是源文件。

如果使用了 IMPORTED,说明导入的是已经编译好的库,不需要再指定源文件和头文件。

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [<source>...])
# Add libcam.so library
# 使用 src 文件夹中的 .cpp 和 .hpp 文件,编译得到名为 liblibcam.so 的库
# 这里目标名写成 libcam,默认生成的库文件名会自动加上 lib 前缀
add_library(libcam SHARED src/cam_block.cpp src/cam_block.hpp)

# 添加 libcam 需要依赖的 OpenCV 库
target_link_libraries(libcam ${OpenCV_LIBS})

# Add liboptical.so library
# 导入文件夹下已经编译好的 liboptical.so 库
add_library(liboptical SHARED IMPORTED)
set_target_properties(liboptical PROPERTIES IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/../liboptical.so)

这里需要注意:CMake 的 target 名称不一定要和最终生成的库文件名完全一致。如果希望生成 libcam.so,更常见的写法是把目标名写成 cam

add_library(cam SHARED src/cam_block.cpp src/cam_block.hpp)

这样在 Linux 下默认生成的库文件名就是 libcam.so

用于把库链接到指定目标上。上面的例子中,生成目标是 libcam,所以这句的含义就是把 OpenCV 相关库链接到 libcam 上:

target_link_libraries(libcam ${OpenCV_LIBS})

现代 CMake 更推荐围绕目标组织工程:先创建目标,再给这个目标添加头文件路径、编译选项和链接库。这样依赖关系更清晰,也更不容易影响其他目标。

set_target_properties

add_library 中指定 IMPORTED 后,需要通过 set_target_propertiesPROPERTIES 设置 IMPORTED_LOCATION,告诉 CMake 这个已编译库文件的具体位置。

set_target_properties(target1 target2 ...
                      PROPERTIES prop1 value1
                      prop2 value2 ...)

关于 CMake 的内置变量,可以看 https://stdrc.cc/post/2021/12/28/cmake-variables/ 这篇文章,写得很清晰。

  • CMAKE_SOURCE_DIR:当前 CMake source tree 的顶层,通常是项目源码根目录。
  • CMAKE_BINARY_DIR:当前 CMake build tree 的顶层,也就是执行配置时指定的构建目录,不一定固定为项目根目录下的 build。
  • CMAKE_CURRENT_SOURCE_DIR:当前正在处理的 source 目录。
  • CMAKE_CURRENT_BINARY_DIR:当前正在处理的 binary 目录。
  • CMAKE_CURRENT_LIST_FILE:当前正在处理的 CMake list 文件,例如 CMakeLists.txt 或 *.cmake。
  • CMAKE_CURRENT_LIST_DIR:当前正在处理的 CMake list 文件所在目录。
  • PROJECT_SOURCE_DIR:当前最近一次 project()命令所在的 source 目录,例如project(v8pkg VERSION 1.0)
  • PROJECT_BINARY_DIR:当前最近一次 project() 命令对应的 binary 目录。如果 CMakeLists.txt 中只有一个 project(),它通常分别等同于 CMAKE_SOURCE_DIRCMAKE_BINARY_DIR;如果后面添加了新的 project(),这两个变量就可能发生变化。

add_executable

用于将源文件和头文件编译成可执行文件。

下面这个例子中,可执行文件目标名称为 test_obj_detect。

add_executable(test_obj_detect vp_detect.cpp
    # src/controlcan.h
    src/yolo_v2_class.hpp
    src/cam_block.hpp
    src/HungarianAlg.h src/HungarianAlg.cpp
    src/kcftracker.hpp src/kcftracker.cpp
    src/ffttools.hpp src/fhog.cpp src/fhog.hpp
    src/labdata.hpp src/recttools.hpp
    src/tracker.h src/tracking_block.hpp src/tracking_block.cpp
    src/display.cpp src/display.h
    src/opticalflow_vp_detect.h src/readpra.cpp src/readpra.h
    src/other.cpp src/other.h
    # src/cansend.cpp src/cansend.h
    src/dis_spe_cal.cpp src/dis_spe_cal.h
    src/measurement_package.h src/ground_truth_package.h
    src/ukf.cpp src/ukf.h
    src/detector.cpp src/detector.h
    src/logging.h
)

add_subdirectory

# 作用:添加子文件夹
# 执行完当前文件夹的 CMakeLists.txt 后,会继续执行 src 子目录下的 CMakeLists.txt
# 用法:add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
# 示例:add_subdirectory(src)

link_librariestarget_link_libraries 都可以用于链接库,但使用方式和影响范围不同。

# link_libraries 通常写在 add_executable 或 add_library 之前
# 它会影响当前目录及其后续创建的目标,作用范围更大
link_libraries(${OpenCV_LIBS})

# target_link_libraries 通常写在目标创建之后
# 它只给指定目标链接库,依赖关系更明确
target_link_libraries(test_obj_detect ${OpenCV_LIBS})

一般更推荐使用 target_link_libraries,因为它能明确说明某个库属于哪个目标,工程变大后更容易维护。

add_dependencies

add_executable(funnywiisubscriber src/funnywiisubscriber.cpp)
target_link_libraries(funnywiisubscriber ${catkin_LIBRARIES})

# add_dependencies 必须在 add_executable 之后
add_dependencies(funnywiitest ${PROJECT_NAME}_gencpp)
add_dependencies(funnywiisubscriber ${PROJECT_NAME}_gencpp)

用于添加目标之间的构建依赖关系。比如在 ROS / catkin 工程中,某些目标需要等消息代码生成完成后再编译,就可以用它显式声明依赖顺序。


参考文章

[1] 轻松搞定 CMake 系列之 find_package 用法详解

[2] IPADS 新人培训第二讲:CMake

[3] add_library — CMake Documentation

[4] CMAKE_XXX_DIR 等 CMake 内置变量辨析

[5] CMake 学习笔记