1. 为什么使用CMake
如果你曾经维护过软件包的构建与安装过程,你应该会对CMake
比较感兴趣,CMake
是一个开源的工程构建管理工具,它借助一个名为CMakeLists.txt
的文本配置文件让开发者可以指定构建参数,从而生成标准的构建文件,例如Unix
的Makefile
文件或者Visual Stuido
,Applet Xcode
的工程文件。对于构建过程中跨平台
,系统检测
,用户自定义构建
等复杂概念,CMake
以一种非常简单的方式帮助开发者完成这些事。
当你在同一平台不同的机器上开发同一个项目时,CMake提供如下功能:
- 在你工程构建编译时自动寻找需要的
程序
,库
,以及相关的头文件
,搜索查找的依据是你系统的环境变量以及windows的注册表。 - 分离源码目录和构建目录,Cmake可以将构建目录和源码目录分离,这样开发者可以很安全的删除构建目录,而不用担心影响源码目录
- 提供在配置时选择组件的能力,例如你有多个VTK库提供选择,CMake提供一种很简单的方式让用户选择使用哪一个库参与构建。
- 能够从简单的文本文件自动生成workspace和projects,这个对于需要多个程序或者测试用例的系统是很方便的,特别是需要手动创建多个工程。
- 能够自动生成文件依赖,支持并行构建
- 能够在动态和静态构建之间很容易的切换
2. GETTING STARTED
2.1 安装CMake
2.2 从源码构建安装CMake
2.3 CMake的基本用法和语法
使用CMake是很简单的,通过在工程目录中创建一个或者多个CMakeLists.txt文件来控制整个构建过程,CMakeLists文件使用CMake语言来描述整个工程项目,CMake语言就是一系列的命令,每一个命令按着其在CMakeLists文件中出现的顺序进行求值运算。命令格式如下:
Command (args...)
其中Command
是命令的名字,args
是用空格分离的参数列表(空白参数应该用双引号括起来),CMake 命令从2.2版本开始不区分大小写,老版本只识别全部大写的命令。
CMake 支持的变量很简单,既可以是一个字符串也可以是一个字符串列表,变量可以通过${VAR}
语法被引用,多个参数可以使用set
命令打包成一个list
。例如set(Foo a b c)
这个命令会将a b c
保存到Foo
中,如果Foo
被传递给另一个命令command(${Foo})
这等价于command(a b c)
。如果你想将一个参数列表作为一个参数传递给某个命令,你只需要在引用时,用双引号括起来。例如:command("${Foo}")
它等价于command("a b c")
.
系统环境变量和注册表的值可以直接传递给CMake,如果想要访问系统变量可以使用$ENV{VAR}
2.4 CMake hello
让我们通过CMakeList文件来完成一个简单的Hello工程吧
cmake_minimum_required(VERSION 3.14)
project(hello)
set(CMAKE_CXX_STANDARD 11)
add_executable(Hello hello.cpp)
第一行指明了CMake的最小版本号,第二行指明工程的名称,第三行设置工程编译的C++标准,这里我们设置成C++11标准,第四行设置添加了一个目标,该目标是一个名为Hello的可执行程序,后面紧接着是生成这个程序需要的源码文件。
如果你的项目中有多个源码文件你可以这样写
add_executable(Hello hello.cpp utils.cpp other_source.cpp)
add_executable
命令仅仅是众多CMake命令中的一个,我们看一个更复杂的例子
cmake_minimum_required(VERSION 3.14)
project(Hello)
set(CMAKE_CXX_STANDARD 11)
set(HELLO_SRC hello.cpp utils.cpp other_source.cpp)
if (WIN32)
set(HELLO_SRC ${HELLO_SRC} WinSupport.cpp)
else ()
set(HELLO_SRC ${HELLO_SRC} UnixSupport.cpp)
endif()
add_executable(Hello ${HELLO_SRC})
在这个例子中我们使用set
命令将源码文件打包到变量HELLO_SRC
中,根据目标平台的不同可以使用if
命令来将不同的源码文件选择加入。
2.5 如何运行CMake
一旦你的系统成功安装了CMake,你就可以愉快的使用它了,CMake在工程构建的时候有两个主要的目录,源码目录
和build目录
(构建目录
),源码目录
是你的源代码存放的目录,在这里CMake能够找到所需要的CMakeLists.txt
文件,build目录
是你想要CMake在构建时,将中间的二进制结果文件,库文件,以及可执行文件存放到哪里,通常情况下CMake不会在源码目录
中写入任何文件,只会在build目录
中进行写入,如果你想要将二进制文件目录和源码文件目录设置成同一目录,这样的构建称之为源内构建
,两者不同,称之为源外构建
。CMake同时支持两种构建方式,这意味着你完全可以配置你的build目录
在源码目录
之外,这样很容易删除全部构建过程中生成的中间文件而不会影响源码文件。同时我们也可以针对不同的构建目标设置不同的构建目录,让他们共享同一份源码。
2.5.1 从命令行中运行CMake
命令行中运行CMake通常按着如下命名签名,一般需要指定源码目录,build目录,和生成的的目标编译工程。
cmake [<option>] <path-to-source>
先看一个简单的例子,首先我们需要创建Build目录,然后在该目录下执行 camke <path-to-source>
命令
$ mkdir build; cd build
$ cmake ../src
这样camke就会默认以当前目录作为build目录
,后面的参数是源码目录
。通常情况下目标编译系统不是必须的,CMake会检查CMAKE_GENERATOR环境变量作为默认的目标编译系统,当然我们也可以手动指定
cmake -G "generator-name" ../src
其中gennerator-name是目标平台的名称有如下选择
- Borland Makefiles
- MSYS Makefiles
- MinGW Makefiles
- NMake Makefiles
- NMake Makefiles JOM
- Unix Makefiles
- Watcom WMake
- Ninja
- Visual Studio 14 2015
- Visual Studio 15 2017
- Visual Studio 16 2019
- Green Hills MULTI
- Xcode
- CodeBlocks
- CodeLite
- Eclipse CDT4
- Kate
- Sublime Text 2
同时我们显示的指定编译目录调用如下命令
cmake -S src -B build -G Ninja -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/clang -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/clang++
向CMake传递参数
让我们再来回顾一下之前的例子
cmake_minimum_required(VERSION 3.14)
project(Hello)
set(CMAKE_CXX_STANDARD 11)
set(HELLO_SRC hello.cpp utils.cpp test2.cpp)
if (WIN32)
set(HELLO_SRC ${HELLO_SRC} WinSupport.cpp)
else ()
set(HELLO_SRC ${HELLO_SRC} UnixSupport.cpp)
endif()
add_executable(Hello ${HELLO_SRC})
我们看到这里我们使用了条件编译,那么这个WIN32
条件该如何传递给CMake呢?我们可以使用如下命令
-D<var>:<type>=<value> -D<var>=<value>
cmake -S . -B build -DWIN32:BOOL=TRUE
指定编译器编译参数
有些系统可能安装了多个编译器,特别是某些项目可能需要指定某个特殊的编译器,此时我们可以调用如下命令
-DCMAKE_CXX_COMPILER=编译器路径
-DCMAKE_C_COMPILER=编译器路径
-DCMAKE_CXX_FLAGS
-DCMAKE_C_FLAGS
2.8 执行CMake进行构建任务
当执行CMake生成目标工程时,剩下来的任务就是构建,如果你生成的是VS工程,你可以使用VSStudio打开,这里我们不做讨论,如果你生成的是Makefile工程,我们可以使用CMake进行接下的构建任务
camke --build <dir> [<option>] [-<build-tool-option>]
-
--build <dir>
: 工程编译的二进制文件存放目录,这个目录必须设置而且必须放到第一位 -
--parallel [<jobs>, -j [jobs]]
:最大并发编译线程数 -
--clean-first
:编译前先clean -
--target <tgt> ... , -t <tgt>
:替换默认的编译目标任务,例如--target clean
只是执行clean任务。