CMake工具使用手册

  之前自己的小项目都是直接手动人肉Makefile,因为项目的文件不是很多,发现以前写的那个Makefile模板增增改改还是挺够用的。但现在发现CMake真是越来越流行了,不知道是不是借着QT/KDE等项目的东风,越来越多的C++开源项目开始使用CMake工具来用作管理项目了。此外,著名IDE Clion和CMake也集成的很好,只不过我现在已经不用IDE进行调试了,因为命令行的GDB更方便。
cmake
  这次在一个项目中使用了CMake,简单直白的几行命令,就可以快速生成Makefile,相比Makefile晦涩的语法和隐暗的规则,可以说是懒人(对我来说主要是弱鸡)用的Makefile。除了辅助生成编译用的Makefile,CMake还集成安装、测试、打包等功能,同时跟Boost库的单元测试框架也很好的集成,Bingo!
  注意:CMake工具的实现主要是由一系列的命令构成,同时还有大量的预定义变量,正是他们的存在使得用户可以用极少的语句生成功能丰富的Makefile文件。现在CMake官方最新发布版本是v3.8,但是在CentOS-7系列的机器上最新的CMake版本仍然是v2.8分支,由于代码的部署环境所限,本篇文档还是针对CMake v2.8.12老版本操作实验的结果,有时候同最新CMake可能会有所差异,建议查看查看老版本的文档。
  CMake中有很多涉及到文件、目录的相关命令或者变量,在作为参数的时候有时可以使用相对路径,有时可以使用绝对路径;在使用相对路径的时候,有时候是相对于项目的源代码路径,有时候是相对于编译路径,还有些时候相对路径参考于某些预制变量,这些东西如果被搞糊涂了还是建议查看相关文档。
  本文档不求最全,够用即可!

一、起步

  凡是手动安装过CMake管理的软件的同学,都知道CMake的用法。通常会在项目路径下面建立一个build目录,然后进入build目录使用➜ build git:(master) ✗ cmake ../ 命令就可以生成Makefile文件,接下来的操作想必大家就都知道了。额外创建build路径进行项目外编译,就是可以保护你的源代码路径整洁干净。
  所以开发者就是要编写这个CMakeLists.txt文件,方便自己的同时也方便了用户。
  CMakeLists.txt文件使用#打头的是注释,项目根目录CMakeLists.txt所必须行如下,前者规定了可以使用该文件的最低版本CMake版本,后者指明了项目名称。

1
2
cmake_minimum_required (VERSION 2.8.11)
project (aimlsrvd)

  如果你的项目是比较简单的结构,所有源代码都放在source/目录下面,而根目录包含一个main.cpp入口函数,则只需要再添加如下两行,就可以编译产生可执行文件了。

1
2
aux_source_directory( source/ DIR_SRCS )
add_executable( aimlsrvd main.cpp ${DIR_SRCS} )

  aux_source_directory 命令的作用就是将指定目录下的文件名收集起来并保存到指定变量中。这个命令原本不是专门为上面这种情况使用的,因为CMake只会检查CMakeLists.txt文件的时间戳来决定是否执行cmake更新编译环境,那么当你在指定目录下添加了新的源代码文件的时候而没有更新过CMakeLists.txt,那么CMake可能就不能感知到该目录下源文件增删的变化,此时你必须手动执行cmake命令进行刷新。
  add_executable 命令用于指定生成可执行文件,该名字必须在整个项目中相对其他可执行文件必须是全局唯一的。不仅仅是可执行文件,通过add_library还可以生成库类型的目标。
  默认CMake编译输出的信息是比较简洁的,如果出现什么问题了,或者想查看编译过程的实际执行的编译命令和编译参数,可以设置变量CMAKE_VERBOSE_MAKEFILE为ON,或者直接在命令行执行cmake -DCMAKE_VERBOSE_MAKEFILE=ON ../即可。

二、自定义库

  将所有的源代码都丢在一个目录下“扁平化”管理是很糟糕的,现在的软件工程都讲求模块化设计,所以通常的手段是将模块的代码单独放在一个文件夹,将模块编译成库,一方面方便代码(库)的重用,而来对于整个项目的管理、调试都是大有裨益的。
  CMake支持这种递归形式的处理,只需要在项目根目录的CMakeLists.txt通过add_subdirectory命令添加你模块所在的源代码目录:

1
add_subdirectory( source/Netd/ )

  然后调到你模块所在的源代码目录(比如上面例子的source/Netd/),再新建一个CMakeLists.txt文件,通过上面提到的add_library命令指定所需生成的库名称,生成的库还可以指定生成动态库SHARED还是静态库STATIC的类型。

1
2
3
4
5
6
7
aux_source_directory(. DIR_LIB_SRCS)
add_library (Netd SHARED ${DIR_LIB_SRCS})
add_library (Netd_static STATIC ${DIR_LIB_SRCS})
install (TARGETS Netd Netd_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static )

  后面那个install是在安装的时候需要使用到的。因为CMake有个问题,就是安装必须在当前目录下指明,所以该库的安装命令不能在顶层的CMakeLists.txt文件中去设定,虽然麻烦,但是设计如此。
  一旦可执行程序使用了连接库(无论是自己生成的还是其他外部组件的),都需要在CMakeLists.txt中指明这种依赖关系。如果你想让自己的CMakeLists.txt看得优雅一些,推荐set个变量来慢慢收集,而且有时候你还不得不这么做。

1
2
3
4
set (EXTRA_LIBS ${EXTRA_LIBS} pthread hiredis iconv)
set (EXTRA_LIBS ${EXTRA_LIBS} boost_system boost_thread boost_date_time boost_log boost_log_setup boost_regex)
set ( EXTRA_LIBS ${EXTRA_LIBS} Netd )
target_link_libraries( aimlsrvd ${EXTRA_LIBS} )

三、宏变量和编译选项

3.1 编译命令行选项

  通过add_definitions函数可以设置编译时候命令行的宏定义,比如gcc的-D和巨硬的/D。一个常用的例子就是assert的NDEBUG宏开关了。

1
add_definitions (-DBOOST_LOG_DYN_LINK -DNDEBUG)

  除了这个,CMake还有很多内置的变量和可以设置,比如Debug/Release便宜版本,编译参数的设置等(比如c++11标准必须显式设定),在此罗列出来供大家参考。Debug版本和Release版本编译出来的可执行程序的尺寸相差巨大啊!

1
2
3
4
5
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 " )
#set(CMAKE_CXX_FLAGS "-Wall -Wconversion -Woverloaded-virtual -Wpointer-arith -Wshadow -Wwrite-strings -march=native " )
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -g")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O2 ")

3.2 通过文件配置宏定义

  通过configure_file这个命令,CMake可以将输入配置文件进行处理,并转换成可用的C/C++头文件,而且在配置文件模板中使用的@VAR@变量会被在CMakeLists.txt中定义的值所替换,这样就提供了CMakeLists.txt-配置模板-应用程序三者交换数据的通道。对此,在CMake手册中最经典不过的例子就是软件版本号了。下面是在CMakeLists.txt定义的主、次两个版本号信息:

1
2
3
set (AIMLSRV_VERSION_MAJOR 0)
set (AIMLSRV_VERSION_MINOR 85)
configure_file ( "include/config.h.in" "../include/config.h" )

  然后在模板配置文件config.h.in中定义两个变量,变量的值可以引用之前在CMakeLists.txt中定义的变量:

1
2
3
4
5
#ifndef _CONFIG_H_
#define _CONFIG_H_
#define aimlsrv_VERSION_MAJOR @AIMLSRV_VERSION_MAJOR@
#define aimlsrv_VERSION_MINOR @AIMLSRV_VERSION_MINOR@
#endif // _CONFIG_H_

  正常使用cmake命令,就会发现可被C/C++引用的指定头文件config.h生成了,其中的两个宏aimlsrv_VERSION_MAJOR、aimlsrv_VERSION_MINOR的值已经被正常替换,在C/C++源代码中可以像普通的宏一样被使用。

1
std::cerr << "      VERSION: "  << aimlsrv_VERSION_MAJOR << "." << aimlsrv_VERSION_MINOR << std::endl;

3.2 定义选项开关

  option命令可以提供类似选项开关(ON、OFF)的效果,而且在使用ccmake的时候可以提供ncurses库支持的图形化选择效果,不过需要特别注意使用该命令的时候,其设置结果会被缓存起来,如果你最开始生成CMake的缓存文件之后,如果修改了其取值后直接运行cmake,那么该新值是没有生效的,只有当你删除build目录下缓存文件之后再次生成,才会实际的生效。
  我有个库AiSQLpp在当前的项目中还没有完成调试,所以其选项设置为OFF,该变量会影响到下面的编译和生成过程:

1
2
3
4
5
6
7
8
9
option(USE_AISQLPP "Currently not work with aisqlpp, so must be disabled!" OFF)
if (USE_AISQLPP)
add_subdirectory( source/Aisql/ )
endif (USE_AISQLPP)
if (NOT USE_AISQLPP)
set (EXTRA_LIBS ${EXTRA_LIBS} mysqlclient z m ssl crypto dl)
else (NOT USE_AISQLPP)
set (EXTRA_LIBS ${EXTRA_LIBS} Aisql mysqlcppconn)
endif (NOT USE_AISQLPP)

  上面定义了名称为USE_AISQLPP的开关选项,其ON和OFF的选项值决定了程序是使用AiSQLpp库还是使用原生的mysqlclient来访问数据库。我们在之前的config.h.in配置模板文件中可以使用cmakedefine来声明这个宏:

1
#cmakedefine USE_AISQLPP

  如果USE_AISQLPP的值是ON,那么在生成的config.h中就会有#define USE_AISQLPP这个宏定义的存在,如果该选项是OFF,那么就将没有这个宏的定义,因此这个选项的定义还可以控制程序源代码的行为。

四、单元测试

  CMake自带单元测试工具ctest,更令我高兴的是可以和Boost.Test单元测试框架无缝结合,这样单元测试的代码也可以使用CMakeLists.txt文件进行管理了。当然,根据网上的资料看来,集成gtest和CMake使用也不是难事,不过个人来说还是倾向于喜欢Boost全家桶。
  单元测试的目录的CMakeLists.txt跟项目差不多,只不过需要额外的添加enable_testing()命令,在文件的末尾对于每个测试用例,都要使用add_test()命令添加到测试用例集中去。按照下面的步骤编译测试用例:

1
2
3
4
test git:(master) ✗ mkdir build && cd build
➜ build git:(master) ✗ cmake ..
➜ build git:(master) ✗ cmake --build .
➜ build git:(master) ✗ ctest

  下面是执行单元测试的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  build git:(master) ✗ ctest 
Test project /home/taozj/remote_build/aimlsrvd/test/build
Start 1: dividewordtest
1/3 Test #1: dividewordtest ................... Passed 0.20 sec
Start 2: aisqlpptest
2/3 Test #2: aisqlpptest ...................... Passed 0.41 sec
Start 3: utfgbktest
3/3 Test #3: utfgbktest ....................... Passed 0.38 sec

100% tests passed, 0 tests failed out of 3

Total Test time (real) = 1.42 sec
➜ build git:(master) ✗

  如果想要更详细的输出,比如想在测试的过程中查看测试用例和应用程序代码中的打印信息,可以使用ctest –verbose命令,虽然现实的东西比较多额多,但是总体输出格式还是非常友好第。

五、安装和打包

5.1 安装

  通过install命令可以安装编译产物——可执行文件、动态|静态库、头文件、配置文件等,默认情况下CMake还是比较保守的,全部都安装到/usr/local/{bin,lib,include}目录下。对于可执行文件和头文件可以按照下面的方式进行设置,而对于动态和静态库文件,由于CMake的限制必须在当前目录进行安装,所以需要在对应的子文件下进行安装,其代码已经在上面自定义库的段落进行了示范。

1
2
install (TARGETS aimlsrvd DESTINATION bin )
install (DIRECTORY include/ DESTINATION include/aimlsrvd)

  设置好之后,使用➜ build git:(master) ✗ sudo make install就可以进行实际的安装。不过,默认情况下CMake不提供uninstall目标,虽然从官方的手册上面可以在配置模板文件中添加uninstall目标的支持,其实安装文件都记录在了install_manifest.txt文件中,只需要执行下面一行命令就可以搞定删除操作:

1
2
➜  build git:(master) ✗ xargs rm < install_manifest.txt
➜ build git:(master) ✗ cat install_manifest.txt | sudo xargs rm

5.2 软件打包

  如果上面的安装配置好了,那么软件打包也极为的简单,只需要将下面几行代码粘贴到根目录的CMakeLists.txt文件的末尾,然后使用cpack工具就可以进行可执行文件、源代码文件的打包操作。
  不过我个人而言觉得不是很必要,因为:线上的项目基本都是编译玩直接在当前目录启动运行,不会发布给其他人使用,也就没有了软件打包的需要;如果需要打包,各大发行版有更专业的打包工具(比如rpmbuild),使用cpack打包成一个.sh的安装文件总感觉显得比较的业余。

1
2
3
4
include (InstallRequiredSystemLibraries)
set (CPACK_PACKAGE_VERSION_MAJOR "${AIMLSRV_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${AIMLSRV_VERSION_MINOR}")
include (CPack)

六、其他

  这里记录一些使用过程实际遇到的问题,主要是GCC的相关吧。
  (1). 作为一个代码洁癖控来说,通常自己的代码都会添加严格的编译选项,所以有什么Warning都会在开发的萌芽阶段将其消灭。而如果接收别人的代码写的没那个规矩,编译过程很可能被Warning刷屏,那么在编译参数的时候可以临时关闭某些Warning的提示,比如常见的参数有:-Wno-write-strings、-Wno-format。
  (2). 关于链接库也是有顺序问题的。为了节省内存增加效率,链接器通常采用one-pass的工作方式,链接器按照顺序扫描目标和库,如果发现没有解析的符号就将这个符号记录下来,在后续扫描的生活如果发现了该符号的定义再回头补上该符号的地址。所链接器这种工作模式就需要手动维护链接库的垂直依赖关系,如果未解析的符号定义再这个顺序之前就无法连接,所以习惯上链接库的排列顺序是最上层的应用库、项目的基础库、系统库来避免上述问题,越是基础、越是独立的库越往后面写。
  如果项目中出现了库的交叉引用,就需要寻求额外的方式进行处理了,比如多次扫描等。

本文完!

参考