CMake使用

本文更新于 2019.01.04

本文的示例代码见: https://github.com/zzqcn/storage/blob/master/code/cpp/cmake_demo_20190104.tar.gz

CMake是一个跨平台的自动化建构系统,它使用一个名为CMakeLists.txt的文件来描述构建过程, 可以产生标准的构建文件, 如Unix的Makefile或Windows Visual C++的projects/workspaces. 文件CMakeLists.txt需要手工编写, 也可以通过编写脚本进行半自动的生成. CMake 提供了比autoconfig更简洁的语法. 在 linux 平台下使用 CMake 生成Makefile并编译的流程如下:

  1. 编写 CmakeLists.txt
  2. 执行命令”cmake PATH”生成 Makefile ( PATH 是 CMakeLists.txt 所在的目录 )
  3. 使用 make 命令进行编译

Hello World

建立hello目录存放我们的工程文件, 其中只有一个源文件hello.cpp:

#include <iostream>

int main(void)
{
    std::cout << "hello world\n";
    return 0;
}

编写CMakeLists.txt文件, 并将它与hello.cpp放在同一个目录下, 其中的内容见注释:

# 项目名称
PROJECT(hello)
# 限定CMake最小版本
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
# 将当前目录中的源文件名赋值给DIR_SRCS
AUX_SOURCE_DIRECTORY(. DIR_SRCS)
# DIR_SRCS中的源文件需要编译为名为hello的可执行文件
ADD_EXECUTABLE(hello ${DIR_SRCS})

CMakeLists.txt的语法比较简单, 由命令、注释和空格组成,其中命令是 不区分大小写 的,符号”#”后面的内容被认为是注释.命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔.

完成了文件 CMakeLists.txt 的编写后使用 cmake命令生成Makefile.进入hello目录, 执行:

$ cmake .
-- The C compiler identification is GNU 4.8.5
-- The CXX compiler identification is GNU 4.8.2
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: /root/zzq/tmp/cmake/hello

这时就生成了Makefile, 此时hello目录下文件列表如下:

$ ls -F
CMakeCache.txt  CMakeFiles/  cmake_install.cmake  CMakeLists.txt  hello.cpp  Makefile

然后调用make即可完成编译:

$ make
Scanning dependencies of target hello
[100%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
Linking CXX executable hello
[100%] Built target hello

清理编译后留下的文件

cmake在生成Makefile的同时, 还会生成一堆别的文件和目录, 如何清理这些文件呢?

答案是专门建立一个用于编译构建的目录, 编译完成后删除它即可. 比如上面的hello工程, CMakeLists.txt仍放在Hello目录, 再建一个与hello目录同级的hello_build目录, 在其中调用:

$ cmake ../hello
$ make

这样cmake生成的Makefile, 中间文件, 以及最后编译生成的可执行文件都在hello_build目录中.

处理多个目录

上面的hello world是最简单的示例. 实际项目中往往有多个目录, 下面演示一下多目录的处理方法.

将hello目录复制为hello2目录, 往hello2目录中添加一个子目录lib, 里面有两个文件mylib.h和mylib.cpp, 它们会编译成库, 供hello链接. 这两个文件的内容如下:

// mylib.h
#ifndef __MYLIB_H__
#define __MYLIB_H__

void printHello(void);

#endif

// mylib.cpp
#include <cstdio>
#include "lib/mylib.h"

void printHello(void)
{
    printf("hello world\n");
}

hello2/下的CMakeLists.txt需要修改:

# 项目名称
PROJECT(hello)
# 限定CMake最小版本
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
# 添加子目录
ADD_SUBDIRECTORY(lib)
# 将当前目录中的源文件名赋值给DIR_SRCS
AUX_SOURCE_DIRECTORY(. DIR_SRCS)
# DIR_SRCS中的源文件需要编译为名为hello的可执行文件
ADD_EXECUTABLE(hello ${DIR_SRCS})
# 可执行文件hello需要链接一个名为mylib的链接库
TARGET_LINK_LIBRARIES(hello mylib)

lib/下也需要创建一个CMakeLists.txt文件, 其内容为:

# 将当前目录中的源文件名赋值给DIR_LIB_SRCS
AUX_SOURCE_DIRECTORY(. DIR_LIB_SRCS)
# DIR_LIB_SRCS中的源文件需要编译为名为mylib的库
ADD_LIBRARY(mylib ${DIR_LIB_SRCS})

此时项目的目录结构为:

$ tree hello2
hello2
├── CMakeLists.txt
├── hello.cpp
└── lib
    ├── CMakeLists.txt
    ├── mylib.cpp
    └── mylib.h

进入hello_build目录, 重新生成Makefile, 然后用make编译:

# make
Scanning dependencies of target mylib
[ 50%] Building CXX object lib/CMakeFiles/mylib.dir/mylib.cpp.o
Linking CXX static library libmylib.a
[ 50%] Built target mylib
Scanning dependencies of target hello
[100%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
Linking CXX executable hello
[100%] Built target hello

到目前为止, mylib默认编译为静态库不是动态库, hello.cpp里还需要包含”lib/mylib.h”, 而不是”mylib.h”.

处理第3方依赖库

在开发软件的时候我们会用到一些函数库,这些函数库在不同的系统中安装的位置可能不同,编译的时候需要首先找到这些软件包的头文件以及链接库所在的目录以便生成编译选项.

将hello2目录复制为hello3目录, 然后建立一个与hello3同级的3rd目录, 并把原来lib目录中的源码移动到其中, 先手动编译成动态库:

$  g++ -O3 -fPIC -shared -o libmylib.so mylib.cpp

在hello3/目录中建立子目录结构cmake/modules/, 在其中创建文件Findlibmylib.cmake, 内容如下:

# 将参数的内容输出到终端
MESSAGE(STATUS "using bundled Findlibmylib.cmake...")
# 指明头文件查找路径
FIND_PATH(
    LIBMYLIB_INCLUDE_DIR
    mylib.h
    /root/zzq/tmp/cmake/3rd/
)
# 指明链接库查找路径
FIND_LIBRARY(
    LIBMYLIB_LIBRARIES NAMES mylib
    PATHS /root/zzq/tmp/cmake/3rd/
)

注意这里的文件命名有限制, 必须是FindlibXXX.cmake, 其中XXX是库的名字, 对于此示例是mylib. 另外:

  • MESSAGE: 将参数的内容输出到终端, 有点类似于Makefile中的$(info XXX)
  • FIND_PATH: 指明头文件查找的路径, 原型如下: FIND_PATH(<VAR> name1 [path1 path2 ...]) 该命令在参数pathXXX指示的目录中查找文件name1并将查找到的路径保存在变量 VAR 中
  • FIND_LIBRARY: 与FIND_PATH类似, 用于查找链接库并将结果保存在变量中

根目录hello3/下面的CMakeLists.txt改为:

# 项目名称
PROJECT(hello)
# 限定CMake最小版本
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
# 设置module cmake files查找路径
SET (CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules)
# 将当前目录中的源文件名赋值给DIR_SRCS
AUX_SOURCE_DIRECTORY(. DIR_SRCS)
# DIR_SRCS中的源文件需要编译为名为hello的可执行文件
ADD_EXECUTABLE(hello ${DIR_SRCS})
# 在CMAKE_MODULE_PATH中查找libmylib依赖库相关设定
FIND_PACKAGE(libmylib REQUIRED)
# 如果LIBMYLIB_INCLUDE_DIR和LIBMYLIB_LIBRARIES都被赋值, 设置头文件路径并链接库
IF (LIBMYLIB_INCLUDE_DIR AND LIBMYLIB_LIBRARIES)
    MESSAGE(STATUS "found libmylib libraries")
    INCLUDE_DIRECTORIES(${LIBMYLIB_INCLUDE_DIR})
    TARGET_LINK_LIBRARIES(hello ${LIBMYLIB_LIBRARIES})
ENDIF (LIBMYLIB_INCLUDE_DIR AND LIBMYLIB_LIBRARIES)

此时目录结构为:

$ tree 3rd hello3
3rd
├── libmylib.so
├── mylib.cpp
└── mylib.h
hello3
├── cmake
│   └── modules
│       └── Findlibmylib.cmake
├── CMakeLists.txt
└── hello.cpp

在hello_build下, 重新运行cmake和make即可编译hello3.

生成debug和release版的程序

在Visual Studio中我们可以生成debug版和release版的程序,使用CMake我们也可以达到上述效果. debug版的项目生成的可执行文件需要有调试信息并且不需要进行优化, 而release版的不需要调试信息但需要优化. 这些特性在gcc/g++中是通过编译时的参数来决定的, 如果将优化程度调到最高需要设置参数-O3, 最低是-O0即不做优化; 添加调试信息的参数是-g, 如果不添加这个参数, 调试信息就不会被包含在生成的二进制文件中.

CMake中有一个变量CMAKE_BUILD_TYPE, 可以的取值是Debug, Release, RelWithDebInfo和MinSizeRel. 当这个变量值为Debug的时候, CMake会使用变量CMAKE_CXX_FLAGS_DEBUG和CMAKE_C_FLAGS_DEBUG中的字符串作为编译选项生成Makefile, 当这个变量值为Release的时候, 工程会使用变量CMAKE_CXX_FLAGS_RELEASE和CMAKE_C_FLAGS_RELEASE选项生成Makefile.

下面把最开始的hello例子目录hello/复制为hello4/, 在其中使用这个特性. 修改hello4/CMakeLists.txt:

# 项目名称
PROJECT(hello)
# 限定CMake最小版本
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
# 设置Release版本的编译选项
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
# 设置Debug版本的编译选项
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -g -Wall")
# 将当前目录中的源文件名赋值给DIR_SRCS
AUX_SOURCE_DIRECTORY(. DIR_SRCS)
# DIR_SRCS中的源文件需要编译为名为hello的可执行文件
ADD_EXECUTABLE(hello ${DIR_SRCS})

然后就可以通过设置CMAKE_BUILD_TYPE这个选项来生成Makefile:

  • Debug版本 cmake -DCMAKE_BUILD_TYPE=Debug ../hello4
  • Release版本 cmake -DCMAKE_BUILD_TYPE=Release ../hello4

cmake生成的Makefile在编译时可以在make命令添加VERBOSE=1参数, 来打印编译过程中的详细信息, 这可以帮助我们检查编译选项是否生效.

当编译Release版时, 输出如下:

$ make VERBOSE=1
/usr/bin/cmake -H/root/zzq/tmp/cmake/hello4 -B/root/zzq/tmp/cmake/hello_build --check-build-system
    CMakeFiles/Makefile.cmake 0
/usr/bin/cmake -E cmake_progress_start /root/zzq/tmp/cmake/hello_build/CMakeFiles
    /root/zzq/tmp/cmake/hello_build/CMakeFiles/progress.marks
make -f CMakeFiles/Makefile2 all
make[1]: 进入目录“/root/zzq/tmp/cmake/hello_build”
make -f CMakeFiles/hello.dir/build.make CMakeFiles/hello.dir/depend
make[2]: 进入目录“/root/zzq/tmp/cmake/hello_build”
cd /root/zzq/tmp/cmake/hello_build && /usr/bin/cmake -E cmake_depends "Unix Makefiles"
    /root/zzq/tmp/cmake/hello4 /root/zzq/tmp/cmake/hello4 /root/zzq/tmp/cmake/hello_build
    /root/zzq/tmp/cmake/hello_build /root/zzq/tmp/cmake/hello_build/CMakeFiles/hello.dir/DependInfo.cmake --color=
Dependee "/root/zzq/tmp/cmake/hello_build/CMakeFiles/hello.dir/DependInfo.cmake" is newer than depender
    "/root/zzq/tmp/cmake/hello_build/CMakeFiles/hello.dir/depend.internal".
Dependee "/root/zzq/tmp/cmake/hello_build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than
    depender "/root/zzq/tmp/cmake/hello_build/CMakeFiles/hello.dir/depend.internal".
Scanning dependencies of target hello
make[2]: 离开目录“/root/zzq/tmp/cmake/hello_build”
make -f CMakeFiles/hello.dir/build.make CMakeFiles/hello.dir/build
make[2]: 进入目录“/root/zzq/tmp/cmake/hello_build”
/usr/bin/cmake -E cmake_progress_report /root/zzq/tmp/cmake/hello_build/CMakeFiles 1
[100%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
/usr/bin/c++    -O3 -Wall   -o CMakeFiles/hello.dir/hello.cpp.o -c /root/zzq/tmp/cmake/hello4/hello.cpp
Linking CXX executable hello
/usr/bin/cmake -E cmake_link_script CMakeFiles/hello.dir/link.txt --verbose=1
/usr/bin/c++    -O3 -Wall    CMakeFiles/hello.dir/hello.cpp.o  -o hello -rdynamic
make[2]: 离开目录“/root/zzq/tmp/cmake/hello_build”
/usr/bin/cmake -E cmake_progress_report /root/zzq/tmp/cmake/hello_build/CMakeFiles  1
[100%] Built target hello
make[1]: 离开目录“/root/zzq/tmp/cmake/hello_build”
/usr/bin/cmake -E cmake_progress_start /root/zzq/tmp/cmake/hello_build/CMakeFiles 0

可以看出其中加入了-O3 -Wall这些编译选项. 而编译调试版本时则带有-O0 -g, 这里就不再贴出了.