CMake构建使用及技巧

举报
enali 发表于 2021/06/26 15:26:27 2021/06/26
【摘要】 CMake是`C/C++`项目最广泛使用的构建工具,同时也是一门独立的语言,可以编写分支、循环、字符串处理等操作。CMake的历史非常悠久,类似C++11起称为Modern C++,同样的3.X版本的CMake称为Modern CMake。现代版CMake推荐基于Target进行构建,Target之间构成DAG依赖,这种类似面向对象的风格使相关的构建选项更加易于控制。

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进行. 在定义targetCMakeLists.txt文件中, 加入target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra)这一行, 会开启此target编译时的所有警告.

开启安全编译

推荐基于具体的target进行:

  1. 编译时:target_compile_options(${PROJECT_NAME} PRIVATE -fstack-protector-strong -fpie).
  2. 链接时: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)

适用场景:

  1. cmake禁止target重名,因此有时候需要不同target生成的程序名相同(位于不同目录下)
  2. 希望生成命名相同的动态库和静态库

构建测试配置

通常测试文件在项目根目录下的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.cppb_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

基于clanglinter工具,支持非常多的检查项,可以配置.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

FetchContentcmake提供了在"配置时(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)
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。