cmake 构建工具

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/

安装cmake

在Ubuntu下我们可以直接从软件源安装cmake命令,后续我们也都以Linux操作系统为例对cmake的使用进行介绍。

apt install cmake

在Windows下可以通过安装包的方式安装cmake工具。

下载地址:https://cmake.org/download/

cmake脚本语法

在使用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入门例子

这里我们以一个小工程为例,介绍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是可选的,表示添加的目录是系统头文件目录,用于屏蔽一些编译器的警告;BEFOREAFTER也是可选的,表示添加目录的相对位置,相对于默认的位置。INTERFACEPRIVATEPUBLIC分别用于指定构建目标的接口、私有和公共头文件路径;最后,我们还需要指定头文件的文件夹路径。

编译执行该工程时,我们应该执行如下命令。

# 进入build文件夹
cd build
# 编译
cmake .. && make
# 执行
./democpp

为什么我们要先切换到build目录然后执行cmake ..,而不是直接在工程根目录cmake .呢?这也是一种最佳实践,因为cmake执行过程中会生成很多临时文件,如果我们在工程根目录执行cmake命令,这些文件都会生成在工程根目录下,污染我们的目录结构,切换到build后再执行就可以避免这个问题,如果要将这些临时文件删除,直接删除build目录即可,此外这些临时文件也不应提交到版本控制系统,统一在build目录下执行,也方便了Git的.gitignore文件配置。

编译Debug和Release版本可执行文件

编译器会在Debug版本的可执行文件中插入调试信息,如果需要生成Debug版本的可执行文件,我们可以设置CMAKE_BUILD_TYPE变量为DebugRelease

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会自动处理路径相关的配置。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap