学习了C/C++,居然不了解Cmake这一利器

2024-05-29 12:05:55 浏览数 (2)

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 命令进行配置:

代码语言:javascript复制
project(<PROJECT-NAME>
        [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
        [DESCRIPTION <project-description-string>]
        [HOMEPAGE_URL <url-string>]
        [LANGUAGES <language-name>...])

<> 是必填的,[] 是可选的。实例如下:

代码语言:javascript复制
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 文件:

代码语言:javascript复制
#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 文件如下:

代码语言:javascript复制
#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 编译器类型,例如 DebugReleaseRelWithDebInfo MinSizeRel 等,例如:

代码语言:javascript复制
set(CMAKE_BUILD_TYPE Debug)

也可不在cmake 文件中指定,而是通过执行cmake 时通过 -B 指令参数指定,例如:

代码语言:javascript复制
cmake -B build -DCMAKE_BUILD_TYPR=Debug
  • -B build:指定构建目录,-B 选项后面跟着的是构建目录的路径,会在当前工作目录下创建(如果不存在的话)并使用这个目录来存放生成的构建系统文件。
  • -DCMAKE_BUILD_TYPE=Debug:设置了构建类型。-D 选项用于定义变量,这里定义了 CMAKE_BUILD_TYPE 变量,其值被设置为 Debug,生成调试版本的构建文件,通常包括额外的调试信息,以便于我们去调试程序。

5、添加全局宏定义

可以添加全局的宏定义,使用 add_definitions 可以增加全局的宏定义,这样在源码中可以判断宏定义实现不同的代码逻辑。

代码语言:javascript复制
add_definitions(-DENABLE_FEATURE_X -ISDEBUG)

6、添加 include 目录

源代码中包含多个头文件,可以通过 include_directories 添加头文件所在的 include 目录,这个命令会将指定的目录添加到编译器的头文件搜索路径中,使得在编译源代码时,编译器能够找到这些目录下的头文件。

代码语言:javascript复制
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 需要确认编译目标需要的源文件,以及链接需要依赖的库。

  • 编译目标:静态库、动态库、可执行文件
Pasted image 20240527104305.pngPasted image 20240527104305.png

这里我们需要做的有以下任务:

  • 把 math 路径下编译成静态库;
  • main.c 编译成可执行文件,并依赖math 静态库;
  • 将 test 路径下的测试源文件编译成执行文件,并使用命令进行测试。

1、编译静态库

首先,我们需要将 src/c/math 路径下源文件编译成静态库。先使用 file 或者 set 命令获取源文件路径下的文件列表,再通过 add_library 命令来编译静态库。

代码语言:javascript复制
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 命令。

代码语言:javascript复制
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 构建和编译指令。

代码语言:javascript复制
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目录下。
代码语言:javascript复制
cmake -DCMAKE_INSTALL_PREFIX=/path/to/install
  • CMAKE_INSTALL_PREFIX 变量说明安装的路径。
2、打包

可以使用 CPack 模块来打包生成的二进制文件,该指令会在构建编译之后使用cpack 命令进行打包安装。也可以使用make 工具的指令 make package

代码语言:javascript复制
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三部分构成

代码语言:javascript复制
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 指令,在构建编译时开启单元测试。

代码语言:javascript复制
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 文件的源码:

代码语言:javascript复制
#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 程序将被执行,传入 102434 作为命令行参数。
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中文实战教程

0 人点赞