CMake是一个开源的跨平台C/C++构建工具,目前可以说是C/C++工程构建的事实标准。在Linux下编译项目使用cmake要比直接使用make简单直观得多;在Windows下,大部分IDE也都支持集成cmake来构建项目,包括最新的Qt也放弃了qmake转为使用cmake了。
cmake并不直接编译代码,它使用cmake脚本来描述项目的构建、编译和安装规则,然后生成适用于不同编译系统的构建脚本。举例来说,具体执行时,cmake
命令会先根据CMakeLists.txt
脚本生成Makefile(通常用于Linux下)或Sln工程(用于Microsoft Visual Studio),然后我们还需要再执行对应平台的构建命令来编译代码。
官方网址:https://cmake.org/
在Ubuntu下我们可以直接从软件源安装cmake命令,后续我们也都以Linux操作系统为例对cmake的使用进行介绍。
apt install cmake
在Windows下可以通过安装包的方式安装cmake工具。
下载地址:https://cmake.org/download/
在使用cmake前,我们需要了解一些cmake脚本语法,cmake脚本是cmake定义一种DSL,它有自己的命令和控制流程语句。
注释:cmake脚本中,使用#
开头的为注释。
# 指定项目名字、版本和语言
project(democpp VERSION 1.0.1 LANGUAGES CXX)
命令:cmake脚本会调用一系列的命令来完成工程构建,例如message()
命令用于打印一些调试信息。命令都有自己的参数格式,我们需要传递正确的参数命令才能正确工作。
message("Hello, world!")
变量:cmake脚本中,可以使用set()
命令定义变量,使用${}
语法读取变量。
# 定义变量
set(HELLO hello)
# 使用变量
message("Variable HELLO: ${HELLO}")
set()
命令也支持设置数组类型,下面例子中创建了一个包含3个元素的数组FILES
。
set(FILES a.cpp b.cpp c.cpp)
除了在脚本中使用set()
命令,cmake中也可以使用命令行参数指定变量。
cmake -D<variable>=<value> <工程路径>
控制流程:cmake脚本支持编写判断、循环逻辑。不过注意cmake脚本是很难调试的,我们尽量不要编写太过复杂的脚本逻辑。
下面实现了一个判断逻辑。
set(HELLO hello)
if (${HELLO} STREQUAL "hello")
message("True")
else()
message("False")
endif()
下面实现了一个循环逻辑。
set(FILES a.cpp b.cpp c.cpp)
foreach(FILE ${FILES})
message(${FILE})
endforeach()
这里我们以一个小工程为例,介绍cmake最基本的使用方法,工程目录结构如下。
├── CMakeLists.txt # cmake构建脚本
├── build # 输出目录
├── include # 头文件目录
│ └── myfunc.h
└── src # C++源代码目录
├── main.cpp
└── myfunc.cpp
cmake构建脚本是由一系列cmake命令构成的,内容如下。
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(democpp VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
add_executable(${PROJECT_NAME}
src/myfunc.cpp
src/main.cpp
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
cmake_minimum_required
命令指定了构建此项目所需的cmake最低版本号,project
指定了项目名、版本(1.0)和语言(使用C++)。
之后的几个set
命令用于设置变量,cmake支持一些内置的变量,我们可以读取内置变量也可以设置内置变量来配置cmake的构建行为;此外我们也可以自定义变量,供其他位置引用,cmake脚本中可以用${}
的语法引用变量。
CMAKE_CXX_STANDARD
用于指定C++版本,这里指定为C++11,如果不指定,默认使用编译器的默认值。CMAKE_BINARY_DIR
用于指定cmake生成的Makefile、项目文件和其他构建过程中生成的中间文件的存放目录。通常最佳实践是将这些文件生成在一个独立的文件夹中,比如build
文件夹,以避免源代码目录的混乱。这里我们还用到了CMAKE_CURRENT_SOURCE_DIR
变量,表示当前处理的CMakeLists.txt
文件所在的目录的路径。EXECUTABLE_OUTPUT_PATH
用于指定生成的可执行文件的输出目录,我们这里将其指定到CMAKE_BINARY_DIR
的同一级目录,即build
文件夹。add_executable
命令用于指定编译目标为可执行文件,同时指定需要编译的源代码文件,语法格式如下。
add_executable(<target> [source1] [source2] [...])
它接收构建目标名和文件列表作为参数。注意add_executable
不支持指定文件夹,如果你的源代码文件有很多,可以使用file
命令的GLOB功能来生成文件列表。
file(GLOB SOURCE_FILES "src/*.cpp")
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_include_directories
命令用于指定头文件,语法格式如下。
target_include_directories(
<target>
[SYSTEM]
[BEFORE]
[AFTER]
[INTERFACE]
[PRIVATE | PUBLIC | INTERFACE]
[directory1]
[directory2]
[...]
)
target
是构建目标名;SYSTEM
是可选的,表示添加的目录是系统头文件目录,用于屏蔽一些编译器的警告;BEFORE
和AFTER
也是可选的,表示添加目录的相对位置,相对于默认的位置。INTERFACE
,PRIVATE
,PUBLIC
分别用于指定构建目标的接口、私有和公共头文件路径;最后,我们还需要指定头文件的文件夹路径。
编译执行该工程时,我们应该执行如下命令。
# 进入build文件夹
cd build
# 编译
cmake .. && make
# 执行
./democpp
为什么我们要先切换到build
目录然后执行cmake ..
,而不是直接在工程根目录cmake .
呢?这也是一种最佳实践,因为cmake执行过程中会生成很多临时文件,如果我们在工程根目录执行cmake命令,这些文件都会生成在工程根目录下,污染我们的目录结构,切换到build
后再执行就可以避免这个问题,如果要将这些临时文件删除,直接删除build目录即可,此外这些临时文件也不应提交到版本控制系统,统一在build目录下执行,也方便了Git的.gitignore
文件配置。
编译器会在Debug版本的可执行文件中插入调试信息,如果需要生成Debug版本的可执行文件,我们可以设置CMAKE_BUILD_TYPE
变量为Debug
或Release
。
set(CMAKE_BUILD_TYPE Debug)
该选项也经常通过命令行参数来指定。
cmake -DCMAKE_BUILD_TYPE=Debug ..
前面例子中我们配置过项目的名字、版本等信息,实际上cmake的一些配置可以通过配置头加入编译的目标中。
以版本号为例,我们在CMakeLists.txt
中加入如下配置。
project(democpp VERSION 1.0.1 LANGUAGES CXX)
configure_file(config.h.in config.h)
configure_file
命令指定了一个config.h.in
,它相当于一个模板文件,cmake在构建时会使用cmake脚本的变量替换其中的占位符,输出到config.h
中。
config.h.in
#define PROJECT_VERSION "v@PROJECT_VERSION@"
在config.h.in
中我们需要使用@xxx@
语法来引用变量,这里我们引入cmake内置的PROJECT_VERSION
变量,它实际上就是project()
命令中配置的版本号。
main.cpp
#include <iostream>
#include "config.h"
int main(void)
{
std::cout << PROJECT_VERSION << std::endl;
return 0;
}
在main.cpp
中,我们直接引入这个宏定义即可。
前面我们使用过add_executable
,它用于编译可执行文件,实际上我们经常还需要编译库文件。下面例子中,我们编译一个简单的静态库mylib
,文件目录结构如下。
├── CMakeLists.txt
├── build
├── include
│ └── mylib.h
└── src
└── mylib.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(mylib VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
add_library(mylib src/mylib.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
cmake脚本中,我们使用了add_library
,该命令用于编译库文件,它的格式如下。
add_library(target [STATIC | SHARED | MODULE] [source1] [source2] [...])
其中,[STATIC | SHARED | MODULE]
用于指定库的类型,默认类型为STATIC
静态库,如果想要编译为动态链接库,指定SHARED
即可。
配置好后,我们即可在build
目录中执行编译,即可生成对应的库文件。
前面我们编译了库文件,我们得到了静态库libmylib.a
或者动态链接库libmylib.so
,这里我们再写一个小例子,在前面的democpp
项目中引入该库。
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(democpp VERSION 1.0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
add_executable(${PROJECT_NAME}
src/myfunc.cpp
src/main.cpp
)
target_link_libraries(${PROJECT_NAME} PUBLIC /home/ubuntu/mylib/build/libmylib.so)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
/home/ubuntu/mylib/include
)
这里我们使用了target_link_libraries
命令,它用于指定当前编译目标所需要的静态库或动态链接库,除了指定库文件以外,我们还需要指定库的头文件。这样配置后,我们的代码中调用库中的函数,就可以正确编译执行了。
注意这里我们的库工程mylib
和当前的democpp
是两个独立的工程,libmylib.so
也不在库文件的默认查找路径中,因此我们只能使用绝对路径引入库和头文件;如果这里引入的是系统库或者cmake工程子工程中的编译目标,则可以直接使用库的名字,cmake会自动处理路径相关的配置。