概要

本篇笔记从实例出发,逐步分析 cmake 的使用方法。为了更好地拟合真实项目,本实例有如下功能:

  • 添加项目版本号

  • 添加编译选项,可选择使用 myMath 库或第三方 3rdMath 库

  • 使用了静态链接库和动态链接库,既有项目生成的库,也有第三方库

  • 提供了安装功能

  • 能够在 Windows 和 Linux 下成功编译运行

    读者可能认为 cmake 本来就是跨平台的,所以支持 Windows 和 Linux 应该是理所当然的。实际上,支持跨平台还需要注意一些问题,后文会提到它们。

项目地址:cmake实例

另外,初学 cmake 的朋友们可以在 Linux 下试试 CLion ,其本身就是用 cmake 管理项目,很容易上手。

分析本项目之前,先来简单了解一下 cmake 。

cmake、nmake、make、makefile

make 是 Unix/Linux 下的一个构建工具,用于自动化构建和编译过程。它可以读取一个名为 Makefile 的文件,根据其中的指令来编译和链接源代码文件,生成可执行文件或库文件。实际上,make 最后也是调用的 gcc 和 ld 来完成编译和链接任务。Makefile 最早由程序员直接编写,但很快凸显出瓶颈:1)对于大型项目,手写 Makefile 文件相当费时费力;2)Makefile 和 make 都是 Unix/Linux 下管理工程的工具,无法跨平台使用。于是 cmake 应运而生,利用 cmake,可以根据 CMakelist.txt 在不同的平台下上产生不同的构建文件比如 Linux 下产生 Makefile,Windows 下则产生 .sln 解决方案文件和 .vcxproj 项目文件等 。那么 nmake 呢?在 Linux 下,make 根据 Makefile 来构建项目,而 Windows 下则是 nmake 根据 Makefile 来构建项目。

目录结构

为了使后续的讲解更加具体,先给出本项目的目录结构:

.
├── 3rdParty
│   ├── lib3rdMath
│   │   ├── bin
│   │   │   └── lib3rdMath.a
│   │   └── include
│   │       └── 3rdMath.h
│   └── libplay
│       ├── bin
│       │   └── libplay.a
│       └── include
│           └── play.h
├── CMakeLists.txt
├── config.h.in
├── include
│   └── student.h
├── libs
│   ├── CMakeLists.txt
│   ├── myMath
│   │   ├── include
│   │   │   └── myMath.h
│   │   └── src
│   │       └── myMath.c
│   └── work
│       ├── include
│       │   └── work.h
│       └── src
│           └── work.c
├── README.md
└── src
    ├── main.c
    └── student.c
  • src 目录用来存放项目的主要逻辑代码,include 则存放对应头文件。
  • lib 目录用来存放项目自己的库的源代码,myMath是静态库,work是动态库
  • 3rdParty 目录存放第三方库文件,包含静态链接库和对应头文件。
  • config.h.in 用来配置项目,后文细说。
  • README 用来简单说明项目的使用方法,必不可少。
  • CMakeLists.txt 当然是用来指挥整个项目的编译啦,注意,文件名严格区分大小写。

外部构建

通常我们不会在 根目录(最外层的 CMakeLists.txt 所在的目录) 中直接编译项目,这样的话生成文件会和源文件会混杂在一起凌乱不堪。常见的做法是在根目录下创建一个 build 目录,然后在 build 中进行编译,这就叫做 外部构建

mkdir build
cd build
cmake ..
make

这样所有的生成文件都会存放在 build 中,不会影响原来的目录结构。

提到构建目录,就不得不说 cmake 的内置变量 PROJECT_BINARY_DIRPROJECT_BINARY_DIR 指向的是我们执行 cmake 命令时所处的目录,对于本项目,就指向 build 目录。PROJECT_SOURCE_DIR 指向的是根目录。 其他变量见后文。

构建顺序与传播规律

cmake 根据 CMakeLists.txt 来生成 Makefile 。项目的不同目录中可以存在多个 CMakeLists.txt ,它们之间存在一定的联系和依赖关系 。就本项目而言,内层的 CMakeLists.txt 指导生成 myMath 静态库和 work 动态库,外层的 CMakeList.txt 则需要使用这两个库。
CMakeLists.txt 文件还可以定义一些全局变量或宏,这些变量或宏可以在不同的CMakeLists.txt文件之间共享和使用 ,以便在整个项目中保持一致性,本项目中外层就向内层传递了 USE_MYMATH 宏,内层判断此宏是否被定义,若定义则会生成 myMath 库,细节后文详述。

内外层的 CMakeLists.txt 是怎么联系起来的呢?很简单,使用 cmake 的内置命令 add_subdirectory 。外层 CMakeLists.txt 中使用此命令来添加内层 CMakeLists.txt 所在的目录,然后 cmake 就会自动搜索该目录下的 CMakeLists.txt 并建立联系。

绝大多数教程会忽略但又必须提到的一点:cmake 构建项目时采用的是深度优先遍历 ,也就是说,扫描 CMakeLists.txt 时只要碰到 add_subdirectory 就会立刻进入内层 CMakeLists.txt 并继续扫描。证明这个结论很简单,利用 cmake 的打印命令 message 即可,如下构建目录:

dir0
├── CMakeLists.txt
├── dir1
│   ├── CMakeLists.txt
│   └── dir3
│       └── CMakeLists.txt
├── dir2
│   └── CMakeLists.txt
└── main.c

各自目录的 CMakeLists.txt 如下:

#dir0
cmake_minimum_required(VERSION 3.5)
project(untitled C)
message("dir0-before")
add_subdirectory(dir1)
add_subdirectory(dir2)
message("dir0-after")

#dir1
message("dir1-before")
add_subdirectory(dir3)
message("dir1-after")

#dir2
message("dir2")

#dir3
message("dir3")

创建并进入 build 目录,构建项目,提示如下:

~/CLionProjects/dir0$ mkdir build
~/CLionProjects/dir0$ cd build/
~/CLionProjects/dir0/build$ cmake ..
.....省略......
dir0-before
dir1-before
dir3
dir1-after
dir2
dir0-after

笔者强调这个不起眼的顺序问题是因为它很多时候会影响构建结果,而且错误很难排查。具体而言,局部变量(自定义变量)在各层 CMakeLists.txt 中的共享情况就会受构建顺序的影响 。那么,局部变量是如何在各个 CMakeLists.txt 中传递并共享的呢?笔者总结了一个前提条件和两个约束条件:

前提条件:

  • 如果想要变量向下层传递,则必须在调用 add_subdirectory 前定义变量。

约束条件:

  • 作用域:变量只能从外层向内层传递。
  • 继承链:变量只能沿着父(外)子(内)继承链传播,兄弟之间不能共享。

注意,以上规则只适用于自定义变量,不适用于全局变量,即 cmake 的内置变量。

用下面的图来表示也许更加直观,其中红色箭头代表不能传播,绿色箭头代表可以传播:
![dir0定义了A变量,dir1定义了B变量]

另外,不仅变量有这样的规则,某些函数也符合此规律 ,比如 add_definition,使用此函数定义 C/C++ 宏后,该宏也会按照以上规则传播,同样的还有 option 和 configure_file 函数,下文还会提到这个问题。

声明:这三个条件是由笔者实践得出,如有错误,敬请指正!

项目分析

本项目的目录结构已经在上面给出,下面是内外层的 CMakeLists.txt 和 configure.in.h :

#外层CMakeLists.txt
cmake_minimum_required(VERSION 3.5) #要求cmake最低版本
project(cmake VERSION 3.0) #指定项目名称和版本
# 是否使用自己的Math库
option (USE_MYMATH #添加编译选项,这是变量而不是宏
        "Use provided math implementation" #打印信息
        ON) #变量默认值为 ON
configure_file ("config.h.in" "config.h") #添加配置文件,上面的编译选项就记录在配置文件中
add_subdirectory(libs) #关联子目录
aux_source_directory(src/ SRC) #自动搜索src下的所有文件并赋值给SRC变量
set(LIB play work) #将play和work库赋值给LIB变量
# 是否加入 myMath 库
if (USE_MYMATH)
    include_directories ("libs/myMath/include") #添加头文件搜索目录
    set (LIB ${LIB} myMath) #向LIB变量中添加myMath
    #推荐使用list(APPEND LIB myMath)代替上行
else ()
    include_directories ("3rdParty/lib3rdMath/include")
    link_directories("3rdParty/lib3rdMath/bin") #添加库文件搜索目录
    set (LIB ${LIB} 3rdMath)
endif (USE_MYMATH)

include_directories(
        "include"
        ${PROJECT_BINARY_DIR} #config.h
        "libs/work/include"
        "3rdParty/libplay/include")
link_directories("3rdParty/libplay/bin")
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) #指定可执行文件的输出目录
add_executable(ctest ${SRC}) #利用SRC变量中的源文件生成可执行文件ctest
target_link_libraries(ctest ${LIB}) #将库文件链接到ctest可执行文件
install (TARGETS ctest DESTINATION bin) #安装ctest到目录bin
#内层CMakeLists.txt
aux_source_directory(myMath/src/ myMathSrc)
aux_source_directory(work/src workSrc)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) #设置库文件的输出目录
if (USE_MYMATH) #该变量是在上层txt中定义
    add_library(myMath STATIC ${myMathSrc}) #生成myMath静态链接库
endif (USE_MYMATH)
if(WIN32) #如果是在Windows下构建
    add_definitions(-D_WIN32_) #则编译源文件时定义宏_WIN32_
    set(LIB_POS bin)
else()
    set(LIB_POS lib)
endif()
add_library(work SHARED ${workSrc}) #生成work共享库
install (TARGETS work DESTINATION {LIB_POS}) #将work库安装到{LIB_POS}中
//configure.h.in
#cmakedefine USE_MYMATH //如果USE_MYMATH变量为ON,则定义该宏,否则不定义
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ //用cmake中的版本变量来定义宏
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@ //用cmake中的版本变量来定义宏
#define PROJECT_NAME @PROJECT_NAME@ //用cmake中的名称变量来定义宏

注释较为详细,下面选择性讲解一些需要补充说明的内容。先来分析外层 txt :

  • 外层 txt 的任务是生成可执行文件并链接库,库的生成则交给内层 txt

  • 第 3 行, project 函数指定了项目名称和版本,有什么用呢?首先,指定项目名称后,内置变量 PROJECT_NAME 会被自动赋值为该名称,后续就可以直接使用该变量。使用变量的语法为 ${变量名} 。其次,指定版本后,内置变量 PROJECT_VERSION_MAJOR (主版本号)和 PROJECT_VERSION_MINOR (副版本号)也会被自动赋值。我们在 configure.h.in 中使用了它们,该文件作用见下文。

  • 第 5 行,定义了 USE_MYMATH 选项,默认为 ON ,即使用 myMath 库。其实 option 函数完全可以使用 set 函数来替代:

    set(USE_MYMATH ON)
    

    只不过采用 set 则无法被 ccmake 检测为选项。ccmake 是 cmake 的扩展,能够在构建时提供简单的可视化界面来配置编译选项

    图中的 CMAKE_BUILD_TYPE 也是 cmake 内置的选项,你可以将其定义为 Debug 以开启调试模式(默认是 Release 模式) 。除了这种方式,还可以通过其他cmake变量开启调试:

    //指定CMAKE_CFLAGS参数
    cmake -DCMAKE_C_FLAGS="-g -O0" ..
    
  • 第 8 行,生成配置文件。configure.hconfigure.h.in 的输出文件,后者由我们手动编写 。configure.h 会被输出到构建目录(build)中。configure.h.in 能够使用 CMakeLists.txt 中的变量 ,使用方法见上文 configure.h.in

  • 生成 configure.h 时,需要注意顺序——configure.h.in 中使用的变量应该在调用 configure_file 函数前定义 。如果你不信,可以将第 5 行的 option 放到 configure_file 函数之后,编译并运行程序后你会发现奇怪的现象。

    所以为了保险起见,可以总是将 configure_file 放在最后。

  • 第 15 行,set (LIB ${LIB} myMath) 的含义是:如果 LIB 不存在,则为其赋值 myMath;如果存在,则添加值,而不会覆盖原有值。可以使用可读性更强的 list(APPEND LIB myMath) 来代替

  • 第 19 行,添加库文件的搜索目录,这里添加的是第三方库的目录。注意,对于我们自己生成的库(内层 txt 的第 14 行)则无需手动添加搜索目录,cmake 会自动将库的输出目录(内层第 4 行)添加进去

  • 第 32 行,bin 目录是相对路径,其前缀随平台而变化:对于 Linux,前缀为 /usr/local ;对 Windows 而言,前缀为 C:\Program Files (x86)\项目名 。你可以在执行 cmake 时通过 -D 选项来指定安装目录的位置:

    cmake -DCMAKE_INSTALL_PREFIX=\xxx\yyy
    

    CMAKE_INSTALL_PREFIX 是 cmake 内置变量,指定安装目录的前缀。注意,cmake 的 -D 选项只能定义 CMakeLists.txt 中的变量,不能定义 C/C++ 中的宏

下面来看内层 txt,很简单:

  • 第 8 行,WIN32 也是 cmake 的内置变量,当在 Windows 下构建项目时,该变量就会被定义。

  • 第 9 行,add_definition 函数用来添加 C/C++ 的宏定义。

  • 第 8~12 行的含义是:如果为 Windows,则将所有库安装到 bin 目录下,即与可执行文件放在一起;如果为 Linux,则将库放在 lib 中,可执行文件放在 bin 中。

    为什么要这样安排呢?因为 Linux 通常将可执行文件放在 usr\bin 下,库文件则放在 usr\lib 中,程序运行时会自动在 usr\lib 中搜寻所需的库文件。而 Windows 的习惯则是将所有库文件(尤其是 dll)和 exe 文件存放在同一个目录,当 exe 运行时,优先从当前目录搜寻库文件,例如 QQ :
    exe与dll一起存放

Linux下构建

~/cmakeLearn$ mkdir build
~/cmakeLearn$ cd build
~/cmakeLearn/build$ cmake ..
~/cmakeLearn/build$ make

默认使用 myMath 库,输出结果如下:

~/cmakeLearn/build$ ./bin/ctest 
PROJECT cmake
VERSION 3.0
use myMath.h //myMath库
The older age is 20.
Let's sing!  //play库
Let's dance! //play库
Let's work!  //work库

也可以不使用 myMath 库,方法如下:

~/CLionProjects/cmakeLearn/build$ cmake -DUSE_MYMATH=OFF ..
~/CLionProjects/cmakeLearn/build$ make

输出结果:

~/cmakeLearn/build$ ./bin/ctest 
PROJECT cmake
VERSION 3.0
use 3rdMath.h
The older age is 20.
Let's sing!
Let's dance!
Let's work!

接着进行安装,输入 sudo make install (一定要 sudo),安装成功:

注意,现在直接在命令行中运行 ctest 可能会提示找不到库文件,这是因为库的默认搜索路径没有包含 /usr/local/lib,所以我们需要将这 lib 目录添加到系统变量 ,如下:

export LD_LIBRARY_PATH=/usr/local/lib

这种做法仅临时有效,如果要长期有效,则需要修改 .bashrc 文件。

然后就可以运行成功啦:

/$ ctest
PROJECT cmake
VERSION 3.0
use 3rdMath.h
The older age is 20.
Let's sing!
Let's dance!
Let's work!

Windows下构建

首先需要安装 cmake,直接去官网下载安装即可。

同样步骤:

>mkdir build
>cd build
>cmake ..

目录中的 .sln.vcxproj 等文件 VS 的解决方案文件和项目管理文件。不了解 Visual Studio 的工程管理方式的朋友请先移步 如何使用 VS 编译调试一个大型项目?

打开 cmake.sln 文件:

可见,cmake 很智能地将我们的工程分为了 6 个项目,主要是 ctest,myMath 和 work 项目,剩余的 ALL_BUILD、INSTALL、ZERO_CHECK 则是 cmake 自动构建的,作用如下:

  • ALL_BUILD :相当于 makefile 的默认目标 make all,用于构建整个解决方案,但不包括 INSTALL 。
  • INSTALL :执行安装任务,相当于 make install 。
  • ZERO_CHECK :检查 CMakeLists.txt 文件是否发生了变化,如果有变化则重新生成构建系统,以确保构建系统始终与 CMakeLists.txt 文件保持同步。通常情况下,不需要手动运行 ZERO_CHECK 项目,因为它会在构建过程中自动运行。

编译整个解决方案,然后构建 INSTALL,报错如下:

提示权限不够,这是因为 C:\Program Files (x86) 目录需要管理员权限才能进行更改。我们使用管理员权限启动 VS 并打开此 sln,重新编译并 INSTALL,成功:

运行 ctest.exe 后只会闪现,你可以在源代码中添加 sleep 函数来仔细看看输出结果。


常见变量与函数

变量 描述
CMAKE_BINARY_DIR
PROJECT_BINARY_DIR
执行编译时所位于的目录,对于外部构建,则一般为 build 目录。
CMAKE_SOURCE_DIR
PROJECT_SOURCE_DIR
工程顶层目录,即最外层 CMakeLists.txt 所在目录。
CMAKE_CURRENT_SOURCE_DIR 当前 CMakeLists.txt 所在的路径。
EXECUTABLE_OUTPUT_PATH
LIBRARY_OUTPUT_PATH
最终目标文件存放的路径。
PROJECT_NAME 通过 PROJECT 指令定义的项目名称。
CMAKE_INSTALL_PREFIX 安装目录的前缀
CMAKE_BUILD_TYPE 构建类型,如 Debug 或 Release
CMAKE_C_COMPILER C 编译器的路径
CMAKE_CXX_COMPILER C++ 编译器的路径
CMAKE_C_FLAGS C编译器的编译选项
WIN32 如果是在 Windows 下编译,则会定义该宏。
函数 描述
set 设置变量(覆盖原有值)
list 为变量添加值
add_subdirectory 添加子目录
add_definitions 添加编译器定义
configure_file 用于生成配置文件
message 输出消息到控制台
add_executable 添加可执行文件
add_library 添加库文件
aux_source_directory 自动包含指定目录下的所有文件
link_directories 为整个cmake项目设置库文件搜索目录
include_directories 为整个cmake项目设置头文件搜索目录
target_include_directories 为目标(可执行文件或库)设置头文件搜索路径
target_link_directories 为目标设置库文件搜索路径
target_link_libraries 将指定库链接进目标
install 安装目标文件

其他

  • 相比 Linux 下的 ccmake,Windows 下的 cmake 具有更人性化的图形界面,你可以很方便地设置构建目录、安装目录以及编译选项:

  • Windows 下执行 cmake 后会生成 sln 文件,此时你可以用 VS 打开该解决方案然后进行编译(正如我们上面所做);另外,你也可以直接在命令行中编译:

    >mkdir build
    >cd build
    >cmake  .. 
    >cmake --build . --config Release //以Release模式编译工程
    

    cmake .. 会生成系统默认的构建文件,你能够指定构建文件,可以是 Unix Makefiles、Ninja、Visual Studio 等等

    >cmake -G "Visual Studio 17" ..
    >cmake --build . --config Release
    

    这里指定的是 Visual Studio 17 。执行 cmake --build . --config Release 命令时会使用在 CMake 配置时指定的编译器来构建项目,如果没有指定编译器,则会使用默认的编译器 。在 Unix/Linux 系统上,通常是 gcc/g++;在 Windows 系统上,通常是 CL.exe 编译器。

  • 笔者起初有一个疑惑:我们常说的 Linux 下的编译三部曲:

    #./configure
    #make
    #make install
    

    这个 ./configure 是什么?它和 cmake .. 有什么区别?具体来说,configure 是 Autoconf 的配置文件,Autoconf 通过 configure 文件来生成构建系统,而 cmake 根据 CMakeLists.txt 来生成构建系统。Autoconf 和 cmake 都是跨平台的构建工具,用于自动化配置软件包的编译和安装过程。Autoconf 使用 M4 宏语言和 shell 脚本来编写配置文件,而 CMake 使用自己的 CMake 语言来编写配置文件;M4 宏语言比较复杂,而 CMake 语言简单易懂。选择哪个工具取决于个人的喜好和项目的需求。

  • 最后,虽然 cmake 是跨平台构建工具,但如果你想要你的项目能够跨平台运行,首先需要保证你的代码能够兼容多个平台,比如 I/O 复用模块 epoll 只存在于 Linux 下,Windows 对应的为 IOCP,那么你就需要编写不同的代码来适应多个平台。本项目中碰到的跨平台问题是动态链接库的编写,因为 Linux 和 Windows 对动态链接库的处理有所不同,所以也要做一些配置,参见静态链接与动态链接

本文结束。

文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
工具
喜欢就支持一下吧