Skip to main content

CMake

1. 前言

想象一下我们有如下 C++ 程序 hello.cpp :

#include <iostream>
int main() {
std::cout << "hello world!" << std::endl;
}

我们需要在终端输入以下指令:

g++ hello.cpp -o hello

这时我们就可以生成可执行文件 hello 。但在实际应用场景中,我们可能会面临如下问题:

  • 项目中的 .h 文件和 .cpp 文件十分繁多。
  • 各 .h 文件 .cpp 文件的依赖关系十分复杂。
  • 多文件可能会出现重复编译的情况,拖慢编译速度。
  • ...

为了解决这些问题, Makefile 和 CMake 应运而生。

2. Makefile

在 Linux(Ubuntu) 平台上, make 工具可以通过以下方式安装:

sudo apt-get install make

makefile 文件描述了 C/C++ 工程的编译规则,可以用来指明源文件的编译顺序、依赖关系、是否需要重新编译等,自动化编译 C/C++ 项目(实际上也不止局限于 C/C++ 项目)。

我们可以考虑以下实例:

.
├── test.cpp
├── test.h
├── main.cpp
└── makefile

makefile如下:

CXXFLAGS = -std=c++17 -O2

main: main.o test.o
$(CXX) $(CXXFLAGS) -o $@ $^

main.o: main.cpp test.h
$(CXX) $(CXXFLAGS) -o $@ -c $<

test.o: test.cpp test.h
$(CXX) $(CXXFLAGS) -o $@ -c $<

.PHONY: clean
clean:
rm main.o test.o main

此处,我们不需要理解每一段代码的具体含义 —— 由于 makefile 文件的可读性较差,在日后的开发工作中,我们并不需要直接编写 makefile(之后我们可以看到,我们可以直接通过 CMake 工具生成 makefile )。

当然,目前仍然有许多开源项目使用 makefile 构建程序,因此了解如何使用 makefile 仍然是十分有必要的。大部分开源项目中的 makefile 支持以下指令:

  • 在 makefile 的同目录下输入 make ,就可以按照 makefile 所指定的编译规则自动编译整个工程。
  • 在 makefile 的同目录下输入 make clean ,可以删除编译生成的中间文件(如 .o 文件等)和可执行文件。
  • 在 makefile 的同目录下输入 make install (一般需要 root 权限),可以安装编译好的可执行文件(默认路径为 /usr/local/bin ,安装好后可以在命令行中直接调用)、库(默认路径为 /usr/local/lib ,安装好后可以直接链接)、头文件(默认路径为 /usr/local/include ,安装好后可以直接使用 #include <xxx.h> 引用)。

3. Cmake

makefile 存在以下问题:

  • 代码可读性极差,难以维护。
  • 语法复杂。
  • 跨平台性差。比如 Linux 平台下的 makefile 在 Windows 下可能无法工作,因为 Linux 的删除指令是 rm ,Windows 下的删除指令是 del 。
  • ...

因此,在目前的 C++ 工程中,我们多使用 CMake 来管理项目。CMake 是一种跨平台的编译工具,可以用较为简洁易读的语法描述 C++ 项目的编译、链接、安装过程等,在现代 C++ 项目上得到了广泛应用。

3-1. Linux

在 Linux(Ubuntu) 平台上, cmake 工具可以通过以下方式安装:

sudo apt-get install cmake

在 Linux 平台上,cmake 工具的使用一般分为两步

1)使用 CMakeLists.txt 生成 makefile 。

2)使用 makefile 自动化编译项目。

3-2. 第一个 CMake 项目

CMake 的项目文件叫做 CMakeLists.txt 。其放置位置如下图所示:

├── CMakeLists.txt
└── main.cpp

该项目的 CMakeLists.txt 中需要添加以下内容:

cmake_minimum_required(VERSION 3.5)
project(hello_world)
add_executable(hello_world main.cpp)

语法总结1:

  • cmake_minimum_required(VERSION 3.5) CMake 需要的最小版本。CMake 的版本可以在命令行中输入 cmake --version 获取,一般无强制要求。
  • project(<project_name>) 指定工程名称。
  • add_executable(<executable_name> <cppfile_name>) 生成可执行文件。

操作方法如下:

  1. 输入 cmake CMakeLists.txt ,目录下将会生成一个 Makefile 文件。
  2. 输入 make ,即可将源代码编译生成可执行文件。此处将会在与 CMakeLists.txt 相同目录的位置生成一个可执行文件 hello_word ,输入 ./hello_word 即可运行该可执行文件。
  3. 此外,输入 make help ,你也可以查看使用当前的 Makefile 所能执行的所有指令,例如 make clean (清楚生成的可执行文件和中间文件)。

3-3. 多文件

在平时的课程实验中,你可能习惯将所有的代码都写在一个 .cpp 文件中。但在实际工程中,为了方便代码复用和运行维护,通常将所有的文件划分为头文件( .h ),模块文件( .cpp )和主程序文件( .cpp )。

在本节中,我们将在头文件中声明一个计算平方根的函数,在模块文件中实现其主体,然后在主函数中调用它。项目结构如下:

.
├── CMakeLists.txt
├── include
│ └── sqrt.h
└── src
├── sqrt.cpp
└── main.cpp

Tips: 在 C++ 工程中,我们通常在 include/ 目录下放置头文件,在 src/ 目录下放置源文件。

该项目的 CMakeLists.txt 中需要添加以下内容:

# build part
cmake_minimum_required(VERSION 3.5)
project(sqrt)
set(SOURCES src/sqrt.cpp src/sqrt.cpp)
add_executable(sqrt ${SOURCES})
target_include_directories(sqrt PUBLIC ${PROJECT_SOURCE_DIR}/include)
# debug part
message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("SOURCES: ${SOURCES}")

语法总结2:

  • set(<variable> <value>) 设置变量。
  • target_include_directories(<project_name> <INTERFACE|PUBLIC|PRIVATE> <headfile_directory>) 指定所要包含的头文件。
  • message("your message") 在终端打印信息。

这里需要特别说明一下 CMake 中的变量使用。CMake 中的变量分为两种:

  • 显式变量:使用 set 指令定义的变量。
  • 隐式变量:通过其它指令隐式生成的变量。如该项目中会隐式生成 PROJECT_SOURCE_DIR 变量,默认为 CMakeLists.txt 所在的文件夹。

CMake 中有丰富的变量,用于定义工程目录、编译选项等,此处不做过多展开。想要了解更多,可以参考文末列出的参考文档。

3-4. 静态库和动态库

静态库

在此处,我们将上一小节中计算平方根的程序封装为静态库。项目结构如下:

.
├── CMakeLists.txt
├── include
│ └── sqrt.h
└── src
├── sqrt.cpp
└── main.cpp

该项目的 CMakeLists.txt 中需要添加以下内容:

cmake_minimum_required(VERSION 3.5)
project(sqrt)
# create static library
add_library(sqrt_static STATIC src/sqrt.cpp)
target_include_directories(sqrt_static PUBLIC ${PROJECT_SOURCE_DIR}/include)
# create executable
add_executable(sqrt src/main.cpp)
target_link_libraries(sqrt PRIVATE sqrt_static)

语法总结3:

  • add_library(<library_name> STATIC <cppfile_name>) 生成静态库。
  • target_link_libraries(<executable> <INTERFACE|PUBLIC|PRIVATE> <library_name>) 指定所要链接的库。

此处我们使用一种更为优雅的生成方式——我们期望将生成的静态库、可执行文件输出到 build 文件夹里,而不是和主项目混杂在一起。为此我们需要输入以下指令:

mkdir build
cd build
cmake .. # 使用的是上一层目录的 CMakeLists.txt,因此需要输入'..'
make

我们将会在 build/ 目录下看到静态库 libsqrt_static.a 和可执行文件 sqrt 。

动态库

项目目录结构同静态库一节。

该项目的 CMakeLists.txt 中需要添加以下内容:

cmake_minimum_required(VERSION 3.5)
project(sqrt)
# create shared library
add_library(sqrt_shared SHARED src/sqrt.cpp)
target_include_directories(sqrt_shared PUBLIC ${PROJECT_SOURCE_DIR}/include)
# create executable
add_executable(sqrt src/main.cpp)
target_link_libraries(sqrt PRIVATE sqrt_shared)

语法总结4:

  • add_library(<library_name> SHARED <cppfile_name>) 生成动态库。

同样按照上小节的方法生成项目。我们将会在 build/ 目录下看到动态库 libsqrt_shared.so 和可执行文件 sqrt 。

3-5. 使用第三方库

在实际的 C++ 工程中,我们可能需要链接一些开源的第三方库。CMake 也提供了相关的配置方式。我们以谷歌开发的单元测试框架googletest 为例:

googletest 的安装方法:

git clone https://github.com/google/googletest.git
# or git clone git@github.com:google/googletest.git
cd googletest
mkdir build
cd build
cmake ..
make
sudo make install

项目结构如下:

.
├── CMakeLists.txt
├── include
│ └── mysqrt.h
└── src
├── mysqrt.cpp
└── main.cpp

cmake_minimum_required(VERSION 2.6)
project(cmake_with_gtest)
set(SOURCES src/mysqrt.cpp src/main.cpp)
find_package(GTest)
message("GTEST_LIBRARIES: ${GTEST_LIBRARIES}")
message("GTEST_INCLUDE_DIRS: ${GTEST_INCLUDE_DIRS}")
include_directories(${GTEST_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/include)
add_executable(cmake_with_gtest ${SOURCES})
target_link_libraries(cmake_with_gtest ${GTEST_LIBRARIES} pthread)

语法总结5:

  • find_package(<package_name>) 查询第三方库的位置。若查找成功,则初始化变量 <package_name>_INCLUDE_DIR (第三方库的头文件目录)以及<package_name>_LIBRARIES (第三方库静态/动态库目录)。

CMake 支持的所有第三方库可以在 https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html 中找到。

CMake 还有很多强大的功能:

  • 设置 C++ 工程的语言标准、编译优化选项。
  • 层级文件之间 CMakeLists.txt 的相互调用,以便应用于目录层级更加复杂的 C++ 工程。
  • 对生成的库、可执行文件等进行安装。
  • ...

4. 最后

Official CMake Tutorial

cmake-examples

cmake-examples-Chinese

跟我一起写Makefile

为什么编译c/c++要用makefile,而不是直接用shell呢?