CMake简明手册(一)


对于小型的C++项目,我们可以手写Makefile来完成项目的构建,但是当项目变的越来越复杂,模块越来越多时,模块间的依赖关系将会变得非常复杂,手写Makefile的方式扩展性会变差。此时就可以用到CMake,CMake是一套支持多语言的跨平台的代码构建工具,很多大型的C++工程都用它来构建,比如PyTorch, LLVM等。本文并不是CMake教程,而是记录一些日常使用过程中经常使用的命令和容易出错的地方,以及一些概念模糊的知识点。如果想要阅读CMake的教程,网上有很多不错资料可以参考,详见文末的参考文献。

CMake基础

一个常见的项目文件组织方式如下图所示

当我们运行cmake命令时,该命令会读取项目根目录下的CMakeLists.txt,从而开始程序的构建。

mkdir build 
cd build
cmake -G "XCode" ../source

上面命中的第二个参数是Generator的名称,我们可以用-G显式的指定用什么Generator编译。CMake提供若干个Generator,比如Visual Studio,XCode等,如下所示

Generators
* Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Xcode                        = Generate Xcode project files.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.

使用不同的Generator,产生的结果是不同的,比如使用XCode构建,结果是一个.xcodeproj的工程文件,如果使用默认的”UNIX Makefiles”,构建结果则为Makefile文件。

-- Configuring done
-- Generating done
-- Build files have been written to: /some/path/build

当执行cmake时,可以看到上面的log,这说明CMake在构建程序时分为两个阶段,第一阶段是COnfiguring,该阶段CMake的任务是解析CMakeList.txt,处理模块间的依赖关系;第二个阶段是Generating,这个阶段会根据第一阶段生成好的依赖关系来构建程序,注意,此时并不会编译代码,而是生成项目文件(Makefile文件,如果使用XCode作为Generator,则会生成工程文件)。除了产生项目文件外,CMake在build目录下还会生成一个CMakeCache.txt的文件,用来缓存一些变量值,当再次运行cmake时可以直接使用。

有了项目文件后,我们就可以来真正的编译程序了

#build目录下
cmake --build . --config Release --target MyApp

此时CMake会根据当前平台选择对应的Tool Chain,比如Mac下使用Clang,在Linux下则使用GCC。一个构建executable的例子如下

cmake_minimum_required(VERSION 3.5)
project(build_executable LANGUAGES CXX)
set(src "main.cpp" "src/person.h")
add_executable(hello ${src})

Libraries和Linking

构建库文件的完整命令如下

add_library(targetName [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...]
)

从说明中可知,库在CMake中是一种Target,Target的概念很重要,有很多设计直接和它先关。CMake支持三种库类型,分别是静态库,动态库,以及MODULE。Module也是一种动态库,不同的地方在于它不参与最终的link,而是被用作runtime加载的动态库(dlopen)。

链接库的命令如下

target_link_libraries(targetName
<PRIVATE|PUBLIC|INTERFACE> item1 [item2 ...]
[<PRIVATE|PUBLIC|INTERFACE> item3 [item4 ...]]
...
)

这里需要注意下第二个参数,也就是链接方式,我们假设有两个库A和B,令A来链接B

  1. PRIVATE,A只有在实现上依赖B,因此B的接口会被隐藏,使用A的库不需要知道B的存在
  2. PUBLIC,A不仅在实现上依赖B,接口上也依赖,比如A的头文件中要include B的接口,这样使用A的库也可以看到B,并且也要链接B
  3. INTERFACE,A只依赖B的接口(头文件),而不依赖B的内部实现,可能A内部有自己的实现

来看一个具体的例子

cmake_minimum_required(VERSION 3.5)
project(static_libs LANGUAGES CXX)

set (src1 "src/Person.cpp")
set (hdr1 "src/Person.h")
set (src2 "src/Bar.cpp")
set (hdr2 "src/Bar.h")
set (src3 "src/Foo.cpp")
set (hdr3 "src/Foo.h")
set (src "src/main.cpp")

#1. genreate a static library
add_library(person STATIC ${src1} ${hdr1})
add_library(bar STATIC ${src2} ${hdr2})
add_library(foo STATIC ${src3} ${hdr3})
add_executable(app ${src})

#2. generate executable binary
target_link_libraries(person PRIVATE bar)
target_link_libraries(person INTERFACE foo)
target_link_libraries(app PUBLIC person)

例子中personfoo的链接是基于INTEFACE的。在Foo.h中只有一个接口

#pragma once
#include <iostream>
namespace Foo{
    void foo();
}

此时如果person内部有Foo:foo的实现,则person会用自己的实现,如果没有,则会用Foo.cpp中的实现

Variables

CMake中变量的定义为

set(varName value... [PARENT_SCOPE])
unset(varName value... [PARENT_SCOPE])

变量的类型为string,定义时不需要引号,为了避免歧义,尽量使用双引号

set(myVar a b c) # myVar = "abc"
set(myVar a;b;c) # myVar = "abc"
set(myVar "a b c") # myVar = "a b c"
set(myVar a b;c) # myVar = "abc"
set(myVar a "b c") # myVar = "ab c"

使用变量的语法是${myVar},CMake并不要求变量在使用前被定义,对于没有定义的变量,其值为空字符串。

CMake可以将一个Variable变成Cache Variable,语法如下

set(varName value... CACHE type "docstring" [FORCE])

被Cache的变量会保存到CMakeCache.txt中,它会存在于整个CMake构建周期中,并且对所有的CMake文件可见。这点和普通的variable不同,一般的variable的可见性和生命周期只限于该文件内。为了更好的理解,我们看一个例子

├── CMakeLists.txt
├── main.cpp
├── src1
│   └── CMakeLists.txt
└── src2
    └── CMakeLists.txt

在上述工程中,我们有一个root CMakeLists文件,和两个子CMakeLists文件,各自内容如下


cmake_minimum_required(VERSION 3.5)
project(build_executable LANGUAGES CXX)

set(src "main.cpp")
set(ROOT "ROOT")
set(ROOT-CACHED "ROOT-CACHED" CACHE STRING "")
# set(VAR1 "var1" CACHE STRING "")
add_subdirectory(src1)
add_subdirectory(src2)
message(STATUS "[ROOT] ROOT: ${ROOT}")
message(STATUS "[ROOT] ROOT-CACHED: ${ROOT-CACHED}")
message(STATUS "[ROOT] SRC1: ${SRC1}")
message(STATUS "[ROOT] SRC2: ${SRC2}")
message(STATUS "[ROOT] SRC1-CACHED: ${SRC1-CACHED}")
message(STATUS "[ROOT] SRC2-CACHED: ${SRC2-CACHED}")

add_executable(hello ${src})


message(STATUS "[SRC1] ROOT: ${ROOT}")
set(ROOT "SRC1-ROOT")
set(ROOT-CACHED "SRC1-ROOT-CACHED" CACHE STRING "")
set(SRC1 "SRC1")
set(SRC1-CACHED "SRC1-CACHED" CACHE STRING "")
message(STATUS "[SRC1] ROOT: ${ROOT}")
message(STATUS "[SRC1] ROOT-CACHED: ${ROOT-CACHED}")
message(STATUS "[SRC1] SRC2-CACHED: ${SRC2-CACHED}")


message(STATUS "[SRC2] ROOT: ${ROOT}")
set(ROOT "SRC2-ROOT")
set(ROOT-CACHED "SRC2-ROOT-CACHED" CACHE STRING "")
set(SRC2 "SRC2")
set(SRC2-CACHED "SRC2-CACHED" CACHE STRING "")
message(STATUS "[SRC2] ROOT: ${ROOT}")
message(STATUS "[SRC2] ROOT-CACHED: ${ROOT-CACHED}")
message(STATUS "[SRC2] SRC1-CACHED: ${SRC1-CACHED}")

用CMake编译,输出如下

-- [SRC1] ROOT: ROOT
-- [SRC1] ROOT: SRC1-ROOT
-- [SRC1] ROOT-CACHED: ROOT-CACHED
-- [SRC1] SRC2-CACHED: 
-- [SRC2] ROOT: ROOT
-- [SRC2] ROOT: SRC2-ROOT
-- [SRC2] ROOT-CACHED: ROOT-CACHED
-- [SRC2] SRC1-CACHED: SRC1-CACHED
-- [ROOT] ROOT: ROOT
-- [ROOT] ROOT-CACHED: ROOT-CACHED
-- [ROOT] SRC1: 
-- [ROOT] SRC2: 
-- [ROOT] SRC1-CACHED: SRC1-CACHED
-- [ROOT] SRC2-CACHED: SRC2-CACHED

从上面的结果我们能得到下面一些结论

  1. 父文件中的变量对子文件可见,反之则不行
  2. 子文件中的Cache变量对父文件可见,对所有文件可见
  3. 在子文件中修改父文件中的一般变量,仅对子文件有效,对父文件无效
  4. 在子文件中修改父文件的Cache变量,对子文件无效,对父文件无效,除非使用FORCE

实际上Cache变量和普通变量根本上是两个不同的变量,很多时候我们会为它们取相同的名字,这造成了很多非常困惑的问题,比如下面情况

Resources