CMake构建使用及技巧
CMake是C/C++
项目最广泛使用的构建工具,同时也是一门独立的语言,可以编写分支、循环、字符串处理等操作。CMake的历史非常悠久,类似C++11起称为Modern C++,同样的3.X版本的CMake称为Modern CMake。现代版CMake推荐基于Target进行构建,Target之间构成DAG依赖,这种类似面向对象的风格使相关的构建选项更加易于控制。
本文不详细讲解CMake这门构建语言,只写一些笔者在基于CMake进行项目构建实践中的一些积累。希望读者对CMake的基本使用有一定的认识。
推荐学习资料:
- cmake-examples, 这个的样例和知识比较新, 建议只学习这个就够了
- modern cmake, cmake3.x版本被称为
modern
,在部分用法上与旧版有区别
知识点
构建命令
通常情况下, cmake
应该使用out-of-source
编译, 即创建单独的目录, 在目录里面编译, 并生成编译中间文件和编译结果.
注:惯例是在项目根目录下创建
build
目录并在里面编译, 也可使用cmake-build-<buildtype>
目录, 跟Clion
官方相同.
传统cmake
的核心使用一共两个命令(cmake ..
和make
):
cd cmake-build-debug # 切换到构建目录
rm -rf * # 删除上次构建的中间文件和结果, 默认情况下, cmake会使用上次的构建缓存
cmake .. # 生成makefile
make # 构建
ctest # 如果定义有测试, 执行测试,或make test
make install # 如果定义有安装, 执行安装
现代CMake
, 可基于项目源码目录构建:
cmake -S . -B cmake-build-debug # -S 指定源码目录 -B 指定构建目录, 生成make缓存, 替换cmake ..
cmake --build cmake-build-debug --config Debug -- -j8 # 构建项目,替换make -j8
在install
后会在构建目录中生成一个install_manifest.txt
文件, 每一行是安装的内容. 因此, 可以执行xargs rm < install_manifest.txt
来删除所有的安装文件.
注:无法删除目录, 只能删除文件
如果为测试程序添加了工作目录
, 直接跑测试程序可能会有问题, 尽量使用以下方式来跑测试程序.
ctest # 运行所有测试
ctest -N # 列出所有的测试
ctest -R <test-name> # 执行匹配特定名称的测试
ctest -R <test-name> --verbose # 冗余模式, 输出更多信息
构建选项
使用方式为cmake -DCMAKE_BUILD_TYPE=Release ..
定义构建类型
CMAKE_BUILD_TYPE
及对应的编译选项:
Debug
:-g
Release
:-O3 -DNDEBUG
RelWithDebInfo
:-O2 -g -DNDEBUG
MinSizeRel
:-Os -DNDEBUG
定义安装前缀
-DCMAKE_INSTALL_PREFIX=/path/to/install
定义编译器
CMAKE_CXX_COMPILER
CMAKE_C_COMPILER
定义C++标准
设定set(CMAKE_CXX_STANDARD 11)
, 默认在Linux
下为-std=gnu++11
开启编译警告
警告的开启通常建议针对具体的target
进行. 在定义target
的CMakeLists.txt
文件中, 加入target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra)
这一行, 会开启此target
编译时的所有警告.
开启安全编译
推荐基于具体的target
进行:
- 编译时:
target_compile_options(${PROJECT_NAME} PRIVATE -fstack-protector-strong -fpie)
. - 链接时:
target_link_options(Cut2dServer PRIVATE -Wl,-z,relro,-z,now,-z,noexecstack -pie)
注意:不同安全编译选项有自己的适用范围,有的用于静态库、动态库、可执行程序, 有的用于编译时、链接时,不同场景选项可能不同。另外,安全编译选项可以会影响性能,确保知道在做什么。
静态链接标准库
暂时没测试,也不太了解具体。表面看,将标准库静态链接到执行程序,则不再依赖标准库
add_library(static_libstd INTERFACE)
if (STATIC_LINK_LIBSTD AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_link_libraries(static_libstd INTERFACE -static-libgcc -static-libstdc++)
endif ()
编译可重定向静态库
库A依赖库B,将库B编译为静态库,将库A编译为动态库,在链接静态库时,经常会出现这样的错误:relocation R_X86_64_PC32 against symbol xxx can not be used when making a shared object; recompile with -fPIC
,根本原因是在编译静态库时未加入可重定向选项。
在CMAKE
中有多种添加方式, 在本质上就是添加-fpic
的编译选项:
target_compile_options(myLib PRIVATE -fPIC) # 目标级
add_compile_options(-fPIC) # 全局级
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fpic") # 全局级
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic")
set_property(TARGET lib1 PROPERTY POSITION_INDEPENDENT_CODE ON) # 独立于编译器,目标级
set(CMAKE_POSITION_INDEPENDENT_CODE ON) # 独立于编译器,全局级,可命令行指定-DCMAKE_POSITION_INDEPENDENT_CODE=ON
生成库重命名
set_target_properties(MyLibStatic PROPERTIES OUTPUT_NAME MyLib)
适用场景:
cmake
禁止target
重名,因此有时候需要不同target
生成的程序名相同(位于不同目录下)- 希望生成命名相同的动态库和静态库
构建测试配置
通常测试文件在项目根目录下的test
目录, test
目录与src
目录的结构应该是相同的, 每个测试文件与src
目录下的文件应该是一一对应的. 比如, src
目录下有个a/b/c.cpp
文件, 则test
目录下对应的测试文件为a/b/c_test.cpp
.
测试文件的命名: 如果源文件使用HelloWorld.cpp
, 则测试文件为HelloWorldTest.cpp
; 若源文件为hello_world.cpp
, 则测试文件为hello_world_test.cpp
test
目录类同于常规的源码目录, 假设有a_test.cpp
和b_test.cpp
, 每个测试文件都相当于一个有main
函数的文件, 它在编译后会生成独立的执行程序. 则test/CMakeLists.txt
文件内容如下:
function(add_test_executable test_name)
add_executable(${test_name} ${test_name}.cpp)
target_link_libraries(${test_name}
PRIVATE Catch2::Test) # 这里可能需要链接主项目生成的动态库
endfunction()
add_test_executable(A_test)
add_test_executable(B_test)
这里使用了cmake
函数定义, 这个函数的每次调用为每个测试文件生成可执行程序.
项目根目录下的CMakeLists.txt
的内容需要添加如下:
add_subdirectory(test) # 将test视为包含源码的目录, 级联目录中的CMakeLists.txt
enable_testing() # 开启测试
add_test(NAME A_test COMMAND test/A_test) # 添加第1个测试, 测试名为A_test, 执行命令为test目录下编译生成的A_test程序
add_test(NAME B_test COMMAND test/B_test)
add_test的本质, 就只是执行命令而已, 换句话说, 命令其实可以是任何命令, 可以有命令行参数等.
当测试文件比较多时, 这样写可能会比较麻烦, 可以用文件遍历和函数来简化这一过程.
额外工具集成使用
LWYU (link what you use)
以警告的形式,显示编译的每个target
的无用链接库.
注: 在只能整包使用三方库的情况下,但项目只使用了三方库的部分功能,就会导致项目间接依赖了大量的三方库的依赖库。
注: 目前如何解决,不确定
cmake -DCMAKE_LINK_WHAT_YOU_USE=TRUE ..
IWYU (include-what-you-use)
github: https://github.com/include-what-you-use/include-what-you-use
安装: ubuntu: sudo apt install iwyu
, 官网有更新的版本
针对每个文件分析它的#include
情况,会明确给出哪些头文件应该添加,哪些应该删除,它的完整头文件使用情况等。可以根据此来整改。
cmake "-DCMAKE_CXX_INCLUDE_WHAT_YOU_USE=/usr/bin/iwyu;-Xiwyu;--transitive_includes_only" ..
Clang Tidy
基于clang
的linter
工具,支持非常多的检查项,可以配置.clang-tidy
文件,clion
默认支持
安装: sudo apt install clang-tidy-9
Cppcheck
官网: http://cppcheck.sourceforge.net/
安装: sudo apt install cppcheck
,官网有更新的版本
有专用的clion
插件: https://plugins.jetbrains.com/plugin/8143-cppcheck
doxygen文档
find_package(Doxygen REQUIRED dot)
set(DOXYGEN_BUILTIN_STL_SUPPORT YES) # 支持STL
set(DOXYGEN_UML_LOOK YES)
set(DOC_SRCS docs headers sources) # set for input
list(TRANSFORM DOC_SRCS PREPEND "${PROJECT_SOURCE_DIR}/")
doxygen_add_docs(doxygen-docs # 重点: 添加目标doxygen-docs
${DOC_SRCS}
COMMENT "Generate projects pages"
)
三方库引入
因为历史原因,C++
中对依赖库的管理较为混乱,在cmake
中引入三方库也较为混乱。大体上有以下几种方式:
find_package
如果依赖的三方库提供了FetchPKG.cmake
, 或者xx-config.cmake
文件时,则在三方库安装后可通过find_package(PKG REQUIRED)
来引入. 这些cmake
文件默认安装在<prefix>/lib/cmake
或<prefix>/share/cmake
(具体目录可能跟操作系统相关). 在<prefix>
为标准目录/usr/local/
或/usr/
时,cmake
可以默认找到,
以安装json-schema-validator库为例,通过经典的cmake .. && make -j && make install
安装后,会在/usr/local/lib/cmake
下安装cmake
文件。
find_package(nlohmann_json_schema_validator 2.1.0 REQUIRED) # 引入库
但如果安装前缀在非标准目录,则需要在项目中设置CMAKE_PREFIX_PATH
变量。如安装到/opt/json-schema-validator
前缀,则相应的cmake
文件位置为/opt/json-schema-validator/lib/cmake
中。
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH};/opt/json-schema-validator/lib/cmake") # 设置cmake搜索路径, 注意以;分隔
find_package(nlohmann_json_schema_validator 2.1.0 REQUIRED) # 引入库
通过find_package
具体引入的是环境变量名,还是Targets
, 需要看三方库的使用指导文档,或者看前述的.cmake
文件。json-schema-validator
同时引入了变量和target
,这种方式并不标准:
target_include_directories(target-name
PUBLIC ${NLOHMANN_JSON_SCHEMA_VALIDATOR_INCLUDE_DIRS})
target_link_libraries(target-name
PUBLIC nlohmann_json_schema_validator)
通常cmake
在安装后会自带非常多的cmake
文件,为常见的软件基本都提供了,像find_package(Threads REQUIRED)
.
pkg-config
如果软件本身没有提供cmake
文件,但提供了pkg-config
文件(*.pc
),通常位于<prefix>/lib/pkgconfig
或<prefix>/share/pkgconfig
目录中. pkg-config
工具主要用于makefile
编写时自省已安装库的一些信息,如头文件路径、库路径等,cmake
为其提供了通用方案。
以安装Clipper为例:
find_package(PkgConfig) # 确保pkg-config工具已安装到开发机
pkg_search_module(polyclipping REQUIRED IMPORTED_TARGET polyclipping) # 通过pkg来搜索库
如果pkg
文件未安装在标准目录,则需要额外设置环境变量(除环境变量外,有多种方式):
set(ENV{PKG_CONFIG_PATH} "ENV{PKG_CONFIG_PATH}:/opt/oroas/3rds/Clipper/share/pkgconfig") # 在cmake中设置环境变量, 注意以:分隔
手写cmake
如果软件本身没有提供cmake
文件,也没有pkg-config
文件,则可以手写cmake
文件。通常位于项目根目录下的cmake
目录中,下面为常用的模板,但有更高级的写法,即导出为target
而非变量。
以oneTBB库为例,其在2021版本前未提供完整的cmake
使用方案,使用较为麻烦,就自己写了如下:
find_path(TBB_INCLUDE_DIR tbb/tbb.h # 查找头文件路径
HINTS /usr/ # 以下全部为提示目录, 可以为非标准目录
HINTS /usr/include/
HINTS /usr/local/
HINTS /usr/local/include/
)
find_library(TBB_LIBRARY # 查找libtbb.so库
NAMES tbb libtbb libtbb.so
HINTS ${TBB_INCLUDE_DIR}/../lib/
)
find_library(TBB_MALLOC_LIBRARY
NAMES tbbmalloc libtbbmalloc libtbbmalloc.so
HINTS ${TBB_INCLUDE_DIR}/../lib/
)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(TBB DEFAULT_MSG # 检查是否找到,找到则设置TBB_FOUND变量
TBB_INCLUDE_DIR
TBB_LIBRARY
TBB_MALLOC_LIBRARY
)
if (TBB_FOUND) # 聚合找到的多个库
SET(TBB_INCLUDE_DIRS ${TBB_INCLUDE_DIR})
SET(TBB_LIBRARIES "${TBB_LIBRARY};${TBB_MALLOC_LIBRARY}") # 以;分隔
endif ()
以下面方式来引入库:
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # 设置模块路径
find_package(TBB REQUIRED)
以下面方式来使用:
target_include_directories(target-name
PUBLIC ${TBB_INCLUDE_DIRS})
target_link_libraries(target-name
PUBLIC ${TBB_LIBRARIES})
add_subdirectory
对于部分三方库,在官方使用指导文档会提出这种方式,即将整个库放置在项目目录中,再通过add_subdirectory
来使用.
以Catch2为例,如果安装的话可以通过find_package
,如果不安装的话则通过以下引入:
add_subdirectory(opensource/catchorg/Catch2) # 假设在opensource/catchorg目录
这种类型的库大多是header-only
库,对于部署编译型的库(如json-schema-validator), 也可以直接用。但对于编译型库来说,这种方式会在编译项目代码时也编译三方库,会增加编译时间, 引入额外的警告。
以如下方式使用:
target_link_libraries(target-name
PRIVATE Catch2::Catch2)
手写CMakeLists.txt
对于一些极其不标准的库, 可能没有CMakeLists.txt
文件,这种时候需要手动编写。(不限制header-only
还编译型)
以IMQS为例,这种个人型的库就一个头文件,什么也不包含。直接把头文件复制到项目目录中会混淆自有文件和库文件。
方案:把库整个复制到项目根目录的3rds
目录,并为其编写CMakeLists.txt
文件。
add_library(flatbush INTERFACE)
target_include_directories(flatbush
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
以如下方式引入库:
add_subdirectory(3rds/IMQS/flatbush)
以如下方式使用库:
target_link_libraries(target-name
PRIVATE flatbush)
FetchContent
FetchContent为cmake
提供了在"配置时(cmake ..
)"自动下载(git/svn/url
并导入三方库的能力。FetchContent
不是全新的技术,其在本质上是通过add_subdirectory
来引入库,如果库本身不支持这种方式,则无效。
创建FetchPkgs.cmake
文件, 可位于deps
目录:
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2
GIT_TAG v2.13.2
)
FetchContent_MakeAvailable(Catch2) # 方式一:简单,自动, cmake3.14版本前
FetchContent_GetProperties(Catch2) # 方式二:更多控制
string(TOLOWER Catch2 lcName)
if (NOT ${lcName}_POPULATED)
FetchContent_Populate(Catch2)
# 设置更多属性
add_subdirectory(${${lcName}_SOURCE_DIR} ${${lcName}_BINARY_DIR})
endif()
再在主CMakeLists.txt
文件中,添加:
include(FetchContent)
include(deps/FetchCatch2.cmake)
头文件引入
对于header-only
库,只有头文件,不希望侵入式修改加个CMakeLists.txt
文件,则可以直接引入头文件:
同样以IMQS为例,将其放置在项目根目录下的opensource
下:
target_include_directories(target-name
PRIVATE ${CMAKE_SOURCE_DIR}/opensource/IMQS/flatbush)
- 点赞
- 收藏
- 关注作者
评论(0)