CMake 是一个跨平台的自动化建构系统,可以用简单的命令来控制软件编译过程。下面是一个关于如何使用 CMake 进行项目配置和编译的教程。
一、基础配置
1、设置CMake 版本要求
因为 Cmake 版本之间存在差异,在编写 CMakefile 时还需要用 cmake_minimum_required
语句设置一个最低版本要求,一般位于文件第一行。
格式如下:
代码语言:javascript复制cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
VERSION min
:CMake 最小版本<policy_max>
:CMake 最高版本,在 3.12 版本中引入,如果 cmake 是_3.12_之前的版本,会被忽略。FATAL_ERROR
: 该参数在 cmake 的_2.6_及以后的版本被忽略,在 cmake 的_2.4_及以前的版本,需要指明该参数,使得 cmake 能提示失败而不是一个警告。
如果 CMake 运行的版本低于<min>要求的版本,它将停止处理 project 并报告错误。
2、项目版本规定
项目中通常需要版本号,方便后期进行管理,在 CMakeLists.txt
文件中添加以下代码,用来设置项目的版本号并生成 version.h
文件
可以通过 project
命令进行配置:
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
<>
是必填的,[]
是可选的。实例如下:
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX)
CMakeTemplate
:项目名称VERSION 1.0.0
:通过VERSION
指定版本号,版本号格式为major.minor.patch.tweak
。- major(主版本号)
- minor(次版本号)
- patch(补丁版本号)
- tweak
LANGUAGES
:可选,如果未配置,默认使用 C 以及 CXX
并且CMake会将对应的值分别赋值给对应的变量(如果没有设置,则为空字符串)
名称 | 变量名 | 值 |
---|---|---|
major(主版本号) | PROJECT_VERSION_MAJOR | 1 |
minor(次版本号) | PROJECT_VERSION_MINOR | 0 |
patch(补丁版本号) | PROJECT_VERSION_PATCH | 0 |
tweak | PROJECT_VERSION_TWEAK | |
VERSION | CMAKE_PROJECT_NAME | 1.0.0 |
也可以使用一个 version.h 文件指定项目版本。 |
我们可以通过宏定义设置一个 version.h.in
文件:
#define VERSION_MAJOR @CMakeTemplate_VERSION_MAJOR@
#define VERSION_MINOR @CMakeTemplate_VERSION_MINOR@
#define VERSION_PATCH @CMakeTemplate_VERSION_PATCH@
并设置如下的CMake 文件。
代码语言:javascript复制cmake_minimum_required(VERSION 3.12...3.19.1)
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX)
configure_file(
${CMAKE_SOURCE_DIR}/version.h.in
${CMAKE_BINARY_DIR}/version.h
)
在执行 cmake 构建后,会自动生成 version.h
文件,得到 version.h
文件如下:
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define VERSION_PATCH 0
3、配置编译选项
设定编译时语言版本,可以通过设置 CMake 编译器标志来指定项目所使用的编程语言版本,例如:
代码语言:javascript复制set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 99)
声明了C使用 c99
标准,C 使用 c 11
标准。
如此声明是为了项目在不同的机器上编译时使用统一语言版本。
可以设置编译器的选项,例如优化级别、警告选项等,例如:
代码语言:javascript复制add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c 11")
add_compile_options
:添加了一些额外的警告信息选项(-Wall
,-Wextra
,-pedantic
)和将警告视为错误的选项(-Werror
)。CMAKE_C_FLAGS
: 为C代码添加了-pipe
标志,并将C标准设置为C99。CMAKE_CXX_FLAGS
: 为C 代码添加了-pipe
标志,并将C 标准设置为C 11。
这些命令有助于执行编码标准并在编译过程中发现潜在问题。
4、配置编译类型
可以配置 CMake 编译器类型,例如 Debug
、Release
、RelWithDebInfo
、MinSizeRel
等,例如:
set(CMAKE_BUILD_TYPE Debug)
也可不在cmake 文件中指定,而是通过执行cmake 时通过 -B
指令参数指定,例如:
cmake -B build -DCMAKE_BUILD_TYPR=Debug
-B build
:指定构建目录,-B
选项后面跟着的是构建目录的路径,会在当前工作目录下创建(如果不存在的话)并使用这个目录来存放生成的构建系统文件。-DCMAKE_BUILD_TYPE=Debug
:设置了构建类型。-D
选项用于定义变量,这里定义了CMAKE_BUILD_TYPE
变量,其值被设置为Debug
,生成调试版本的构建文件,通常包括额外的调试信息,以便于我们去调试程序。
5、添加全局宏定义
可以添加全局的宏定义,使用 add_definitions
可以增加全局的宏定义,这样在源码中可以判断宏定义实现不同的代码逻辑。
add_definitions(-DENABLE_FEATURE_X -ISDEBUG)
6、添加 include 目录
源代码中包含多个头文件,可以通过 include_directories
添加头文件所在的 include 目录,这个命令会将指定的目录添加到编译器的头文件搜索路径中,使得在编译源代码时,编译器能够找到这些目录下的头文件。
include_directories(src/inc)
从 CMake 3.0 开始,推荐使用 target_include_directories
命令代替 include_directories
。
target_include_directories
允许指定特定目标(可执行文件或库)的头文件搜索路径,这提供了更高的灵活性和更清晰的代码组织。target_include_directories(my_target PRIVATE ${PROJECT_SOURCE_DIR}/include)
my_target
是想要添加头文件搜索路径的目标,PRIVATE
表示这些头文件目录仅用于编译my_target
,而不传递给链接my_target
的其他目标。
target_include_directories
命令这种方式使得构建配置更加模块化和清晰。
二、编译目标文件——示例演示
小鱼以一个cmake 模板示例一个CMake Project的模板仓库来细说。
编写cmake 需要确认编译目标需要的源文件,以及链接需要依赖的库。
- 编译目标:静态库、动态库、可执行文件
这里我们需要做的有以下任务:
- 把 math 路径下编译成静态库;
- 将
main.c
编译成可执行文件,并依赖math 静态库; - 将 test 路径下的测试源文件编译成执行文件,并使用命令进行测试。
1、编译静态库
首先,我们需要将 src/c/math
路径下源文件编译成静态库。先使用 file
或者 set
命令获取源文件路径下的文件列表,再通过 add_library
命令来编译静态库。
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
file
:用于递归地查找与指定模式匹配的文件。add_library
:用于定义一个库目标,这里定义了一个名为math
的库,STATIC
表示静态库,动态库可使用SHARED
。
递归地查找 src/c/math/
目录及其子目录下所有的 .c
文件,并将这些文件的路径存储在 MATH_LIB_SRC
变量中。并使用这些 .c
文件作为源文件,创建一个名为 math
的静态库。
2、编译可执行文件
可以通过 add_executable 命令来编译可执行文件,首先我们先定位源文件,这里使用 add_executable
命令。若存在依赖其他库的情况,可以使用 target_link_libraries
命令。
add_executable(maindemo src/c/main.c)
target_link_libraries(maindemo math)
add_executable
用于定义一个可执行文件目标,这里定义了一个名为maindemo
的可执行文件。target_link_libraries
用于为目标(可执行文件或库)添加链接库。maindemo
是要链接库的目标名称,即第一行定义的可执行文件。这里为maindemo 可执行文件链接了一个math 库。
到此可以执行cmake 构建编译指令
完整的cmake 命令如下:
代码语言:javascript复制cmake_minimum_required(VERSION 3.12)
project(maindemo VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c 11")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
file(GLOB_RECURSE MATH_LIB_SRC
src/c/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
add_executable(maindemo src/c/main.c)
target_link_libraries(maindemo math)
在 CMakeLists.txt
文件下执行cmake 构建和编译指令。
cmake -B cmake-demo
cmake --build cmake-demo
./cmake-demo/maindemo
cmake -B cmake-demo
:用来初始化构建过程并生成构建系统文件,-B cmake-demo
表示构建路径为cmake-demo
,即生成的构建文件在cmake-demo
路径下。cmake --build cmake-demo
:在生成的构建系统文件路径下执行编译项目。或者使用make
指令,make
指令使用的是Makefile 文件。./cmake-demo/maindemo
:执行二进制文件。
三、Cmake安装和打包
1、安装
可以通过 install 命令来安装生成的二进制文件。
代码语言:javascript复制install(TARGETS math demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
通过 TARGETS
参数指定需要安装的目标列表。
RUNTIME DESTINATION
:可执行文件的安装目录;LIBRARY DESTINATION
:库文件的安装目录;ARCHIVE DESTINATION
:归档文件的安装目录。 指定CMAKE_INSTALL_PREFIX
为/usr/local
,那么math
库将会被安装到路径/usr/local/lib/
目录下;而demo
可执行文件则在/usr/local/bin
目录下。
cmake -DCMAKE_INSTALL_PREFIX=/path/to/install
CMAKE_INSTALL_PREFIX
变量说明安装的路径。
2、打包
可以使用 CPack 模块来打包生成的二进制文件,该指令会在构建编译之后使用cpack 命令进行打包安装。也可以使用make 工具的指令 make package
include(CPack)
include 有如下命令:
命令 | 描述 |
---|---|
CPACK_GENERATOR | 打包使用的压缩工具,比如"ZIP" |
CPACK_OUTPUT_FILE_PREFIX | 打包安装的路径前缀 |
CPACK_INSTALL_PREFIX | 打包压缩包的内部目录前缀 |
CPACK_PACKAGE_FILE_NAME | 打包压缩包的名称(<项目名称>-<版本号>-<附加信息>),默认值由CPACK_PACKAGE_NAME、CPACK_PACKAGE_VERSION、CPACK_SYSTEM_NAME三部分构成 |
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_OUTPUT_FILE_PREFIX "/usr/local/package")
set(CPACK_INSTALL_PREFIX "bin/demo")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
若 ${PROJECT_VERSION}=v1.0.0
,则打包文件的路径为 /usr/local/package/CMakeTemplate-1.0.0.zip
,压缩包内的可执行文件位于 /usr/local/package/bin/demo/
下。
四、单元测试
我们在 CMakeLists.txt
中通过命令 enable_testing()
或者 include(CTest)
来启用测试功能。再使用 add_test
命令添加测试用例,指定测试的名称和测试命令、参数。在构建编译完成后使用 ctest
命令行工具运行测试。
可以增加测试控制变量,可以通过 cmake -DCMAKE_TEMPLATE_ENABLE_TEST=ON
指令,在构建编译时开启单元测试。
option(CMAKE_TEMPLATE_ENABLE_TEST "Whether to enable unit tests" ON)
if (CMAKE_TEMPLATE_ENABLE_TEST)
message(STATUS "Unit tests enabled")
enable_testing()
endif()
1、定义单元测试源码
在开发项目时,通常我们会编写一些单元测试代码。这里针对一个CMake Project的模板仓库增加一个单元测试文件。
一般定义单元测试返回值非零时,单元测试未通过。以下是单元测试 test_add.c
文件的源码:
#include <stdio.h>
#include <stdlib.h>
#include "math/add.h"
int main(int argc, char* argv[]) {
if (argc != 4) {
printf("Usage: test_add v1 v2 expectedn");
return 1;
}
int x = atoi(argv[1]);
int y = atoi(argv[2]);
int expected = atoi(argv[3]);
int res = add_int(x, y);
if (res != expected) {
return 1;
} else {
return 0;
}
}
2、cmake增加测试
先生成单元测试可执行脚本,再通过 add_test 命令来添加测试。
代码语言:javascript复制add_executable(test_add test/c/test_add.c)
target_link_libraries(test_add math)
add_test(NAME test_add COMMAND test_add 10 24 34)
add_executable(test_add test/c/test_add.c)
:创建了一个名为test_add
的可执行目标,即一个可执行程序,源代码路径为test/c/test_add.c
。target_link_libraries(test_add math)
:指定test_add
可执行目标需要链接到math
库。add_test(NAME test_add COMMAND test_add 10 24 34)
:定义了一个名为test_add
的测试。COMMAND test_add 10 24 34
指定了测试运行时将要执行的命令和参数,即当运行ctest
命令时,test_add
程序将被执行,传入10
、24
和34
作为命令行参数。
3、执行Cmake测试
可以使用 ctest 命令来执行测试,例如:
代码语言:javascript复制cmake -B cmake-demo
cmake --build cmake-demo
cd cmake-demo && ctest && cd -
cd cmake-demo && ctest && cd -
:执行单元测试cd cmake-demo
:切换当前工作目录到cmake-demo
构建目录;ctest
:在构建目录中运行 CTest,CTest 是 CMake 的测试驱动程序,用于运行项目中的测试。cd -
:切换回原先的工作目录。这个命令是可选的,即在运行单元测试后返回到原来的目录。
参考
一个CMake Project的模板仓库 Cmake中文实战教程