从LevelDB CMakeFiles.txt开始学习CMake:C++项目构建系统入门

最近开始阅读LevelDB源码,希望通过源码理解一个优秀 C++ 项目的工程设计。当我打开LevelDB项目目录时,发现CMakeLists.txt几乎贯穿整个项目,但是我之前并没有怎么使用过CMake。CMakeLists.txt的内容让我有点无法理解。于是产生了几个问题:

  • CMake是什么?
  • 为什么C++项目需要CMake?
  • CMake和Make有什么区别?
  • CMakeLists.txt如何组织一个大型项目?

带着这些问题,开始学习CMake。

C++项目为什么需要构建系统?

假设我们有一个main.cpp文件:

1
2
3
4
5
6
#include <iostream>

int main()
{
    std::cout<<"Hello";
}

编译:

1
g++ main.cpp -o app

非常简单。但随着项目变大,源码可能牵涉几十个甚至几百个文件。此时,如果再使用:

1
g++ xxx.cc xxy.cc xxz.cc

就不太方便且不可维护,最好能自动管理编译过程。

Make是什么?

最早出现的是Make。Make通过Makefile描述如何编译、文件依赖、编译顺序。例如:

1
2
app:
  g++ main.cpp math.cpp -o app

执行:

1
make

就会生成可执行文件app

Make解决了哪些文件需要重新编译的问题,假如我们只修改了math.cpp就只要重新编译math.cpp,而不是全部重新编译。

为什么不用Make?

不同平台我们使用的编译器可能不一样,比如在Linux下我们用的是gcc,Windows下用的是cl.exe,macOS下用的是clang,这就导致我们需要维护多个Makefile。

另外,项目可能有非常多的第三方依赖,手写非常麻烦。

CMake是什么?

CMake是生成构建文件的工具。

图表 代码
flowchart TD
    A[CMakeLists.txt] --> B[CMake]
    B --> C[Makefile]
    C --> D[make]
    D --> E[程序]
flowchart TD
    A[CMakeLists.txt] --> B[CMake]
    B --> C[Makefile]
    C --> D[make]
    D --> E[程序]
flowchart TD
    A[CMakeLists.txt] --> B[CMake]
    B --> C[Makefile]
    C --> D[make]
    D --> E[程序]
1
2
3
4
5
flowchart TD
    A[CMakeLists.txt] --> B[CMake]
    B --> C[Makefile]
    C --> D[make]
    D --> E[程序]

CMake不负责编译,真正编译的是gcc/clang/cl.exe。

第一个CMake项目

创建:

  • HelloCMake
    • CMakeLists.txt
    • main.cpp

main.cpp:

1
2
3
4
5
6
#include <iostream>

int main() {
    std::cout << "Hello CMake" << std::endl;
    return 0;
}

CMakeLists.txt:

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.10)

project(HelloCMake)

add_executable(
    hello
    main.cpp
)

构建:

1
2
3
4
mkdir build
cd build
cmake ..
cmake --bulid .

生成了hello.exe。

理解CMake核心概念

Target

开始阅读LevelDB CMakeLists.txt时,最重要的概念是Target。CMake并不是告诉编译器执行哪些命令,而是描述项目有哪些构建目标,以及目标之间如何依赖。

例如:

1
add_library(leveldb)

创建了一个Target leveldb,它不是leveldb.cpp也不是leveldb文件夹,而是一个逻辑对象。这个Target可以有源文件、头文件路径、编译参数、链接依赖。最终生成leveldb.lib(Windows)或libleveldb.a(Linux)。

add_library

在LevelDB中:

1
add_library(leveldb ")

表示创建一个名为leveldb的库目标。后续:

1
2
3
4
5
target_sources(
    leveldb
    PRIVATE
    ${LEVELDB_SOURCES}
)

给它添加源码。

target_include_directories

target_include_directories告诉CMake头文件在哪里。

1
2
3
4
5
target_include_directories(leveldb
  PUBLIC
    $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

一个库通常不会孤立存在,target_link_libraries描述了模块之间如何链接。

例如:

1
target_link_libraries(leveldb Threads::Threads)

表明LevelDB使用线程库。

PRIVATE和PUBLIC

图表 代码
flowchart TD
    A[Application] --> B[LevelDB]
    B --> C[Thread Library]
flowchart TD
    A[Application] --> B[LevelDB]
    B --> C[Thread Library]
flowchart TD
    A[Application] --> B[LevelDB]
    B --> C[Thread Library]
1
2
3
flowchart TD
    A[Application] --> B[LevelDB]
    B --> C[Thread Library]

Application是否需要Thread Library?由关键字决定。

PRIVATE表示只属于当前Target,也就是Application不知道Thread Library。

PUBLIC表示自己需要,同时暴露给使用者。

例如:

1
2
3
4
5
target_include_directories(
    leveldb
    PUBLIC
    include
)

表示使用LevelDB的程序,也需要这个头文件目录。

find_package

大型项目不会写C:\xxx\pthread.lib, 而是:

1
find_package(Threads REQUIRED)

让CMake自动寻找,成功后Threads::Threads就是一个Target。

然后:

1
2
3
4
target_link_libraries(
    leveldb
    Threads::Threads
)

add_subdirectory

大型项目通常每个模块都有自己的CMakeLists.txt,主文件add_subdirectory表示继续处理子模块。

例如:

1
add_subdirectory("third_party/googletest")

推荐

CMake Tutorial

Learn Makefiles With the tastiest examples

CMake: The Comprehensive Guide to Managing and Building C++ Projects (From Basics to Mastery)

Modern CMake for C++: Effortlessly build cutting-edge C++ code and deliver high-quality solutions , Second Edition

Professional CMake: A Practical Guide


相关内容

请作者喝杯咖啡!
AndyFree96 支付宝支付宝
AndyFree96 微信微信