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
- PRIVATE,A只有在实现上依赖B,因此B的接口会被隐藏,使用A的库不需要知道B的存在
- PUBLIC,A不仅在实现上依赖B,接口上也依赖,比如A的头文件中要
include
B的接口,这样使用A的库也可以看到B,并且也要链接B - 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)
例子中person
对foo
的链接是基于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
从上面的结果我们能得到下面一些结论
- 父文件中的变量对子文件可见,反之则不行
- 子文件中的Cache变量对父文件可见,对所有文件可见
- 在子文件中修改父文件中的一般变量,仅对子文件有效,对父文件无效
- 在子文件中修改父文件的Cache变量,对子文件无效,对父文件无效,除非使用
FORCE
实际上Cache变量和普通变量根本上是两个不同的变量,很多时候我们会为它们取相同的名字,这造成了很多非常困惑的问题,比如下面情况