CMake自动化
测试#
CMake在命令行中使用ctest [<opetions>]
命令执行测试,需要在构建完CMake项目后,在构建树中执行CTest。不过这种方式需要执行很多命令在多个工作目录间进行切换,为了简化流程,可以通过添加选项在构建时直接执行测试。
ctest --build-and-test <path-to-source> <path-to-build>
--build-generator <generator> [<options>...]
[--build-options <opts>...]
[--test-command <command> [<args>...]]
构建-测试模式需要在ctest
命令添加--build-and-test
选项来开启。
在--test-command
选项后可以添加测试用例需要传入的参数。
不过这并不会直接运行测试,除非在--test-command
的后面提供ctest关键字:
ctest --build-and-test project/src-tree build-tree
--build-generator "Unix Makefiles"
--test-command ctest
另外还有其他的选项可以使用,大体可分为三类,分别控制配置、构建过程和测试。
控制配置阶段选项:
--build-options
: 添加CMake配置的选项,应该在--test-command
之前提供,--test-command
总在最后提供。--build-two-config
: 为CMake运行两次配置阶段。--build-nocmake
: 跳过配置阶段。--build-generator-platform
,--build-generator-toolset
: 提供生成器特定的平台和工具集。--build-makeprogram
: 当使用基于make或ninja的生成器时,需要指定make可执行文件。
控制构建阶段选项:
--build-target
: 构建指定的目标。--build-noclean
: 不构建干净目标的情况下进行构建。--build-project
: 提供要构建的项目的名称。
控制测试阶段选项:
--test-timeout
: 限制测试的执行(以秒为单位)。
剩下的就像是创建一个新的项目一样,再创建一个测试的目标,只不过有一些测试专用的指令。
假设我们现在有一个项目,结构是这样的:
project
├─src
│ calc.cpp
│ calc.h
│ CMakeLists.txt
│ main.cpp
│
└─test
calc_test.cpp
unit_test.cpp
其中的主要内容展示如下
/*****************************************/
// project/src/calc.h
class Calc {
public:
int sum(int a, int b);
int multiply(int a, int b);
};
/*****************************************/
// project/src/calc.cpp
int Calc::sum(int a, int b) {
return a + b;
}
int Calc::multiply(int a, int b) {
return a * a; // mistake
}
/*****************************************/
// project/src/main.cpp
int main() {
Calc c;
cout << "2 + 2 = " << c.sum(2, 2) << endl;
cout << "3 * 3 = " << c.multiply(3, 3) << endl;
return 0;
}
/*****************************************/
// project/test/calc_test.cpp
void sumTwoInteger() {
Calc sut;
if(sut.sum(2 , 2) != 4) {
std::exit(1);
}
}
void multiplyTwoIneger() {
Calc sut;
if(sut.multiply(1, 3)) {
std::exit(1);
}
}
/*****************************************/
// project/test/unit_test.cpp
int main(int argc, char** argv) {
if(argc > 2 && argv[1] == string("1")) {
sumTwoInteger();
}
if(argc > 2 && argv[1] == string("2")) {
multiplyTwoIneger();
}
return 0;
}
写了一个计算的类,可以计算两个数的加法和乘法,但我们不小心地将乘法计算写错了,在测试程序中我们将会对这两个功能进行测试。
# project/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
project(Calculate CXX)
add_subdirectory(src)
add_subdirectory(test)
#[[----------------------------------------]]
# project/src/CMakeLists.txt
add_executable(main main.cpp calc.cpp)
#[[----------------------------------------]]
# project/test/CMakeLists.txt
enable_testing()
add_executable(unit-test unit_test.cpp ${CMAKE_SOURCE_DIR}/src/calc.cpp)
target_include_directories(unit-test PRIVATE "${CMAKE_SOURCE_DIR}/src")
add_test(NAME sumTwoInts COMMAND unit-test 1)
add_test(NAME multiplyTwoInts COMMAND unit-test 2)
在build目录下运行ctest unit-test -C Debug
后,CMake会在终端窗口中打印如下信息,表示加法的测试通过了,而乘法的测试失败了,这正好说明我们故意写错的地方没有通过测试。(Windows下似乎需要使用-C来指定编译模式)
Start 1: sumTwoInts
1/2 Test #1: sumTwoInts ....................... Passed 0.01 sec
Start 2: multiplyTwoInts
2/2 Test #2: multiplyTwoInts ..................***Failed 0.01 sec
50% tests passed, 1 tests failed out of 2
Total Test time (real) = 0.02 sec
The following tests FAILED:
2 - multiplyTwoInts (Failed)
安装和打包#
导出#
如果要使用其他项目,当然可以使用find_package()
指令,但这需要将包安装到系统的路径下,而有时候我们在使用自己的项目时并不想这么麻烦。更快速和便捷的方式是包含要引入项目的导出文件。
project(B)
include(/path/project-A/ProjectTarget.cmake)
这样就相当于包括了项目A的目标,并设置了正确的属性。
这个文件并不需要手写,CMake可以使用export()
指令生成这些文件。
export(TARGETS [target1 [target2] [...]]
[NAMESPACE <namespace>] [APPEND] FILE <path>
[EXPORT_LINK_INTERFACE_LIBRARIES])
在TARGET关键字后面写上需要导出的所有目标,然后在FILE关键字后提供目标的文件名,其他参数的含义:
- NAMESPACE关键字作为提示,说明目标已从其他项目导入。
- APPEND关键字告诉CMake,不应该在写入前擦除文件的内容。
- EXPORT_LINK_INTERFACE_LIBRARIES关键字将导出目标链接所需的依赖项(包括导入的和配置特定的变量)。
add_library(Calc STATIC calc.cpp)
target_include_directories(Calc INTERFACE include)
set(EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cmake")
export(TARGETS Calc FILE "${EXPORT_DIR}/CalcTargets.cmake" NAMESPACE calc::)
还是假设有一个目标Calc静态库,我们想用将其导出,先指定生成的cmake目录位置,然后将目标Calc导出到cmake目录下新建的文件,并且指定了命名空间calc::。
export()
还有一个简短版本export(EXPORT <export> [NAMESPACE <namespace>] [FILE <path>])
。但需要使用导出的名称,而不是要导出的目标列表,这个导出名称可以是由install(<targets>)
定义的目标列表。
install(TARGETS Calc EXPORT CalcTargets)
export(EXPORT CalcTargets FILE "${EXPORT_DIR}/CalcTargets.cmake" NAMESPACE calc::)
这样install()
和export()
将共享一个目标列表。
两种方法的执行结果是一样的,都会在build构建树下生成一个cmake目录,其中自动生成了一个CalcTargets.cmake文件,CMake自动在这个文件中生成了包含这个项目所需的配置和目标。
顺便提一下,既然有导出,自然也有导入,其实那个CalcTargets.cmake文件就是自动生成的导入配置文件,通过include()
指令来引入。导入也可以是其他的库或可执行文件等,导入后可以像本地目标一样使用,不过需要添加个IMPORT关键字。
add_library(foo STATIC IMPORTED)
set_property(TARGET foo PROPERTY
IMPORTED_LOCATION "/path/to/libfoo.a")
假设foo是个需要导入的静态库目标,现在我们要引入它,只需要像声明一个静态库一样设置这个目标,然后使用IMPORT关键字表明其是由外部导入的。通过IMPORT_LOCATION属性来设置foo在本地的路径来找到它。
安装#
可以在命令行中使用cmake --install <dir> [<options>]
在系统中安装已构建的项目。
其中,<dir>是生成构建树的必需路径,<options>有以下可用:
--config<cfg>
: 这将为多配置生成器选择生成配置。--component <comp>
: 这将安装限制为指定的组件。--default-directory-permissions <permissions>
: 这将设置已安装目录的默认权限,以<u=rwx,g=rx,o=rx>类似的格式。--prefix <perfix>
: 指定非默认的安装路径,将保存在CMAKE_INSTALL_PREFIX变量中。不指定的话,在类unix系统中默认是/usr/local;在Windows系统中默认是C:/ProgramFiles/${PROJECT_NAME}。-v
,--verbose
: 输出更多冗长的细节。
install()
指令有很多模式:
install(TARGETS)
: 这将安装诸如库或可执行文件等输出构件。install(FILES|PROGRAMS)
: 这将安装各个文件并设置权限。install(DIRECTORY)
: 这将安装整个目录install(SCRIPT|CODE)
:这将在安装期间运行CMake脚本或代码段。install(EXPORT)
: 这将生成并安装一个目标导出文件
当使用了安装指令后,会在编译树中生成cmake_install.cmake文件。
另外,安装指令中又有大量的选项可用,其中一些是共享的,以相同的方式工作:
- DESTINATION: 指定安装路径,相对路径将会以CMAKE_INSTALL_PREFIX作为前缀,而绝对路径不会(并且不支持cpack打包),最好还是使用相对路径。
- PERMISSIONS: 设置文件权限,可用的值包括OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID,SETGID。安装期间创建的目录的默认权限可以通过CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS变量来设置。
- CONFIGURATIONS: 指定了一个配置列表(Debug、Release)。只有当前构建配置在此列表中,命令中跟随这个关键字的选项才会应用。
- OPTIONAL: 这将禁止在安装的文件不存在时引发错误。
下面简单介绍几个安装常用模式:
安装目标#
install(TARGETS <target>... [EXPORT <export-name>]
[<output-artifact-configuration> ...]
[INCLUDES DESTINATION [<dir> ...]]
)
导出关键字在前面介绍过,可以把安装的目标共享给导出文件。
[<output-artifact-configuration> ...]
是提供配置块的列表,单个块的完整语法是这样:
<TYPE> [DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[NAMELINK_COMPONENT <component>]
[OPTIONAL] [EXCLUDE_FROM_ALL]
[NAMELINK_ONLY|NAMELINK_SKIP]
TYPE是必须的,可以识别以下的几种TYPE关键字:
- ARCHIVE: 静态库(.a)和Windows的系统导入库(.lib)。
- LIBRARY: 动态库(.so)。
- RUNTIME: 可执行文件和dll。
- OBJECTS: Object库中的对象文件。
- FRAMEWORK: 设置了FRAMEWORK属性的静态库和动态库(这将排除在ARCHIVE和LIBRARY之外),只适用于macOS。
在3.20版本后的CMake,所有的类型都会被安装,就好像配置了默认的选项一样,如果不想安装某个类型,可以使用<TYPE> EXCLUDE_FROM_ALL
来排除。
DESTINATION关键字指定了安装的路径,而CMake会使用${CMAKE_INSTALL_PREFIX} + ${DESTINANTION}
来设置目标最终的安装路径。如果没有指定路径,CMake为每个类型提供了默认值(需要引入组件include(GNUInstallDirs)
):
类型 | 猜测内置路径 | 安装目录变量 |
---|---|---|
RUNTIME | bin | CMAKE_INSTALL_BINDIR |
LIBRARY, ARCHIVE | lib | CMAKE_INSTALL_LIBDIR |
PRIVATE_HEADER, PUBLIC_HEADER | include | CMAKE_INSTALL_INCLUDEDIR |
用户可以在命令行中使用-DCMAKE_INSTALL_BINDIR=/path
来自定义安装目录。
set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install")
install(TARGETS myexe mylib mylib-shared
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
现在有二进制可执行文件myexe,静态库mylib,动态库mylib-shared,执行cmake --install build
命令后,可执行文件会被安装到build/install/bin下,静态库和动态库会被安装到build/lib下(如果是在linux系统上的话)。
安装文件#
install(<FILES|PROGRAMS> files...
TYPE <type> | DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL]
)
FILES和PROGRAMS的区别在于文件安装时的权限不同,PROGRAMS将会给文件赋予执行权限,而FILES关键字不会。其他的关键字使用与上面的类似。另外每个类型也会有内置的默认路径。
在指令中,可以指定TYPE或者路径两种,二选一即可。类型有以下可用:
文件类型 | 内置猜测路径 | 安装目录变量 |
---|---|---|
BIN | bin | CMAKE_INSTALL_BINDIR |
SBIN | sbin | CMAKE_INSTALL_SBINDIR |
LIB | lib | CMAKE_INSTALL_LIBDIR |
INCLUDE | include | CMAKE_INSTALL_INCLUDEDIR |
SYSCONF | etc | CMAKE_INSTALL_SYSCONFDIR |
SHAREDSTATE | com | CMAKE_INSTALL_SHAREDSTATEDIR |
LOCALSTATE | var | CMAKE_INSTALL_LOCALSTATEDIR |
RUNSTATE | $LOCALSTATE/run | CMAKE_INSTALL_RUNSTATEDIR |
DATA | $DATAROOT | CMAKE_INSTALL_DATADIR |
INFO | $DATAROOT/info | CMAKE_INSTALL_INFODIR |
LOCALE | $DATAROOT/locale | CMAKE_INSTALL_LOCALEDIR |
MAN | $DATAROOT/man | CMAKE_INSTALL_MANDIR |
DOC | $DATAROOT/doc | CMAKE_INSTALL_DOCDIR |
这个模式最常用的是用来安装头文件到指定的地方。比如下面这个指令会把myheader.h安装到include/somewhat目录下,有时我们需要把一些头文件安装到其他一些需要的地方的时候是很有用的。
install(FILES myheader.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/somewhat)
安装整个目录#
install(DIRECTORY dirs...
TYPE <type> | DESTINATION <dir>
[FILE_PERMISSIONS permissions...]
[DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>] [EXCLUDE]
[PERMISSIONS permissions...]] [...]
)
主要用法依旧和上面大差不差。
有个细节需要注意,如果路径不以/
结尾,路径的最后一个目录会被追加到目标目录,比如install(DIRECTORY a DESTINATION /x)
,会创建一个名为/x/a的目录,然后将a的内容复制到其中。
如果使用install(DIRECTORY a/ DESTINATION /x)
,会把a目录的内容复制到/x下。
可重定位目标#
安装指令解决了很多问题,但因为其灵活的特性也增加了复杂性,尤其是可以使用CMAKE_INSTALL_PREFIX指定路径前缀,不仅不同平台下不同,而且还可以被用户通过--prefix
设置。然而,目标导出文件是在安装之前就生成了的,在构建阶段还不知道安装的组件会在哪里。这就导致导出的目标可能无法在其他项目中使用,因为它可能依赖的是源路径,而导出后路径已经改变了。
add_library(calc STATIC calc.cpp)
target_include_directories(calc INTERFACE include)
还是假设有一个calc的静态库,其指定了头文件的路径,CMake会自动补全完整的路径,在include前加上${CMAKE_CURRENT_SOURCE_DIR}
。
不幸的是,这个静态导出后是无法使用的,因为导出后它还是会去${CMAKE_CURRENT_SOURCE_DIR}/include
目录中去找头文件,明显路径已经变了。
CMake提供了两个生成器表达式来解决这个问题:
- $<BUILD_INTERFACE> : 这会在构建阶段生效,而不会包括安装内容。
- $<INSTALL_INTERFACE> : 这会在安装阶段生效,而不包括构建内容。
add_library(calc STATIC calc.cpp)
target_include_directories(calc INTERFACE
"$<BUILD_INTERFACE: ${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE: ${CMAKE_INSTALL_INCLUDEDIR}>"
)
set_target_properties(calc PROPERTIES
PUBLIC_HEADER src/include/calc/calc.h
)
这样修改后,在构建阶段头文件的搜索路径会被展开成源文件中的路径,而在生成阶段会被展开成安装路径下的头文件目录。把对应的头文件设置为目标的公共头文件,并把它安装到对应的头文件路径中,当安装完成被其他库引用时,就能正确地使用头文件了。
安装目标导出文件#
install(EXPORT <export-name> DESTINATION <dir>
[NAMESPACE <namespace>]
[FILE <name>.cmake]
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[EXPORT_LINK_INTERFACE_LIBRARIES]
[COMPONENT <component>]
[EXCLUDE_FROM_ALL]
)
还是用calc举例:
cmake_minimum_required(VERSION 3.20.0)
project(InstallExport CXX)
add_library(Calc STATIC calc.cpp)
set_target_properties(Calc PROPERTIES PUBLIC_HEADER calc.h)
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install)
install(TARGETS Calc EXPORT CalcTargets
ARCHIVE PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
)
install(EXPORT CalcTargets
DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
NAMESPACE Calc::
)
运行install后,会将头文件安装到build/install/include/calc文件目录下,静态库会被安装到build/install/lib文件目录下,导出目标文件会被安装到/build/install/lib/calc/cmake下。
此时的导出文件还不能被find_package()
指令使用,可以被include()
引入到项目中。如果想要让其他项目能够使用包,还需要编写配置文件。
配置文件
我们可以使用cmake --install
命令将项目安装到系统中的任意地方,要使用安装在非默认位置的包,项目需要在配置阶段通过CMAKE_PREFIX_PATH变量提供安装路径,比如cmake -B <build-tree> -DCMAKE_PREFIX_PATH=<install path>
。
find_package()
命令会以特定于平台的方式扫描文档中列出的路径列表,比如
<prefix>/<name>*/(lib/<arch>|lib*|shared)/<name>*/(cmake|CMake)
必须强调,配置文件必须命名为<PackageName>-config.cmake或者<PackageName>Config.cmake才会被找到。那我们将配置文件也安装到导出目标文件对应的目录下。
install(FILES "CalcConfig.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
)
# 配置文件中可以简单地引入导出目标
# CalcConfig.cmake
include("${CMAKE_CURRENT_LIST_DIR}/CalcTargets.cmake")
CMAKE_CURRENT_LIST_DIR将被展开为当前文件的所在路径,因为配置文件和导出目标文件在同一个目录下,这样就可以使用了。
创建一个简单的项目来使用配置文件。
cmake_minimum_required(VERSION 3.20.0)
project(FindCalcPackage CXX)
find(Calc REQUIRED)
不要忘记在构建的时候指定包的路径前缀cmake -S<source-dir> -B<build-tree> -DCMAKE_PREFIX_PATH=<calcconfig_dir>
。
配置文件还有高级用法,还挺复杂,真正用到再去看文档吧。
打包#
软件分发更方便的一种形式是使用二进制包,其中包含了已编译的弓箭和运行时需要用到的其他静态文件,CMake支持使用cpack工具生成多种类型的包。下面列出可用的包生成器:
名称 | 文件类型 | 平台 |
---|---|---|
Archive | 7Z-7zip-(.7z) TBZ2(.tar.bz2) TGZ(.tar.gz) TXZ(.tar.xz) TZ(.tat.Z) TZST(.tar.zst) ZIP(.zip) |
跨平台 |
Bundle | Bundle(.bundle) | macOS |
DEB | DEB(.deb) | Linux |
DragNDrop | DMG(.dmg) | macOS |
External | JSON(.json) | 与外部打包工具集成 |
FreeBSD | PKG(pkg) | *BSD,Linux,OSX |
IFW | Binary | Linux,Windows,macOS |
NSIS | Binary(.exe) | Windows |
NuGet | NuGet(.nupkg) | Windows |
productbuild | PKG(.pkg) | macOS |
RPM | RPM(.rpm) | Linux |
WIX | MSI(.msi) | Windows |
要使用CPack,需要使用必要的install()
指令正确地配置了项目的安装,并构建了项目。cpack将会使用构建树生成的cmake_install.cmake文件,在基于CPackConfig.cmake配置文件来准备二进制包。这个配置文件可以手动创建,也可以使用include(CPack)
将打包模块引入项目,其会在项目的构建树中生成配置,并在需要的地方提供默认值。
cmake_minimum_required(VERSION 3.20.0)
project(CPackPackage VERSION 1.2.3 LANGUAGES CXX)
include(GNUInstallDirs)
add_subdirectory(src bin)
install(...)
install(...)
install(...)
set(CPACK_PACKAGE_VENDOR "Rafal Swidzinski")
set(CPACK_PACKAGE_CONTACT "email@example.com")
set(CPACK_PACKAGE_DESCRIPTION "Simple Calculator")
include(CPack)
另外,CPack模块还会从project()中推断一些值,包括CPACK_PACKAGE_NAME, CPACK_PACKAGE_VERSION, CPACK_PACKAGE_FILE_NAME。最后一个值用于生成输出包,结构是$CPACK_PACKAGE_NAME-$CPACK_PACKAGE_VERSION-$CPACK_SYSTEM_NAME
。
构建项目后,在命令行中使用cpack [<options>]
来生成二进制包。可选项包括:
-G<generators>
: 一个用分号分割的包生成器列表,默认值在CPackConfig.cmake中的CPACK_GENERATOR变量中指定。-C<configs>
: 一个分号分割的构建配置列表(Debug、Release)。-D<var>=<value>
: 使用<value>值覆盖CPackConfig.cmake中设置的<var>。--config<config-file
: 指定配置文件,而不是使用默认的CPackConfig.cmake。--verbose
,-V
: 详细输出。-P<packageName>
: 覆盖包的名称。-R<packageVersion>
: 覆盖包的版本。--vendor<vendorName>
: 覆盖包的供应商。-B<packageDirectory>
: 指定cpack的输出目录。
例如使用cpack -G "ZIP;7Z;DEB" -B packages
,会生成三个包:
- CPackPackage-1.2.3-Linux.7z
- CPackPackage-1.2.3-Linux.deb
- CPackPackage-1.2.3-Linux.zip
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南