[Linux] CMake

CMake可以跨平台的生成makefile,再由make/ninja等编译工具根据makefile的规范进行编译

CMake根据 CMakeLists.txt 文件执行 指令是大小写无关的

这里以Linux下的使用来介绍CMake

简单实例

一个最简单的Cmake项目形如

cmake_minimum_required(VERSION 3.15)
# set the project name
project(Tutorial)
# add the executable
add_executable(Tutorial tutorial.cpp)

cmake_minimum_required 指定使用 CMake 的最低版本号
project 指定项目名称
add_executable 用来生成可执行文件,需要指定生成可执行文件的名称和相关源文件(如果有多个,那么就用空格隔开)。

如果源文件很多,把所有源文件的名字都加进add_executable将是一件烦人的工作。
更省事的方法是使用 aux_source_directory 命令

aux_source_directory(<dir> <variable>)

该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名

构建使用cmake

使用cmake

cmake  <CMakeLists.txt文件存放的目录>
cmake . # CMakeLists.txt文件在当前文件夹底下

编译和链接

cmake --build  <生成的文件希望存放的目录>

变量

set(SRC_LIST a.cpp b.cpp c.cpp)

通过set命令可以指定变量 类似于makefile的变量
使用时也是类似方法 $加上用{}括起的变量名 eg. ${SRC_LIST}

在CMake中,list命令用于操作列表。它可以用于创建、修改和查询列表,以及将列表转换为字符串。

以下是list命令的一些常用用法:

  • list(APPEND <list> <element1> [<element2> ...]):将一个或多个元素添加到列表的末尾。

  • list(INSERT <list> <index> <element1> [<element2> ...]):将一个或多个元素插入到列表的指定位置。

  • list(REMOVE_ITEM <list> <value1> [<value2> ...]):从列表中删除一个或多个指定的元素。

  • list(REMOVE_DUPLICATES <list>):从列表中删除重复的元素。

  • list(GET <list> <index>):获取列表中指定位置的元素。

  • list(LENGTH <list>):获取列表的长度。

  • list(SUBLIST <list> <start> [<length>]):获取列表的子列表。

  • list(JOIN <glue> <list>):将列表中的元素用指定的分隔符连接成一个字符串。

以下代码演示了如何使用list命令创建、修改和查询列表:

# 创建一个空列表
set(my_list)

# 向列表中添加元素
list(APPEND my_list "apple" "banana" "orange")

# 在列表的第二个位置插入一个元素
list(INSERT my_list 1 "pear")

# 从列表中删除一个元素
list(REMOVE_ITEM my_list "banana")

# 获取列表的第三个元素
list(GET my_list 2)

# 获取列表的长度
list(LENGTH my_list)

# 获取列表的子列表
list(SUBLIST my_list 1 2)

# 将列表中的元素用逗号连接成一个字符串
list(JOIN "," my_list)

通过SET指令重新定义 EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH 变量可以指定最终生成的目标二进制的位置(EXECUTABLELIBRARY分别对应于两种最终输出的二进制形式)

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

输出信息

MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message")

向终端输出用户定义的信息,包含三种类型:

  • SEND_ERROR 产生错误,生成过程被跳过。
  • STATUS 输出前缀为--d的信息。
  • FATAL_ERROR 立即终止所有的cmake过程。

添加搜索目录

target_include_directories(${PROJECT_NAME} PUBLIC${PROJECT_BINARY_DIR})

target_include_directories 命令可以将需要添加的头文件目录加入搜索路径

添加子目录

使用命令 add_subdirectory 指明本项目包含一个子目录, 这样该子目录下的CMakeLists.txt 文件和源代码也会被处理。

ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

这个指令用于向当前工程添加存放源文件的子目录, 并可以指定中间二进制和目标二进制存
放的位置。

其中,source_dir是要添加的子目录的路径,binary_dir是要生成二进制文件的路径(默认为source_dir),EXCLUDE_FROM_ALL表示是否将该子目录排除在所有目标之外(默认为不排除),使用时直接写上这个变量就行,eg. add_subdirectory(my_library EXCLUDE_FROM_ALL)

使用add_subdirectory命令向CMake项目中添加一个子目录:

# CMakeLists.txt

# 添加一个子目录
add_subdirectory(my_library)

# 在当前目录中定义一个可执行文件
add_executable(my_app main.cpp)

# 将子目录中的库添加到可执行文件中
target_link_libraries(my_app my_library)

使用链接库

ADD_LIBRARY(libname [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL] source1 source2 ... sourceN)
  • SHARED, 动态库
  • STATIC, 静态库
  • MODULE,在使用dyld的系统有效,如果不支持dyld,则被当做SHARED对待。
  • EXCLUDE_FROM_ALL参数的意思是这个不会被默认构建,除非有其他的组件依赖或者手工构建。

使用命令 add_librarysrc 目录中的源文件编译为静态链接库:

aux_source_directory(. DIR_LIB_SRCS)
add_library (MathFunctions ${DIR_LIB_SRCS})
使用命令 target_link_libraries 指明可执行文件需要连接一个链接库
target_link_libraries(my_test MathFunctions)

配置头文件

configure_file 命令用于加入一个配置头文件

configure_file (
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)

这里配置头文件 config.hCMakeconfig.h.in 生成,通过这样的机制,可以通过预定义一些参数和变量来控制代码的生成。

自定义编译选项

option 命令可以添加一个选项,并且设置默认值

option (USE_MYMATH "Use provided math implementation" ON)

这里添加了一个 USE_MYMATH 选项,并且默认值为ON

接着在config.h.in文件中使用该选项:

#cmakedefine USE_MYMATH

这样 CMake 会自动根据 CMakeLists 配置文件中的设置自动生成 config.h 文件

添加库的使用要求

INTERFACE是指消费者需要、但生产者不需要的那些东西

使用INTERFACE可以指定使用要求 使得任何使用该库的文件自动包含该路径

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

除了 INTERFACE,还有PRIVATEPUBLIC

INTERFACE表示消费者需要生产者不需要
PRIVATE表示消费者不需要生产者需要
PUBLIC 表示消费者和生产者都需要。

如何安装

通过在bash中调用如下指令可以安装cmake目标文件:

cmake -D CMAKE_INSTALL_PREFIX=/usr .

在安装前,需要在CMakeLists中定义好如何安装

INSTALL指令包含了各种类型,我们需要一个个分开解释

目标文件的安装

INSTALL(TARGETS targets ...
[
  [ARCHIVE|LIBRARY|RUNTIME]
  [DESTINATION <dir>]
  [PERMISSIONS permissions ...]
  [CONFIGURATIONS [Debug|Release|...]]
  [COMPONENT <component>]
  [OPTIONAL]
]
[...])

TARGETS后面跟的就是我们通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文
件,可能是可执行二进制、动态库、静态库。

目标类型:ARCHIVE特指静态库,LIBRARY特指动态库,RUNTIME特指可执行目标二
进制。

DESTINATION定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候
CMAKE_INSTALL_PREFIX其实就无效了。

如果你希望使用CMAKE_INSTALL_PREFIX来定义安装路径,就要写成相对路径,不要以/开头
安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/<DESTINATION定义的路径>

INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)

这里将二进制文件myrun安装到${CMAKE_INSTALL_PREFIX}/bin目录
将动态库mylib安装到 ${CMAKE_INSTALL_PREFIX}/lib目录
将静态库mystaticlib安装到 ${CMAKE_INSTALL_PREFIX}/libstatic目录。

为工程添加测试

添加测试同样很简单。CMake 提供了一个称为 CTest 的测试工具。CTestCMake的一个附加模块,它提供了一组工具和命令,用于自动化测试和测试结果的收集和报告。我们要做的只是在项目根目录的 CMakeLists 文件中调用一系列的 add_test 命令。

以下是使用CTest添加测试的一般步骤:

  1. CMakeLists.txt文件中添加enable_testing()命令,以启用CTest

  2. 使用add_test()命令添加测试。该命令需要两个参数:测试名称测试命令测试命令可以是任何可执行文件或脚本,用于运行测试。

  3. 使用add_executable()命令定义测试可执行文件。测试可执行文件应该包含测试代码,并输出测试结果。

  4. 使用target_link_libraries()命令将测试可执行文件链接到需要测试的库或可执行文件。

使用CTest添加测试:

# CMakeLists.txt

# 启用CTest
enable_testing()

# 添加一个测试 测试程序是否成功运行
add_test(test_run my_test 5 2)

# 测试帮助信息是否可以正常提示
add_test(test_usage my_test)
set_tests_properties(test_usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")

# 测试 5 的平方
add_test(test_5_2 my_test 5 2)
set_tests_properties(test_5_2
  PROPERTIES PASS_REGULAR_EXPRESSION "is 25")

# 测试 10 的 5 次方
add_test(test_10_5 my_test 10 5)
set_tests_properties(test_10_5
  PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

# 测试 2 的 10 次方
add_test (test_2_10 my_test 2 10)
set_tests_properties(test_2_10
  PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")

# 定义测试可执行文件
add_executable(my_test my_test.cpp)

# 将测试可执行文件链接到需要测试的库或可执行文件
target_link_libraries(my_test my_library)

test_run 用来测试程序是否成功运行并返回 0 值
使用set_tests_properties()命令设置了测试属性,将PASS_REGULAR_EXPRESSION属性设置为一个正则表达式,用于匹配测试输出中的帮助信息是否包含后面跟着的字符串。

我们可以使用marco命令定义一个宏来简化测试

# 定义一个宏,用来简化测试工作
macro (do_test arg1 arg2 result)
  add_test(test_${arg1}_${arg2} my_test ${arg1} ${arg2})
  set_tests_properties (test_${arg1}_${arg2}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)

# 使用该宏进行一系列的数据测试
do_test(5 2 "is 25")
do_test (10 5 "is 100000")
do_test (2 10 "is 1024")

这段代码定义了一个名为do_test的宏,用于添加测试和设置测试属性。该宏接受三个参数:arg1arg2result,分别表示测试的输入参数和期望输出结果。

在宏的实现中,我们首先使用add_test()命令添加一个名为test_${arg1}_${arg2}的测试,并指定测试命令为my_test,输入参数为${arg1}${arg2}。然后,我们使用set_tests_properties()命令设置测试属性,将PASS_REGULAR_EXPRESSION属性设置为${result},用于匹配测试输出中的期望结果。

使用add_custom_target()命令可以添加自定义测试目标,如代码覆盖率测试、性能测试等。自定义测试目标是一种特殊的目标,它不会生成任何实际的输出文件,而是用于运行测试命令和收集测试结果。

代码覆盖率是一种衡量软件测试质量的指标,用于评估测试是否覆盖了被测试代码的所有执行路径。它通常用百分比表示,表示被测试代码中被测试的部分占总代码量的比例。
常见的代码覆盖率指标包括:

  • 语句覆盖率(Statement Coverage):测试覆盖了被测试代码中的所有语句的比例。

  • 分支覆盖率(Branch Coverage):测试覆盖了被测试代码中的所有分支的比例。

  • 函数覆盖率(Function Coverage):测试覆盖了被测试代码中的所有函数的比例。

  • 条件覆盖率(Condition Coverage):测试覆盖了被测试代码中的所有条件的比例。

  • 路径覆盖率(Path Coverage):测试覆盖了被测试代码中的所有执行路径的比例。

以下是使用add_custom_target()命令添加自定义测试目标的一般步骤:

  1. 使用add_custom_target()命令添加自定义测试目标。该命令需要两个参数:目标名称和测试命令。测试命令可以是任何可执行文件或脚本,用于运行测试。

  2. 使用add_dependencies()命令将自定义测试目标添加到CTest的测试目标中。这样,当我们运行ctest命令时,CTest会自动运行自定义测试目标,并将测试结果收集到CTest的测试报告中。

# CMakeLists.txt

# 添加一个自定义测试目标,用于运行代码覆盖率测试
add_custom_target(coverage
  COMMAND lcov --directory . --capture --output-file coverage.info
  COMMAND genhtml coverage.info --output-directory coverage-report
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  COMMENT "Generating code coverage report"
)

# 将自定义测试目标添加到CTest的测试目标中
add_dependencies(test coverage)

在这个例子中,我们使用add_custom_target()命令添加一个名为coverage的自定义测试目标,并指定测试命令为运行代码覆盖率测试的命令。

这个命令使用lcov工具和genhtml工具生成代码覆盖率报告,并将报告输出到coverage-report目录中。我们还使用WORKING_DIRECTORY选项指定工作目录为CMake的二进制目录,以便正确地生成测试报告。

最后,我们使用add_dependencies()命令将自定义测试目标coverage添加到CTest的测试目标test中,以便CTest能够自动运行代码覆盖率测试,并将测试结果收集到测试报告中。

支持 gdb

让 CMake 支持 gdb 的设置也很容易,只需要指定 Debug 模式下开启 -g 选项:

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")

这段代码设置了CMake的构建类型为Debug,并分别设置了CMAKE_CXX_FLAGS_DEBUGCMAKE_CXX_FLAGS_RELEASE变量,用于指定不同构建类型下的编译选项。

Debug构建类型下,我们设置了以下编译选项:

  • $ENV{CXXFLAGS}:从环境变量CXXFLAGS中获取其他编译选项。

  • -O0:关闭优化,以便在调试时能够更好地查看代码。

  • -Wall:开启所有警告,以便在编译时能够发现潜在的问题。

  • -g:生成调试信息,以便在调试时能够更好地查看代码。

  • -ggdb:生成gdb专属调试信息,以便在使用gdb调试器时能够更好地查看代码。

Release构建类型下,我们设置了以下编译选项:

  • $ENV{CXXFLAGS}:从环境变量CXXFLAGS中获取其他编译选项。

  • -O3:开启最高级别的优化,以便在发布时能够获得最佳的性能。

  • -Wall:开启所有警告,以便在编译时能够发现潜在的问题。

这些编译选项可以帮助我们在不同的构建类型下编译和调试代码,并优化代码的性能和质量。在实际使用中,我们可以根据具体的项目需求和编译环境来调整这些编译选项,并使用其他选项和工具来进一步优化代码的性能和质量。

生成安装包

CPack ,它同样也是由 CMake 提供的一个工具,专门用于打包。首先在顶层的CMakeLists.txt 文件尾部添加下面几行:

#构建一个CPack安装包
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE
  "${CMAKE_CURRENT_SOURCE_DIR}/License. txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Demo_VERSION_MAJOR}" )
set(CPACK_PACKAGE__VERSION_MINOR "${Demo_VERSION_MINOR}")
include(CPack)

这段代码使用了CPack模块来生成软件包,并设置了一些软件包相关的属性。

首先,我们使用include(InstallRequiredSystemLibraries)命令来包含InstallRequiredSystemLibraries模块,以便在安装软件包时自动安装所需的系统库。

然后,我们使用set()命令设置了以下软件包属性:

  • CPACK_RESOURCE_FILE_LICENSE:指定软件包的许可证文件路径。

  • CPACK_PACKAGE_VERSION_MAJOR:指定软件包的主版本号,通常表示重大更新或功能变化。

  • CPACK_PACKAGE_VERSION_MINOR:指定软件包的次版本号,通常表示小的更新或修复。

最后,我们使用include(CPack)命令包含CPack模块,以便生成软件包。CPack模块提供了许多选项和变量,可以帮助我们生成各种类型的软件包,如ZIP、TGZ、RPM、DEB等。我们可以根据具体的需求和环境来调整这些选项和变量,以生成符合要求的软件包。

请注意,这段代码中的变量${Demo_VERSION_MAJOR}${Demo_VERSION_MINOR}是由configure_file()命令生成的,用于指定软件包的版本号。在实际使用中,我们需要根据具体的项目需求和版本管理策略来设置软件包的版本号,并使用适当的选项和工具来生成和发布软件包。

接着在bash下调用cpack即可生成安装包
生成二进制安装包

cpack -C CPackConfig.cmake

生成源码安装包

cpack -C CPackSourceConfig.cmake

-C选项是CPack命令的一个选项,用于指定CPack的配置文件。该选项后面需要跟一个配置文件的路径或名称,以便CPack使用该配置文件来生成软件包。

例如,cpack -C CPackConfig.cmake命令将使用名为CPackConfig.cmake的配置文件来生成软件包。该配置文件通常包含了软件包的属性、组件、文件列表、安装目录等信息,以便根据这些信息来生成符合要求的软件包。

请注意,-C选项是CPack命令的一个必需选项,如果没有指定该选项,CPack将无法生成软件包。

其他一些常用的选项:

  • -G <generator>:指定要使用的生成器,如ZIP、TGZ、RPM、DEB等。可以使用cpack --help命令查看可用的生成器列表。

  • -B <build-dir>:指定生成软件包的临时构建目录。

  • -R <regex>:指定一个正则表达式,用于选择要打包的文件和目录。

  • -D <variable>=<value>:设置CPack变量的值,用于覆盖CMakeLists.txt文件中的默认设置。

  • -V:显示详细的CPack生成过程和输出信息。

  • --config <config-file>:指定CPack的配置文件。

  • --verbose:显示更详细的输出信息。

  • --help:显示CPack的帮助信息。