CMake构建使用及技巧

网友投稿 1481 2022-05-30

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-目录, 跟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 # 执行匹配特定名称的测试 ctest -R --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

设定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).

CMake构建使用及技巧

链接时: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文件默认安装在/lib/cmake或/share/cmake(具体目录可能跟操作系统相关). 在为标准目录/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).

如果软件本身没有提供cmake文件,但提供了pkg-config文件(*.pc),通常位于/lib/pkgconfig或/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文件,也没有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文件,这种时候需要手动编写。(不限制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)

C++ EI企业智能 EI创新孵化Lab 运筹优化

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:基于koa实现的微信JS-SDK调用Demo
下一篇:为什么浏览器会提示网站“不安全”?一文读懂https协议与SSL证书
相关文章