Home CMake学习
Post
Cancel

CMake学习

CMake是一个跨平台、开源的构建工具,在各种大型C/C++项目中被广泛使用。

  • CMake是一个"generator",它不会直接编译目标,而是根据CMakeLists.txt生成当前平台编译所需要的文件(如makefile),然后由当前平台编译工具进行编译(如make)。参考

  • 之所以说CMake是跨平台的,是因为CMakeList.txt是平台无关的,实现了“Write once, run everywhere”参考

CMake根据CMakeLists.txt中的变量,来确定当前的构建环境和构建规则。构建规则包括:要需要包含哪些头文件、链接哪些库、语言的版本、编译器参数等等。参考

CMake作为一项硬技能,要好好学一下。本页为我在学习CMake过程中的一些笔记。分为三个层级:

  • 第一层:如何使用
  • 第二层:每一步背后发生了什么
  • 第三层:一些更细致全面的学习

第一层:如何使用

这一节首先用一个简单的样例,看看如何操作,来借助CMake完成编译。

假如我们现在有一个源文件demo.cpp

1
2
3
4
5
6
7
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

编写CMakeLists.txt

1
2
3
4
5
6
7
8
# cmake最低版本需求
cmake_minimum_required(VERSION 3.13)

# 指定project名称
project(DemoProject)

# 编译源码生成目标
add_executable(demo demo.cpp)

文件目录如下:

1
2
|- demo.cpp
|- CMakeList.txt

依次执行

1
2
3
mkdir build
cd build
cmake ..

这一步直接使用cmake .当然也是可以的。但是生成的中间文件会和源文件混杂在一起,发生凌乱。因此一般都是新建一个build文件夹,用来保存中间文件(即通常说的"out-of-source build"

再执行

1
make

即可完成编译,在build目录下生成可执行文件demo

执行

1
2
3
4
# build目录下
./demo

# 输出: Hello, World!

第二层:每一步发生了什么

这一节看看上面的各个步骤分别发生了什么。

上面各步是一个典型的"out-of-source build"过程。具体而言,build目录是我们执行cmake并保存生成文件的地方;..是CMakeLists.txt所在的目录。

  • mkdir build; cd build

    • 创建并进入build文件夹,编译过程生成的中间文件将保存在此,与源文件隔离开来。
  • cmake ..

    • 这一句的原型为:cmake <path-to-source>。即到当前(/build)上一层目录中寻找CMakeLists.txt

    • 这一步会生成以下文件(最关键的是Makefile),并保存在当前目录(/build)下

      |- CMakeFiles
      	|- ...
      |- cmake_install.cmake
      |- CMakeCache.txt
      |- Makefile
      
      • 这些生成的文件分别代表什么?参考
        • 我理解,除了Makefile,其他都是类似于”中间文件“的存在吧。有了它们之后,再次执行cmake会跳过很多操作。
    • 参考: stackoverflow - What does cmake .. do?

  • make

    • 根据生成的Makefile执行make编译。

第三层:一些更全面的学习

这一节记录一些CMake的基本语法与使用策略。

基本语法

一些常用的指令

CMakeLists.txt文件中指令不区分大小写,但一般来说同一个文件中指令应保持大小写风格一致。

  • cmake_minimum_required
    • 一般这条指令出现在CMakeLists.txt的最开始,指明了对cmake的最低版本的要求。
    • 参考
  • project
    • 指定工程名称(和版本、描述等)。
    • 工程名称给定后,一些内置变量,如PROJECT_NAMECMAKE_PROJECT_NAME会被自动赋为该值。
    • 参考
  • include_directories
  • link_directories
    • 添加需要链接的库文件目录
    • 相当于g++中-L的作用。
    • 何时使用:如果要链接的库,没有放在/lib/usr/lib/usr/local/lib这三个目录里,需要使用该指令指定库文件所在的目录。
  • target_link_libraries

    • 添加需要链接的库名称(或路径等,参见官方文档)。
    • 相当于g++中-l的作用。
  • add_library

    • 把指定的源文件来打包成动态库或静态库。

      问:源文件写.cpp还是.h?

      似乎是要写cpp。写.h的话,会在cmake阶段报错CMake Error: Cannot determine link language for target

  • add_executable

    • 使用指定的源文件来生成目标可执行文件。
    • 参考
  • add_definitions
    • 向C/C++编译器添加 -DXXX 定义。如果代码中定义了#ifdef XXX #endif,这个代码块就会生效。
    • 参考
  • aux_source_directory
    • 使用方法为:aux_source_directory(<dir> <variable>),即根据dir内的所有源代码文件名生成列表,保存在变量variable中。
    • 参考
  • add_subdirectory
    • 添加一个子目录并构建该子目录。
    • 子目录下应该包含CMakeLists.txt文件和代码文件。
    • 后文讲组织关系时会重点介绍该指令。
    • 参考
  • file
  • message
  • option
    • 定义一个bool开关,可在终端赋值
    • 语法为:option(<variable> "<help_text>" [value])
    • 参考

变量

访问变量

使用${}进行变量的访问,如${VAR}

定义变量

显式定义

使用set显式定义变量,基本的用法为

1
set(<variable> <value>)

示例

1
2
set(VAR "I am a variable")
message("${VAR}")   # I am a variable

注:

  • 如果set语句的value不加双引号,则定义的变量则被解释成列表,如

    1
    2
    
    set(VAR I am a variable)
    message("${VAR}")   # I;am;a;variable
    
  • 如果message语句中对变量的引用不加双引号,则输出中各元素中无空格,如

    1
    2
    
    set(VAR "I am a variable")
    message(${VAR})  	# Iamavariable
    

参考-stackoverflow: When should I quote CMake variables?

隐式定义

举例

使用project(PROJ_NAME)语句指定工程名称后,则<PROJ_NAME>_BINARY_DIR<PROJ_NAME>_SOURCE_DIR则会被隐式地定义:

1
2
3
4
5
project(ToyDemo)
message("<PROJ_NAME>_BINARY_DIR: ${ToyDemo_BINARY_DIR}")
# output: path/to/current_bin_dir
message("<PROJ_NAME>_SOURCE_DIR: ${ToyDemo_SOURCE_DIR}")
# output: path/to/current_source_dir

内置变量

以下记录下常见的内置变量

变量名 含义
PROJECT_NAME 通过project()指令定义的项目名称
PROJECT_SOURCE_DIR 源CMakeLists.txt所在的目录
PROJECT_BINARY_DIR 执行cmake命令所在的目录

控制指令写法

分支控制

1
2
3
4
5
6
7
if(<condition>)
  <commands>
elseif(<condition>) 
  <commands>
else()               # ()不能省
  <commands>
endif()              # ()不能省

注:

需要注意的是,<condition>中如果访问变量,不用(不应)加${}

  • 加不加${}的区别是什么?
    • :heavy_check_mark:if (VAR):会以变量VAR的值为判断条件。
    • :x:if (${VAR})${VAR}访问得到一个常量或者另一个变量。如果${VAR}是一个常量,那么if语句会寻找和该常量同名的变量作为判断条件,故可能会导致预料之外的结果。
    • 这里有一个细致的解释。官方文档也提到这个问题。

**示例**

循环控制

foreach

参考

1
2
3
4
5
6
7
foreach(<loop_var> <items>)
  <commands>
endforeach()

foreach(<loop_var> RANGE <stop>)  # 注意右侧闭区间,会输出[0, stop]共stop + 1个数!

foreach(<loop_var> RANGE <start> <stop> [<step>])

其中items代指要遍历的列表,loop_var代指正在被迭代到的项。

示例

1
2
3
4
5
6
7
8
9
10
11
12
set(mylist "a" "b" "c" "d")
foreach(var ${mylist})
	message("current value: ${var}")
endforeach()

foreach(var RANGE 3)
    message("current value: ${var}")  # 输出 0 1 2 3 共【4】个数!
endforeach()

foreach(var RANGE 0 6 2)
    message("current value: ${var}")  # 输出 0 2 4 6
endforeach()
while
1
2
3
while(<condition>)
  <commands>
endwhile()

如果需要用到数学计算,参照这里

示例

1
2
3
4
5
set(var 0)
while(var LESS 3)
    message("cur var ${var}")
    math(EXPR var "${var} + 1")
endwhile()
break和continue

break()continue()插入到循环体中终止循环。

常见的文件组织方式及应对策略

含有多个子目录

示例

文件目录如下:

1
2
3
4
5
6
7
8
9
10
11
.
├── CMakeLists.txt
├── main.cpp
├── sub1
│   ├── CMakeLists.txt
│   ├── test1.cpp
│   └── test1.h
└── sub2
    ├── CMakeLists.txt
    ├── test2.cpp
    └── test2.h

主目录下CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(test)

include_directories(sub1)
include_directories(sub2)

add_subdirectory(sub1)
add_subdirectory(sub2)

add_executable(test main.cpp)
target_link_libraries(test sub1_lib sub2_lib)

主目录下main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
// main.cpp
#include "test1.h"
#include "test2.h"
#include <iostream>

int main(int argc, char** argv)
{
    test1_func();
    test2_func();

    return 0;
}

sub1目录下CMakeLists.txt

1
2
3
4
# sub1/CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(sub1)
add_library(sub1_lib test1.cpp)

sub1目录下test1.htest1.cpp

1
2
3
4
5
6
// sub1/test1.h
void test1_func();

// sub1/test1.cpp
#include "test1.h"
void test1_func(){}

sub2目录下CMakeLists.txt

1
2
3
4
# sub2/CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)
project(sub2)
add_library(sub2_lib test2.cpp)

sub2目录下test2.htest2.cpp

1
2
3
4
5
6
// sub2/test2.h
void test2_func();

// sub2/test2.cpp
#include "test2.h"
void test2_func(){}

之后在主目录下正常执行cmake即可。

需要注意的问题

  • CMakeLists.txt文件名大小敏感,否则会报错"CMake Error: does not appear to contain CMakeLists.txt"

参考

This post is licensed under CC BY 4.0 by the author.

对Roofline模型的理解

格式化输出小节