CMake Tutorial

一个简单但全全面的CMake指南。

Introduction

CMake是一个可扩展的开源系统,它以一种与编译器无关的方式在操作系统中管理构建过程。

和跨平台系统不同,CMake被设计为在本机系统上使用。

CMake支持in-place(在CMAKE_SOURCE_DIR目录下直接构建)构建和out-space(在自定义的目录下构建)编译,也即在一个源码树下进行多个构建工程。

CMake支持构建动态和静态库。

CMake会生成一个缓存文件供图形编辑器(CMake-GUI)使用。

以下是一个当前比较流行的使用CMake进行构建的开源项目列表:

可以浏览Wikipedia以获得更多使用CMake进行构建的开源项目。

阅读开源项目总是一个好的实践,我在入职伊始阅读了一小部分的Linux Kernel源码,这帮助我养成了良好的编码习惯,以及初步形成了编码规范的意识。在以上的开源项目中,OpenCV作为最流行的开源计算机视觉库,无论是源码还是其完备的CMake构建链,都十分值得深入了解,后续关于package-management的章节我将以OpenCV为例进行讲解。

CMake 是一种脚本语言

CMake的目标是成为一个跨平台的构建过程管理器,所以它定义了自己的脚本语言,具有特定的语法和内置特性。同时CMake又是一个软件程序,所以开发者需要调用它的脚本文件来解释和生成真正的构建文件。

CMakeLists.txt<project_name>.cmake都是CMake的脚本文件,但是通常使用CMakeLists.txt作为主脚本(就我目前短浅的经历而言<project_name>.name这种格式更多应用于第三方库管理中)。

  • CMakeLists.txt通常位于要构建的项目源代码中。

  • CMakeLists.txt可以位于任何它将起作用的应用程序、库的源码树的根目录下。

  • 如果工程由多个模块并且每个模块可以独自构建和编译,CMakeLists.txt可以被插入到各个子目录下。

  • .cmake文件也是CMake脚本文件,它运行cmake命令来准备环境预处理或分离可以写在CMakeLists.txt之外的任务。

  • .cmake也可用于为项目定义模块。这些模块可以是单独构建的库活复杂的多模块项目额外的方法(函数)。

CMake 命令

CMake命令类似于c++ /Java方法或函数,它们以列表的形式接受参数,并相应地执行某些任务。

CMake命令不区分大小写,具体的命令可以阅读CMake-Commands文档

一些常用的CMake命令:

  • message:打印信息

  • cmake_minimum_required:设置项目使用的CMake的最低版本

  • add_executable:添加可执行文件target

  • add_library:添加需要编译的target库

  • add_subdirectory:添加子目录

CMake编写规范:

  • 缩进不是强制的,但为了方便阅读,请尽量合理使用缩进。

  • CMake不需要使用‘;’作为语句的结束。

  • 所有条件语句需要以对应的结束命令结束。

CMake 环境变量

CMake的环境变量和普通变量相似,但有一下几点区别:

  • 作用范围:CMake环境变量的作用范围为整个CMake进程,并且不会被缓存(也即构建之后无法再修改)。

  • 引用形式:CMake环境变量可以通过$ENV{<variable>}的形式引用。

  • 初始化:CMake环境变量具有初始值,也可使用set()unset()修改,但只在CMake进程中生效,这点与系统环境变量不同。

cmake-env-variables列出了所有有特殊含义的CMake环境变量。

CMake 变量

CMake包含一系列预定义的变量用于定位源代码树和系统组分,与CMake命令不同,CMake变量区分大小写。

常用的CMake变量如下:

CMAKE_BINARY_DIR, PROJECT_BINARY_DIR :这两个变量内容一致,如果是内部编译,就指的是工程的顶级目录,如果是外部编译,指的就是工程编译发生的目录。 CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR:这两个变量内容一致,都指的是工程的顶级目录。 CMAKE_CURRENT_BINARY_DIR:外部编译时,指的是target目录,内部编译时,指的是顶级目录 CMAKE_CURRENT_SOURCE_DIR:CMakeList.txt所在的目录 CMAKE_CURRENT_LIST_DIR:CMakeList.txt的完整路径 CMAKE_CURRENT_LIST_LINE:当前所在的行 CMAKE_MODULE_PATH:如果工程复杂,可能需要编写一些cmake模块,这里通过SET指定这个变量 LIBRARY_OUTPUT_DIR, BINARY_OUTPUT_DIR:库和可执行的最终存放目录 PROJECT_NAME, CMAKE_PROJECT_NAME:前者是当前CMakeList.txt里设置的project_name,后者是整个项目配置的project_name

cmake-variables给出了所有预定义的CMake变量列表。

开发者也可自定义变量:

set_property (assign value to variable)

CMake 列表

CMake中的所有值都存储为字符串,但在特定的上下文中,字符串可以被视为列表。

# sets files to "a.txt;b.txt;c.txt"
set(files a.txt b.txt c.txt)

foreach(file ${files})
    message("Filename: ${file}")
endforeach()

使用CMake构建C++代码

前面的段落介绍了CMake脚本的核心原理,接下来将从一个"Hello CMake!"程序出发,由浅到深讲解CMake脚本基本构成。

Hello CMake!

假设我们有这样一个main.cpp

// main.cpp
#include <iostream>
int main() {
    std::cout<<"Hello CMake!"<<std::endl;
}

那么在Linux平台下的编译指令为:

g++ main.cpp -o cmake_hello

CMakeLists.txt则如下

# 设置最低cmake版本要求
cmake_minimum_required(VERSION 3.9.1)
# 定义project的名字
project(CMakeHello)
# 添加目标可执行文件
add_executable(cmake_hello main.cpp)

假设是内部编译:

cmake .
make

外部编译:

mkdir build && cd build
cmake ..
make

通常而言不建议使用内部编译,在OpenCV中就禁止了内部编译。

# Disable in-source builds to prevent source tree corruption.
if(" ${CMAKE_SOURCE_DIR}" STREQUAL " ${CMAKE_BINARY_DIR}")
  message(FATAL_ERROR "
FATAL: In-source builds are not allowed.
       You should create a separate directory for build files.
")
endif()

CMAKE_CXX_STANDARD

假如你的源代码中使用了更新的c++标准,那么你需要在CMakeLists.txt中指出。

set(CMAKE_CXX_STANDARD 14)

系统检查

假如你想要为多个平台构建:

  • 为不同平台单独生成可执行文件;

  • 向指定系统的源代码传递宏定义等。

那么你需要在构建过程中检查当前的构建系统:

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)

set(CMAKE_CXX_STANDARD 14)

# UNIX, WIN32, WINRT, CYGWIN, APPLE are environment variables as flags set by default system
if(UNIX)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(WIN32)
    message("This is a Windows System")
endif()

# or use MATCHES to see if actual system name 
# Darwin is Apple's system name
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    message("This is a ${CMAKE_SYSTEM_NAME} system")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    message("This is a Windows System")
endif()

add_executable(cmake_hello main.cpp)

大型代码库的实现通常是与系统无关的,它们使用宏只对正确的系统使用特定的方法。

宏帮助工程师有条件地构建代码,根据运行的系统配置放弃或包含某些方法。通过宏定义,开发者可以向源代码传递信息,进而实现不同的功能。

CMake的宏需要在宏的名字使用-D标志来表明这是一个宏定义。

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)
set(CMAKE_CXX_STANDARD 14)

# Darwin is Apple's system name
# or use MATCHES to see if actual system name 
if(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    # 定义CMAKEMACROSAMPL这个宏
    add_definitions(-DCMAKEMACROSAMPLE="Apple MacOS")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    add_definitions(-DCMAKEMACROSAMPLE="Windows PC")
endif()

add_executable(cmake_hello main.cpp)

然后在代码中就可以根据定义的宏进行不同的操作,以下代码将打印CMAKEMACROSAMPL这个宏的值:

#include <iostream>
#ifndef CMAKEMACROSAMPLE
    #define CMAKEMACROSAMPLE "NO SYSTEM NAME"
#endif
auto sum(int a, int b){
        return a + b;
}
int main() {
        std::cout<<"Hello CMake!"<<std::endl;
        std::cout<<CMAKEMACROSAMPLE<<std::endl;
        std::cout<<"Sum of 3 + 4 :"<<sum(3, 4)<<std::endl;
        return 0;
}

CMake文件组织

CMake允许开发者在根目录下新建自己的构建目录,并在该目录下进行构建,以下为基本的CMake构建文件:

mkdir build
cmake ..
ls -all
-rw-r--r--   1 onur  staff  13010 Jan 25 18:40 CMakeCache.txt
drwxr-xr-x  15 onur  staff    480 Jan 25 18:40 CMakeFiles
-rw-r--r--   1 onur  staff   4964 Jan 25 18:40 Makefile
-rw-r--r--   1 onur  staff   1256 Jan 25 18:40 cmake_install.cmake
make all

除了手动创建build目录在进行构建,CMake还可以通过使用构建参数来完成同样的工作:

cmake -H. -Bbuild
# H indicates source directory
# B indicates build directory

指定目标输出位置

cmake_minimum_required(VERSION 3.9.1)
project(CMakeHello)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_executable(cmake_hello main.cpp)
  • 通过设置CMAKE_RUNTIME_OUTPUT_DIRECTORYEXECUTABLE_OUTPUT_PATH变量的值,可以指定binary生成的位置。

  • 通过指定CMAKE_LIBRARY_OUTPUT_DIRECTORYLIBRARY_OUTPUT_PATH可以指定动态库生成的位置。

  • 通过指定CMAKE_ARCHIVE_OUTPUT_DIRECTORYARCHIVE_OUTPUT_PATH可以指定静态库生成的位置。

通常来说指定单一路径即可,但对于大型工程项目而言,需要编写更复杂的CMake脚本来进行生成目标位置管理。

Debug和Release配置

CMake建议根据需要实现具有多种配置选项的CMakeLists.txt,例如Debug和Release选项允许用户构建两周性能和使用场景不同的项目。CMake通过定义CMAKE_BUILD_TYPE变量来完成构建版本控制:

cmake -DCMAKE_BUILD_TYPE=Debug -H.  -Bbuild/Debug
cmake -DCMAKE_BUILD_TYPE=Release -H. -Bbuild/Release

需要为不同的版本创建不同的目录,这时CMakeLists.txt就可以使用CMAKE_BUILD_TYPE这个变量完成构建类型的检查:

if(${CMAKE_BUILD_TYPE} MATCHES Debug)
    message("Debug Build")
elseif(${CMAKE_BUILD_TYPE} MATCHES Release)
    message("Release Build")
endif()

[1] CMake不是一个跨平台软件,需要为不同的平台单独开发,这里与交叉编译无关。

Last updated