简介

cmake自定义模块(Module)

  • 项目一般从CMakelists.txt开始编写,但是随着时间的推移,CMakeLists.txt文件会变得越来越大,这个时候我们就需要将CMakeLists.txt文件拆开分成几个小的模块,这样做的好处是
    • 主文件CMakeList.txt易读性强
    • CMake模块能够在其他项目中重复使用
    • 和CMake的函数相比,模块能够帮助我们限制变量的作用域

cmake 指定C++版本

  • 在 CmakeLists.txt 中增加对 C++ 版本的定义。方法如下:
    • SET(CMAKE_CXX_STANDARD 11)
  • 生成 Makefile 的时候指定,方法如下:
    • cmake -DCMAKE_CXX_STANDARD=11 ..

for循环

  • foreach:
    1
    2
    3
    4
    5
    
      set(VAR a b c)
         
      foreach(f ${VAR})
        message(${f})
      endforeach()
    

cmake 加入调试信息

  • 首先,加入:SET(CMAKE_BUILD_TYPE "Debug")
  • 然后在下面加入:
    • SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
    • SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

execute_process()

  • execute_process 命令将从当前正在执行的CMake进程中派生一个或多个子进程,从而提供了在配置项目时运行任意命令的方法。可以在一次调用 execute_process 时执行多个命令。但请注意,每个命令的输出将通过管道传输到下一个命令中。该命令接受多个参数:
    • COMMAND:子进程命令行。
    • WORKING_DIRECTORY:指定应该在哪个目录中执行命令。
    • TIMEOUT:如果在指定的时间内(以秒为单位计算,允许有小数位)子进程执行仍未完成,则将会被中断。
    • RESULT_VARIABLE:包含进程运行的结果。这要么是一个整数表示执行成功,要么是一个带有错误条件的字符串。
    • RESULTS_VARIABLE:变量将被设置为以分号分隔的列表形式包含所有进程的结果,按给定命令参数的顺序排列。每个条目都是对应子项的整数返回码或描述错误条件的字符串。
    • OUTPUT_VARIABLEERROR_VARIABLE将包含执行命令的标准输出和标准错误。由于命令的输出是通过管道传输的,因此只有最后一个命令的标准输出才会保存到OUTPUT_VARIABLE中。
    • INPUT_FILE 指定标准输入重定向的文件名。
    • OUTPUT_FILE 指定标准输出重定向的文件名。
    • ERROR_FILE 指定标准错误输出重定向的文件名
    • 设置OUTPUT_QUIET和ERROR_QUIET后,CMake将静默地忽略标准输出和标准错误。
    • COMMAND_ECHO <where>:正在运行的命令将被回送到<where>,而<where>将被设置为STDERR、STDOUT或NONE中的一个。
    • 设置OUTPUT_STRIP_TRAILING_WHITESPACE,可以删除运行命令的标准输出中的任何尾随空 格。
    • 设置ERROR_STRIP_TRAILING_WHITESPACE,可以删除运行命令的错误输出中的任何尾随空格。
    • ENCODING <name>:适用于windows平台,编码名称有NONE、AUTO、ANSI、OEM、UTF8或UTF-8。
    • ECHO_OUTPUT_VARIABLE, ECHO_ERROR_VARIABLE:标准输出或标准错误不会被专门重定向到配置的变量。
    • COMMAND_ERROR_IS_FATAL <ANY|LAST>
      • ANY:如果命令列表中的任何命令失败,execute_process()命令将因错误而停止。
      • LAST:如果命令列表中的最后一个命令失败,则execute_process()命令会因错误而停止。列表中前面的命令不会导致致命错误。
    • 如果在同一管道中同时指定了多个OUTPUT_*ERROR_*选项,则优先级顺序是未知的(应避免这种情况)。
    • 如果未指定任何OUTPUT_*ERROR_*选项,则命令CMake所在进程共享输出管道。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
      execute_process(
        COMMAND
          ${PYTHON_EXECUTABLE} "-c" "import ${_module_name}; print(${_module_name}.__version__)"
        OUTPUT_VARIABLE _stdout
        ERROR_VARIABLE _stderr
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_STRIP_TRAILING_WHITESPACE
        )
    

CMake 定义宏

add_definitions()

  • 采用add_definitions定义宏,对于无值宏:
    • add_definitions(-DMG_ENABLE_OPENSSL)
  • 对应于C语言中的
    • #define MG_ENABLE_OPENSSL
  • 而对于有值宏:
    • add_definitions(-DLIBEVENT_VERSION_NUMBER=0x02010800)
  • 对应于C语言:
    • #define LIBEVENT_VERSION_NUMBER 0x02010800

add_compile_definitions()

  • add_compile_definitions定义宏,但是这个指令只要高版本的cmake支持,所以推荐使用第1种,比如:
    • add_compile_definitions(MG_ENABLE_OPENSSL=1)
  • 对应于C语言中的
    • #define MG_ENABLE_OPENSSL 1

小结

  • CMakeLists.txt 文件中定义的宏在后面C/C++程序中是可以直接使用的

CMake 调试

  • 示例:
    1
    2
    3
    4
    5
    
      include(CMakePrintHelpers)
      # 打印变量 PROJECT_SOURCE_DIR
      cmake_print_variables(PROJECT_SOURCE_DIR)
    
      cmake_print_properties(TARGETS arwen-webserver PROPERTIES POSITION_INDEPENDENT_CODE)
    

CMake 常用命令

aux_source_directory()

  • 简介:查找在某个路径下的所有源文件。
    • 搜集所有在指定路径下的源文件的文件名,将输出结果列表储存在指定的变量中。
    • 该命令主要用在那些使用显式模板实例化的工程上。模板实例化文件可以存储在Templates子目录下,然后可以使用这条命令自动收集起来;这样可以避免手工罗列所有的实例。
  • 语法:aux_source_directory(< dir > < variable >)

add_executable()

  • 简介:使用指定的源文件来生成目标可执行文件。这里的目标可执行文件分为三类:普通可执行目标文件、导入可执行目标文件、别名可执行目标文件。分别对应下面的三种命令格式。
  • 语法:
    • add_executable (<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])
    • add_executable (<name> IMPORTED [GLOBAL])
    • add_executable (<name> ALIAS <target>)

CMake 常用操作

生成动态库或静态库

添加动态链接库

  • 如果使用gcc进行单个c文件编译的时候,有时候后面会需要添加如-L -l之类的参数,如下
    • gcc -o test test.c -L/usr/local/lib/ -lopencv
  • 在这里
    • -L是制定动态库的位置,
    • -l则是指定打算连接的动态库名字
  • 那么在进行项目编译的时候就需要通过cmake来通过Cmakelist.txt引导make进行项目编译。
  • 那么就需要在cmakelists.txt文件内添加动态链接库的配置,具体参数如下
    • 首先申明动态库的位置,LINK_DIRECTORIES({your_library_path}),例如:
      • LINK_DIRECTORIES(/usr/local/lib)
    • 然后制定具体的动态库的名称, target_link_libraries(${PROJECT_NAME} -l{library_name}),例如:
      • 指定hiredis的动态链接文件 libhiredis.dylib : target_link_libraries(${PROJECT_NAME} -lhiredis)

引入外部静态库

  • include_directories(${ROOT_DIR}/3party/include)
    • 用于指定头文件的搜索路径
  • link_directories(${ROOT_DIR}/3party/lib)
    • 指定静态库或者动态库的搜索路径
  • target_link_libraries(sample libmath.a)
    • 指定要连接的静态库,必须要有,第二个参数也可以换成math,那么将自动去搜索libmath.a或者libmath.so或者libmath.dyld动态库

arwen 机械臂项目 CMakeLists.txt 笔记

cmake_minimum_required()

  • 为项目设置cmake的最低要求版本
  • 示例:
    • cmake_minimum_required(VERSION 3.10)

project()

  • 为整个项目设置名称、版本并启用语言
  • 示例:
    • project(arwen)

set()

  • 将常规变量、缓存变量或环境变量设置为给定值。
  • 示例:
    • set(CMAKE_CXX_STANDARD 17)

include_directories()

  • 向构建中添加包含目录
  • 示例:
    • include_directories(./include)

${PROJECT_SOURCE_DIR}

  • 内置变量,当前项目的顶级源目录。

${PROJECT_BINARY_DIR}

  • 内置变量,为项目构建目录的完整路径(Full path to build directory for project.)

execute_process()

  • 执行一个或多个子进程
  • 示例:
    • execute_process( COMMAND ${CMAKE_COMMAND} -E make_directory ${LOG_DIR})

${CMAKE_COMMAND}

  • 可执行文件“cmake(1)”的完整路径

add_subdirectory()

  • 向构建中添加子目录。
  • 示例:
    • add_subdirectory(src)

file()

  • 文件操作命令
  • 规则:
    • file(WRITE <filename> <content>...)
    • file(APPEND <filename> <content>...)
  • 详解:
    • <content>写入名为<filename>的文件中。如果这个文件不存在,则创建该文件。如果该文件已经存在,则WRITE模式下将会覆盖该文件,APPEND模式下将会追加到该文件的末尾.由<filename>指定的路径中任何不存在的目录将被创建
    • 如果文件是一个构建输入,使用configure_file()命令仅在其内容更改时更新文件。

add_library()

  • 该指令的主要作用就是将指定的源文件生成链接文件,然后添加到工程中去。该指令常用的语法如下:
    1
    2
    3
    
      add_library(<name> [STATIC | SHARED | MODULE]
                  [EXCLUDE_FROM_ALL]
                  [source1] [source2] [...])
    
  • 参数:
    • 其中<name>表示库文件的名字,该库文件会根据命令里列出的源文件来创建。
    • STATICSHAREDMODULE的作用是指定生成的库文件的类型。
      • STATIC库是目标文件的归档文件,在链接其它目标的时候使用。
      • SHARED库会被动态链接(动态链接库),在运行时会被加载。
      • MODULE库是一种不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数。
      • 默认状态下,库文件将会在于源文件目录树的构建目录树的位置被创建,该命令也会在这里被调用
    • 而语法中的source1 source2分别表示各个源文件。
  • 使用指定的源文件向项目添加库
  • 示例:
    • add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES})
  • 该指令的作用主要是指定要链接的库文件的路径,该指令有时候不一定需要。因为find_package和find_library指令可以得到库文件的绝对路径。该指令的常用语法如下:
    1
    2
    3
    
      link_directories(
          lib
      )
    
  • 不过你自己写的动态库文件放在自己新建的目录下时,可以用该指令指定该目录的路径以便工程能够找到。
  • 指定链接器将在其中查找库的目录
  • 示例:
    • link_directories(${PROJECT_BINARY_DIR}/lib)
  • 该指令的作用为将目标文件与库文件进行链接。该指令的语法如下:
    1
    2
    
      target_link_libraries(<target> [item1] [item2] [...]
                            [[debug|optimized|general] <item>] ...)
    
  • 参数:
    • 指令中的<target>是指通过add_executable()add_library()指令生成已经创建的目标文件。
    • [item]表示库文件没有后缀的名字。默认情况下,库依赖项是传递的。当这个目标链接到另一个目标时,链接到这个目标的库也会出现在另一个目标的连接线上。
    • 这个传递的接口存储在interface_link_libraries的目标属性中,可以通过设置该属性直接重写传递接口。
  • 指定链接给定目标和/或其依赖项时要使用的库或标志
  • 示例:
    • target_link_libraries(${PROJECT_NAME} PUBLIC ${Etherlab_LIBRARIES})

target_include_directories()

  • 向目标添加包含目录
  • 示例:
    • target_include_directories( ${PROJECT_NAME} PUBLIC ${PRJECT_SOURCE_DIR})

find_package()

  • 加载外部项目的设置。
  • 示例:
    • find_package(Etherlab REQUIRED)

set_target_properties()

  • 目标可以具有影响如何构建它们的属性。
  • 示例:
    • set_target_properties(arwen-planning PROPERTIES LINKER_LANGUAGE CXX)

modern cmake >= 3.11

简介

  • CMake可以编译源代码,制作程序库,产生适配器(wrapper),可以用任意的顺序构建执行档
  • CMake支持in-place建构(二进档和源代码在同一个目录树中)和out-of-place构建(二进档在别的目录里),因此可以很容易从一个源代码目录树中建构出多个二进档.
  • CMake也支持静态与动态程式库的建构
  • CMake是一种工具,它读入用于描述软件如何构建的脚本,CMake将评估这些脚本并构建出表达软件项目的内部结构,如果它处理完这些脚本并没有发现错误,CMake将生成用于实际构建项目的构建文件(可能是脚本或者IDE工程文件)
  • CMake支持多种构建工具,包括一系列命令行构建工具和主流IDE,运行cmake –help可以看到在当前平台下CMake所支持的构建工具.
  • CMake中将这些构建系统称作generator.因此,CMake不是构建系统,而是构建系统生成器,具体的构建工作还是要交给Make, Ninja等构建系统去做
  • 运行CMake时,它会运行多项检查,在检查和评估构建脚本期间,它会将一些值缓存到CMakeCache.txt当中,这让我们在增量式开发中,可以跳过长时间的检查过程,但是CMake的缓存也会带来一些问题,比如:修改了某些配置没有办法及时的反映在下次构建中,所以有时构建出问题可以尝试删除CMake缓存文件后重新构建

什么是CMakeList.txt文件?

  • 放置所有CMake命令的文件,当cmake在某个文件夹下运行时,会寻找该文件,如果找不到则报错

为什么要使用cmake?

  • 跨平台
  • 可配置
  • 众多开源项目都只支持(或仅支持)CMake构建

构建CMake项目

CMake构建的两种形式:就地构建(in-source)和源码外构建(out-of-source)

  • 就地构建:在源文件目录下构建,产生的二进制文件将于源文件生成在一个文件夹里
  • 源码外构建:单独创建一个build文件夹,在该文件夹下构建二进制文件,与源代码文件分开

源码树和构建树

  • 源码树是源代码目录,里面放着源代码文件和CMakeLists.txt文件
  • 构建树就是存放构建系统文件和输出文件的目录
  • 通过变量CMAKE_BINARY_DIR访问到构建树的顶层文件夹(执行cmake命令的目录)
  • 通过变量CMAKE_SOURCE_DIR访问到源码树的顶层文件夹(根CMakeLists.txt文件所在的目录).
  • 注:对于就地构建,源码树和构建树是等价的

CMake的执行过程分为三个阶段: 英文:

  1. Configure:
    • Parse top level CMakeLists.txt,
    • Generates cache variables,
    • Only relevant in GUI builds
  2. Generate:
    • Generate native build tool files,
    • Can do cross compilation passing toolchain files
  3. Build:
    • Uses native build tools to compile the sources,
    • All it takes is cmake –build 中文:
  4. 配置阶段:
    • CMake将解析源码树顶层的CMakeLists.txt,并生成一个CMakeCache.txt以存储cache变量
    • 对于CMake-GUI,该阶段由点击Configure出发,
    • 对于命令版本的CMake,该阶段与生成阶段结合在一起
    • CMake打印消息:Configuring done以表示配置阶段结束
  5. 生成阶段:
    • CMake将根据CMakeLists.txtCMakeCache.txt生成构建系统文件
    • 在CMake-GUI中,该阶段由点击Generate出发
    • CMake打印消息Generating done以表示生成阶段结束
  6. 构建阶段:
    • 这一阶段由构建系统负责,在这个阶段中将生成项目的目标

CMake 基础

  • 在经过一番搜索和学习,我开始了解 Modern CMake 的一些用法与理念,它主张放弃传统的基于变量的方法,而采用基于 target 的结构化模式,使其成为一个更可维护、更直观、更易集成、更具意义的方案。

1.1 Modern CMake 介绍

  • 这部分内容是 An Introduction to Modern CMake 的总结,并不会讲的非常详细,希望通过几句话来高度总结各个用法,旨在了解 CMake 有哪能力,如果对某些部分感兴趣请大家自行查阅具体内容。

  • 为什么需要一个好的构建系统?如果你有以下需求,那么使用 CMake 可以从中获益
    • 你想避免硬编码路径
    • 你需要在多台电脑上建立一个软件包
    • 你想使用CI(持续集成)
    • 你需要支持不同的操作系统(甚至可能只是Unix的不同版本)
    • 你想支持多个编译器
    • 你想使用IDE,但也许并不是所有时间都想使用
    • 你想要更好地组织程序的结构
    • 你想使用别人的库
    • 你想使用工具,比如Clang-Tidy,来帮助你编写代码
    • 你想使用调试器
  • 为什么一定是 CMake?其他工具不行吗?
    • 各个 IDE 都支持 CMake,使用 CMake 可以有更大程度的方便, save you life
    • 目前很多 C/C++ 开源项目都在使用 CMake,通过 find CMake 或者 CMake config 很容易将它们集成至你的项目中
    • 使用 CMake 来管理发布,让别人更容易使用你的库
  • 为什么要用现代 CMake?
    • 通常认为 3.1+ 版本是现代 CMake。CMake 是向下兼容的,可以放心使用
    • 至少应该使用你的编译器出来之后的 CMake 版本,因为它需要知道该版本的编译标志等等
    • 高版本 CMake 有更多特性,可以节省数百行和数小时的 CMakeLists.txt 编写工作,长运来看更容易维护

1.2 运行CMake

  • 经典用法
    1
    2
    3
    4
    
      ~/package $ mkdir build
      ~/package $ cd build
      ~/package/build $ cmake ..
      ~/package/build $ make
    
  • 新版本可以简单一点
    1
    2
    
      ~/package $ cmake -S . -B build
      ~/package $ cmake --build build
    
  • 安装命令
    1
    2
    3
    4
    5
    6
    7
    8
    
      #From the build directory (pick one)
      ~/package/build $ make install
      ~/package/build $ cmake --build . --target install
      ~/package/build $ cmake --install . # CMake 3.15+ only
    
      #From the source directory (pick one)
      ~/package $ cmake --build build --target install
      ~/package $ cmake --install build # CMake 3.15+ only
    
  • 推荐使用 --build 用法
    • 通过 -v 显示执行的构建命令: cmake --build build -v
    • 通过 --target 来选择目标:cmake --build --target install
  • 设置 CC 和 CXX 环境变量来选择 C/C++ 编译器
    • ~/package/build $ CC=clang CXX=clang++ cmake ..
  • 选择不同的工具进行构建
    • make 通常是默认的,通过 cmake –help 查看支持的构建器
    • cmake -G"My Tool" 设置构建器,例如 cmake -S . -B buildXcode -G"Xcode"
  • 通过 -D 来设置选项,例如 cmake -S . -B build -DCMAKE_INSTALL_PREFIX=dist

  • --trace 打印 CMake configure 阶段的输出,例如 cmake -S . -B build --trace

  • 不好的 CMake 的用法
    • 不要使用全局函数,例如 link_directoriesinclude_librariesadd_definitions 等,请你忘记它们
    • 不要滥用 PUBLIC,除非有依赖传递,否则请你使用 PRIVATE 替换 PUBLIC
    • 不要使用 GLOB 来添加文件
    • 不要直接链接文件,而是链接目标
    • 链接时千万不要跳过 PUBLIC/PRIVATE,这会导致未来的链接都没有关键字
  • 良好的 CMake 用法
    • 把 CMake 视作代码,保持它的整洁和可读性
    • 围绕 target 构建你的 CMake。将需要的信息打包在 target 里,然后链接那个目标
    • 导出你的接口
    • 写 Config.cmake 文件,这是一个库作者应该做的,可以方便别人使用你的库
    • 使用 ALAS 目标,以保持使用一致性
    • 将常用的功能提取成函数或者宏,通常函数更好
    • 使用小写的函数名,全大写是变量
    • 使用 cmake_policy 或者 range of versions

1.3 基础语法

  • 最低版本
    • cmake_minimum_required(VERSION 3.1) 指定最低版本
    • cmake_minimum_required(VERSION 3.7...3.18) CMake 3.12+ 后,可以指定版本范围
  • Project
    • VERSION 指定版本,并设置一系列变量,例如MyProject_VERSION
    • DESCRIPTION 项目的描述
    • LANGUAGES 支持 C/CXX/Fortran/ASM/CUDA(3.8+)/CSharp(3.8+)/SWIFT(3.15+), C/C++ 为默认值
  • 示例:
    1
    2
    3
    
      project(MyProject VERSION 1.0
                        DESCRIPTION "Very nice project"
                        LANGUAGES CXX)
    
  • 生成可执行文件
    • add_executable(one two.cpp three.h)
  • 生成库
    • 库类型包括 STATICSHAREDMODULEOBJECT 等。如果没有指定库类型,那么 BUILD_SHARED_LIBS 的值将决定编译 STATIC 或者 SHARED
    • 有些库并不需要编译,例如 header only 库。基于 target 的思想,我们可以将它们打包在一种叫 INTERFACE 的库中。
  • 示例
    • add_library(one STATIC two.cpp three.h)
  • 给 target 添加属性
    • target_include_directories 为 target 添加 include 路径
    • PUBLIC 对于可执行文件而言意义不大,对于库来说,它让 CMake 知道链接这个目标的目标也需要这个 include 目录。也就是 “我自己要用,其他链接我的也要用”,具有传递性
    • PRIVATE 表示 “我自己用,其他人不用”
    • INTERFACE 表示 “我自己不用,其他链接我的要用”
    • target 的属性包括 include 文件夹、需要链接的库、编译选项、宏定义等,这些都可以通过 target_link_libraries 进行传递
  • 示例
    1
    2
    3
    4
    5
    
      # PUBLIC 表示外部也需要这个 include 目录
      target_include_directories(one PUBLIC include)
      add_library(another STATIC another.cpp another.h)
      # 由于具有传递性,another 可以连接 one 的 include 目录
      target_link_libraries(another PUBLIC one)
    

1.4 变量与缓存

  • 局部变量
    • set(MY_VARIABLE "value") 设置局部变量,其作用域为当前文件夹,以及 add_subdirectory 进入的文件夹
    • set(MY_VARIABLE "value" PARENT_SCOPE) 将作用域设置为父目录,通常用在子目录向父目录传递信息
    • set(MY_LIST "one" "two") 会在变量值中间加入 “;,等价于 set(MY_LIST "one;two")
  • 缓存变量
    • 通过 set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description") 设置缓存变量
    • 缓存变量将持久存在于 CMakeCache.txt 中
    • 通过 cmake -DXXX 命令传递的参数为缓存变量
  • 环境变量
    • set(ENV{variable_name} value) 设置环境变量
    • $ENV{variable_name} 获取环境变量
  • 属性
    • 属性有点像变量,但它依附在某个 target 或者文件、目录上。许多属性的初始值来自于 CMAKE_ 开头的变量,例如设置 CMAKE_CXX_STANDARD,将会设置 target 的 CXX_STANDARD 属性初始值。
    • set_property 用于设置属性,get_property 用于获取属性

1.5 用CMake编程

  • 控制流
    • 利用 if 语句来控制程序流
    • if 语句中支持的关键字包括:
      • 一元判断:NOT、‘TARGET’、EXISTS、DEFINED
      • 二元判断:STRQUAL,AND,OR,MATCH,VERSION_LESS,VERSION_LESS_EQUAL
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      if(variable)
          # If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
      else()
          # If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-      NOTFOUND`
      endif()
      # If variable does not expand to one of the above, CMake will expand it then try again
    
      if(NOT TARGET libA OR EXISTS "test.xml")
       # If libA or test.xml exist 
      endif()
    
  • 生成器表达式, generator-expressions
    • CMake 大多数命令在 configure 阶段就被执行了,而生成器表达可以在 build 阶段,或者 install 阶段被执行
  • 示例
    1
    2
    3
    4
    5
    
      target_include_directories(MyTarget 
          PUBLIC
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
          $<INSTALL_INTERFACE:include>
      )
    
  • 宏与函数
    • 它们的区别:函数里头的变量作用域为该函数,而宏没有作用域
    • 推荐用函数,因为它不会 “泄露” 变量。即使想要从函数中传递某些变量给外面,可以主动通过 PARENT_SCOPE 来设置
  • 参数
    • 可以通过 cmake_parse_arguments 来解析函数参数

1.6 与你的代码进行交互

  • 从 CMake 中获取信息,并生成代码。configure_file 命令可以实现这个。
  • 读取文件,例如从 version.h 中获取版本信息。file 命令可以实现这个。

1.7 如何组织你的工程

  • 示例 ```
    • project
      • .gitignore
      • README.md
      • LICENCE.md
      • CMakeLists.txt
      • cmake
        • FindSomeLib.cmake
        • something_else.cmake
      • include
        • project
          • lib.hpp
      • src
        • CMakeLists.txt
        • lib.cpp
      • apps
        • CMakeLists.txt
        • app.cpp
      • tests
        • CMakeLists.txt
        • testlib.cpp
      • docs
        • CMakeLists.txt
      • extern
        • googletest
      • scripts
        • helper.py ```
  • 在 configure 阶段运行命令
    • execute_process 在 configure 阶段运行命令
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      find_package(Git QUIET)
    
      if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
          execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
                          WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                          RESULT_VARIABLE GIT_SUBMOD_RESULT)
          if(NOT GIT_SUBMOD_RESULT EQUAL "0")
              message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
          endif()
      endif()
    
  • 在 build 阶段运行命令
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      find_package(PythonInterp REQUIRED)
      add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp"
          COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument
          DEPENDS some_target)
    
      add_custom_target(generate_header ALL
          DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp")
    
      install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include)
    
  • cmake -E <mode> 运行内置的一些命令,例如解压、复制等

添加特性

1.1 C++11及以后

  • CMake 3.8+: Meta compiler features
    • target_compile_features(myTarget PUBLIC cxx_std_11) 开启 c++11 特性,当然你也可以选择 cxx_std_14 和 cxx_std_17
    • set_target_properties(myTarget PROPERTIES CXX_EXTENSIONS OFF),关闭扩展特性
  • CMake 3.1+: 全局和属性设置
    • 全局变量设置
      1
      2
      3
      
        set(CMAKE_CXX_STANDARD 11)
        set(CMAKE_CXX_STANDARD_REQUIRED ON)
        set(CMAKE_CXX_EXTENSIONS OFF)
      
    • 属性设置
      1
      2
      3
      4
      5
      
        set_target_properties(myTarget PROPERTIES
            CXX_STANDARD 11
            CXX_STANDARD_REQUIRED YES
            CXX_EXTENSIONS NO
        )
      

其他

1.1

  • 地址无关代码(Position independent code; PIC),CMake 会自动将 -fPIC 添加到 SHARED 和 MODULE 库中,当然你也可以显示指定
    • 作用于全局变量,set(CMAKE_POSITION_INDEPENDENT_CODE ON)
    • 或者作用于 target,set_target_properties(lib1 PROPERTIES POSITION_INDEPENDENT_CODE ON)
  • 小型库
    • 如果你需要 dl 库,那么这么写最简单 target_link_libraries(libA PRIVATE ${CMAKE_DL_LIBS})
  • 有些库就没有这么方便了,例如 m, pthread 等,但你可以这么写
    1
    2
    3
    4
    
      find_library(MATH_LIBRARY m)
      if(MATH_LIBRARY)
          target_link_libraries(MyTarget PUBLIC ${MATH_LIBRARY})
      endif()
    
  • Interprocedural optimization,运行时优化(link time optimization),即 -flto
    • CMake 3.9+ 支持设置全局变量开启 set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
    • CMake 3.8+ 支持设置属性
      1
      2
      
        set_target_properties(myTarget PROPERTIES 
                            INTERPROCEDURAL_OPTIMIZATION ON)
      
    • 通过 check_ipo_supported 来检查当前版本是否支持 LTO
      1
      2
      3
      4
      5
      6
      
        include(CheckIPOSupported)
        check_ipo_supported(RESULT result)
        if(result)
          set_target_properties(foo PROPERTIES 
                        INTERPROCEDURAL_OPTIMIZATION TRUE)
        endif()
      
  • 在 CMake 中可以配合其他工具
    • CCache,使用它来加快编译速度
    • Clang tidy,对你的代码做静态扫描
    • Include what you use, 检查冗余头文件
  • CMake Modules 非常有用,简单介绍一些常用的
    • CMakeDependentOption,根据一组变量来设置选项,例如
      1
      2
      
        include(CMakeDependentOption)
        cmake_dependent_option(BUILD_TESTS "Build your tests" ON "VAL1;VAL2" OFF)
      
    • CMakePrintHelpers,用于打印属性或变量
    • CheckCXXCompilerFlag,用于判断是否支持某个编译选项
    • WriteCompilerDetectionHeader,与 CheckCXXCompilerFlag 类似,但它更强大。它可以找到编译器支持的特性列表,并生成 C++ 头文件,让你知道哪些特性可用
    • try_compile/try_run,可以让尝试编译(运行)一些代码,这可以让你获得系统能力的信息
  • 调试CMake代码
    • 打印永远是调试的最简单的办法
      1
      2
      3
      4
      5
      6
      7
      8
      
        message(STATUS "MY_VARIABLE=${MY_VARIABLE}")
      
        include(CMakePrintHelpers)
        cmake_print_variables(MY_VARIABLE)
      
        cmake_print_properties(
          TARGETS my_target
          PROPERTIES POSITION_INDEPENDENT_CODE)
      
    • –trace-source=”filename” 让你观察 CMake 文件到底发生了什么,例如
      • cmake -S . -B build --trace-source=CMakeLists.txt
  • 引入其他项目
    • Submodule
      • 通过 git submodule add ../../owner/repo.git extern/repoextern 添加为子仓库
      • 可以在 CMake 中内置子仓库更新命令
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        
          find_package(Git QUIET)
          if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
          # Update submodules as needed
              option(GIT_SUBMODULE "Check submodules during build" ON)
              if(GIT_SUBMODULE)
                  message(STATUS "Submodule update")
                  execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
                                  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                                  RESULT_VARIABLE GIT_SUBMOD_RESULT)
                  if(NOT GIT_SUBMOD_RESULT EQUAL "0")
                      message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
                  endif()
              endif()
          endif()
        
          if(NOT EXISTS "${PROJECT_SOURCE_DIR}/extern/repo/CMakeLists.txt")
              message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.")
          endif()
        
  • DownloadProject
    • 在 CMake 3.11 以前,主要下载命令都是在 build 阶段完成的(例如 ExternalProject_Add)。这就导致你无法使用 add_subdirectory。
    • CMake 3.11之前,如果想要在 configure 阶段下载,参考 Crascit/DownloadProject
  • Fetch(CMake 3.11+)
    • CMake 3.11 后,你可以使用 FetchContent 在 configure 阶段下载文件或者项目

测试

1.1

  • 通常不在子目录时编译测试代码
  • 示例
    1
    2
    3
    
      if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
        add_subdirectory(tests)
      endif()
    
  • 通过 add_test(NAME TestName COMMAND TargetName) 注册测试

  • GoogleTest
    • 推荐通过以 git submodule 的形式引入 GoogleTest
    • 可以通过作者提供的 AddGoogleTest 工具引入 GoogleTest
    • 通过 FetchContent 引入 GoogleTest
  • Catch
    • Catch 是个 header only 库,如果直接将它的 include 引入你的仓库中,你需要做一些准备,以便使用
      1
      2
      3
      4
      
        # Prepare "Catch" library for other executables
        set(CATCH_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/catch)
        add_library(Catch2::Catch IMPORTED INTERFACE)
        set_property(Catch2::Catch PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${CATCH_INCLUDE_DIR}")
      
    • 通过 ExternalProject、FetchContent 或者 git submodule 引入 Catch,那么直接 add_subdirectory 即可

Exporting and Installing

1.1

  • 关于 Installing,参考 CMake之install方法的使用
  • 关于 Exporting,可以将 build directories 信息导出,以供其他工程使用(注意与 Installing 的区别:Installing 导出的是安装目录的信息)
  • 关于 Package ,没怎么用过,似乎是将你需要的内容进行打包

寻找库

1.1

  • 介绍了引入 CUDA、OpenMP、Boost、MPI、ROOT、Minuit2 等库的标准姿势