# CMake 通常使用cmake自动配置Makefile并进行编译的流程有如下几步。 1. 编写cmake配置文件`CMakeLists.txt` ; 2. 在 `CMakeLists.txt` 文件所在目录创建一个 build 文件夹,然后进入目录; 3. 执行 `cmake ..` 生成 Makefile; 4. 执行 `make` 进行编译。 4. 执行 `make install` 进行安装。 如果是Windows则一般是cmake配合Visual Studio使用,同样下面几步。 1. 编写cmake配置文件`CMakeLists.txt` ; 2. 在 `CMakeLists.txt` 文件所在目录创建一个 build 文件夹,然后进入目录; 3. 执行 `cmake ..` 生成 .vcxproj; 4. 执行 `cmake --build .` 执行编译。 ## 单个源文件 例如有一个源文件`main.cpp` ,是一个幂指数的简单计算。 ```C++ #include #include /** *计算指数幂 */ double power(double base, int exponent) { int result = base; int i; if(exponent == 0) { return 1; } for(i=1; i #include #include "MathFunctions.h" /** *计算指数幂 */ int main(int argc, char *argv[]) { if(argc<3) { printf("Usage: %s base exponent\n", argv[0]); return 1; } double base = atof(argv[1]); int exponent = atoi(argv[2]); double result = power(base, exponent); printf("%g ^ %d = %g\n", base, exponent, result); return 0; } ``` MathFunctions.h ```C++ double power(double base, int exponent); ``` MathFunctions.cpp ```c++ double power(double base, int exponent) { int result = base; int i; if(exponent == 0) { return 1; } for(i=1; i ) ``` 因此,`CMakeLists.txt` 也可以这么写 ```txt #CMake最低版本要求 cmake_minimum_required(VERSION 3.5) #项目信息 project(wang1) #查找当前目录下的所有源文件, 并将名称保存到 DIR_SRCS 变量 aux_source_directory(. DIR_SRCS) #指定生成目标 add_executable(wang1 ${DIR_SRCS}) ``` 接下来操作是一样的。 **不同目录,多个源文件** 目录结构是这样的。 ```bash main.cpp math/ ├── MathFunctions.cpp └── MathFunctions.h ``` 这种情况需要分别在项目根目录和 math 目录下各编写一个 `CMakeLists.txt` 文件。一般为方便通常可以先将 math 目录下的文件编译成静态库,然后再由 main 函数调用。 项目根目录下的 `CMakeLists.txt` ```txt #CMake最低版本要求 cmake_minimum_required(VERSION 3.5) #项目名称 project(wang2) #查找当前目录下的所有源文件, 并将名称保存到 DIR_SRCS 变量 aux_source_directory(. DIR_SRCS) #添加math子目录 add_subdirectory(math) #指定生成目标 add_executable(wang main.cpp) #添加链接库 target_link_libraries(wang MathFunctions) ``` 该文件添加了下面的内容: 第3行,使用命令 `add_subdirectory` 指明本项目包含一个子目录 math,这样 math 目录下的 CMakeLists.txt 文件和源代码也会被处理 。第6行,使用命令 `target_link_libraries` 指明可执行文件 main 需要连接一个名为 MathFunctions 的链接库 。 子目录下的 `CMakeLists.txt` ```txt #查找当前目录下的所有源文件, 并将名称保存到 DIR_LIB_SRCS 变量 aux_source_directory(. DIR_LIB_SRCS) #生成链接库 add_library(MathFunctions ${DIR_LIB_SRCS}) ``` 在该文件中使用命令 `add_library` 将 src 目录中的源文件编译为静态链接库。 ## 自定义编译选项 CMake 允许为项目增加编译选项,从而可以根据用户的环境和需求选择最合适的编译方案。例如,可以将 MathFunctions 库设置为一个可选的库,如果该选项为 `ON` ,就使用该库定义的数据函数来进行运算。 修改顶层 CMakeLists.txt 文件 ```txt #CMake最低版本要求 cmake_minimum_required(VERSION 3.5) #项目信息 project(wang3) #加入一个配置头文件,用于处理CMake对源码的设置 configure_file( "${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}/config.h" ) #是否使用自己的MathFunctions库 option(USE_MYMATH "Use provided math implementation" ON) #是否加入MathFunctions库 if(USE_MYMATH) include_directories("${PROJECT_SOURCE_DIR}/math") add_subdirectory(math) set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions) endif(USE_MYMATH) #查找当前目录下的所有源文件, 并将名称保存到 DIR_SRCS 变量 aux_source_directory(. DIR_SRCS) #指定生成目标 add_executable(wang3 ${DIR_SRCS}) target_link_libraries(wang3 ${EXTRA_LIBS}) ``` 1. 第7行的 `configure_file` 命令用于加入一个配置头文件 config.h ,这个文件由 CMake 从 [config.h.in](http://config.h.in/) 生成,通过这样的机制,将可以通过预定义一些参数和变量来控制代码的生成。 2. 第13行的 `option` 命令添加了一个 `USE_MYMATH` 选项,并且默认值为 `ON` 。 3. 第17行根据 `USE_MYMATH` 变量的值来决定是否使用我们自己编写的 MathFunctions 库。 ## 安装和测试 CMake 可以指定安装规则,也可以添加测试。这两个功能分别可以通过在产生 Makefile 之后使用 `make install ` 和 `make test` 来执行。如果没有CMake工具,就要编写 `install` 和 `test` 来完成,有了CMake就可以大大简化项目的安装和测试过程。 **设置安装规则** ```bash #首先先在 math/CMakeLists.txt 文件里添加下面两行 #指定 MathFunctions 库的安装路径 install(TARGETS MathFunctions DESTINATION bin) install(FILES MathFunctions.h DESTINATION include) #同样的在根目录的 CMakeLists.txt 文件也要同样进行设置 install(TARGETS Demo DESTINATION bin) install(FILES "${PROJECT_BINARY_DIR}/config.h" DESTINATION include) ``` 通过这样的设置,生成的可执行文件和 MathFunctions 函数库 libMathFunctions.o 文件将会被复制到 `/usr/local/bin` 中,而 MathFunctions.h 和生成的 config.h 文件则会被复制到 `/usr/local/include` 中。我们可以验证一下(顺带一提的是,这里的 `/usr/local/` 是默认安装到的根目录,可以通过修改 `CMAKE_INSTALL_PREFIX` 变量的值来指定这些文件应该拷贝到哪个根目录)。 **添加测试规则** 添加测试同样很简单,CMake提供了一个CTest的测试工具,我们要做的只是在项目根目录的 `CMakeLists.txt` 文件中调用一系列的 `add_test` 命令实现添加测试。 ```txt #启用测试 enable_testing() #测试程序是否成功运行 add_test(test_run wang3 5 2) # 测试帮助信息是否可以正常提示 add_test (test_usage Demo) set_tests_properties (test_usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent") # 测试 5 的平方 add_test (test_5_2 Demo 5 2) set_tests_properties (test_5_2 PROPERTIES PASS_REGULAR_EXPRESSION "is 25") # 测试 10 的 5 次方 add_test (test_10_5 Demo 10 5) set_tests_properties (test_10_5 PROPERTIES PASS_REGULAR_EXPRESSION "is 100000") # 测试 2 的 10 次方 add_test (test_2_10 Demo 2 10) set_tests_properties (test_2_10 PROPERTIES PASS_REGULAR_EXPRESSION "is 1024") ``` 上面的代码包含了四个测试。第一个测试 `test_run` 用来测试程序是否成功运行并返回 0 值。剩下的三个测试分别用来测试 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正确的结果。其中 `PASS_REGULAR_EXPRESSION` 用来测试输出是否包含后面跟着的字符串。 测试效果 ```bash [ehome@xman Demo5]$ make test Running tests... Test project /home/ehome/Documents/programming/C/power/Demo5 Start 1: test_run 1/4 Test #1: test_run ......................... Passed 0.00 sec Start 2: test_5_2 2/4 Test #2: test_5_2 ......................... Passed 0.00 sec Start 3: test_10_5 3/4 Test #3: test_10_5 ........................ Passed 0.00 sec Start 4: test_2_10 4/4 Test #4: test_2_10 ........................ Passed 0.00 sec 100% tests passed, 0 tests failed out of 4 Total Test time (real) = 0.01 sec ``` 如果要测试更多的输入数据,像上面那样一个个写测试用例未免太繁琐。这时可以通过编写宏来实现 ```txt # 定义一个宏,用来简化测试工作 macro (do_test arg1 arg2 result) add_test (test_${arg1}_${arg2} Demo ${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") ``` ## 调试 gdb CMake支持gdb的设置,只需要指定 `Debug` 模式下开启 `-g` 选项。 ```txt set(CMAKE_BUILD_TYPE "Debug") set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -00 -Wall -g -ggdb") set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -03 -Wall") ``` ## 添加环境检查 有时候必须在编译之前对系统环境做检查。例如我们在使用平台系统时先检查系统是否自带 pow 函数。如果带有 pow 函数就是用它;否则再次使用我们定义的 power 函数。 **添加 CheckFunctionExists 宏** ```txt #首先在顶层 CMakeLists 文件中添加 CheckFunctionExists.cmake 宏,并调用 check_function_exists 命令测试链接器是否能够在链接阶段找到 pow 函数 #检查系统是否支持 pow 函数 include(${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake) check_function_exists(pow HAVE_POW) ``` **预定义相关宏变量** ```txt #修改 config.h.in 文件,预定义相关的宏变量。 #cmakedefine HAVE_POW ``` **在代码中使用宏和函数** ```c++ #ifdef HAVE_POW printf("Now we use the standard library. \n"); double result = pow(base, exponent); #else printf("Now we use our own Math library. \n"); double result = power(base, exponent); #endif ``` ## 添加版本号 给项目添加和维护版本号是一个好习惯,这样有利于用户了解每个版本的维护情况,并及时了解当前所用的版本是否过时,或是否可能出现不兼容的情况。首先修改顶层 CMakeLists 文件,在 `project` 命令之后加入如下两行。 ```txt set(Demo_VERSION_MAJOR 1) set(Demo_VERSION_MINOR 0) ``` 分别指定当前的项目的主版本号和副版本号。 之后,为了在代码中获取版本信息,我们可以修改 [config.h.in](http://config.h.in/) 文件,添加两个预定义变量 ```txt // the configured options and settings for Tutorial #define Demo_VERSION_MAJOR @Demo_VERSION_MAJOR@ #define Demo_VERSION_MINOR @Demo_VERSION_MINOR@ ``` 这样就可以直接在代码中打印版本信息了。 ## 生成安装包 源码有了,那么如何配置生成各种平台上的安装包呢,这些安装包包括二进制安装包和源码安装包。CMake提供了CPack工具,专门用于打包。 首先在顶层的 CMakeLists.txt 文件尾部添加下面几行 ```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) ``` 上面的代码做了以下几个工作: 1. 导入 InstallRequiredSystemLibraries 模块,以便之后导入 CPack 模块; 2. 设置一些 CPack 相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号; 3. 导入 CPack 模块。 接下来的工作就是像往常一样构建工程,并执行 `cpack` 命令。 ```bash #生成二进制安装包 cpack -C CPackConfig.cmake #生成源码安装包 cpack -C CPackSourceConfig.cmake #运行生成二进制安装包一般会生成三个文件 xxxx-1.0.1-Linux.sh xxxx-1.0.1-Linux.tar.gz xxxx-1.0.1-Linux.tar.Z ```