基礎(chǔ)知識
編譯 C++ 源代碼似乎是一個相當簡單的過程。讓我們創(chuàng)建一個簡單的小程序,例如經(jīng)典的 hello.cpp,如下:
chapter-01/01-hello/hello.cpp
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
現(xiàn)在,要獲得可執(zhí)行文件,我們需要運行一行命令。用文件名作為參數(shù)調(diào)用編譯器:
$ g++ hello.cpp -o a.out
代碼正確,編譯器會默默地幫我們生成機器能夠讀懂的二進制可執(zhí)行文件。我們可以通過文件名來調(diào)用它:
$ ./a.out
Hello World!
$
然而,隨著我們項目的發(fā)展,你很快就會明白,將所有代碼保存在一個文件中是不可能的。良好的代碼實踐建議文件應(yīng)該保持短小并且具有良好的組織結(jié)構(gòu)。手工編譯每個源代碼文件可能是一個令人厭倦并且極易出錯的過程。一定有更好的辦法。
CMake 是什么?
假設(shè)我們通過編寫一個腳本來自動化構(gòu)建,該腳本遍歷我們的項目樹并編譯所有內(nèi)容。為了避免任何不必要的編譯,我們的腳本將檢測源代碼在上次運行之后是否被修改過?,F(xiàn)在,我們想要一種方便的方式來管理每個文件傳遞給編譯器的參數(shù)——最好是我們希望根據(jù)可配置的標準來做。此外,我們的腳本應(yīng)該知道如何以二進制形式鏈接所有已編譯的文件,或者更好的是,如何構(gòu)建可以在更大的項目中重用并作為模塊合并的整體解決方案。
我們添加的特性越多,就越有可能得到一個成熟的解決方案。構(gòu)建軟件是一個通用的過程,可以包括許多不同的方面:
- 編譯成可執(zhí)行文件和庫
- 依賴管理
- 測試
- 安裝
- 打包
- 生成文檔
- 更多的測試
要想出一個真正模塊化的、功能強大的、適合各種用途的 C++ 構(gòu)建應(yīng)用程序,需要很長時間。但確實有人確實做到了。Kitware 的 Bill Hoffman 在 20 多年前實現(xiàn)了 CMake 的第一個版本。如你所料,它非常地成功。它現(xiàn)在已經(jīng)有很多功能并且得到了來自社區(qū)的支持。今天,CMake 正在被積極地開發(fā),并且已經(jīng)成為 C 和 C++ 程序員的行業(yè)標準。
采用自動化方式構(gòu)建代碼的想法比 CMake 出現(xiàn)早得多,所以很自然地,有很多解決方案:Make、Autotools、SCons、Ninja、Premake 等等。但是為什么 CMake 如今獨占鰲頭呢?
就個人主觀而言,我認為 CMake 做到了以下幾點:
- 它專注于支持現(xiàn)代編譯器和工具鏈
- CMake 真正做到了跨平臺——它支持在 Windows、Linux、macOS 和 Cygwin 上構(gòu)建
- 它可以為主流的 IDE 軟件生成項目文件,如:Microsoft Visual Studio、Xcode 和
Eclipse CDT。此外,它也是其他軟件(如 CLion)的項目模型。 - CMake 的操作建立在正確的抽象級別上——它允許你在項目中對文件進行分組,并支持目標的可復(fù)用性。
- 有大量的項目是用 CMake 構(gòu)建的,并提供了一個簡單的方法來將它們包含到你的項目中。
- CMake 將測試、打包和安裝作為構(gòu)建過程的固有部分。
- 過時的、沒用的特性被棄用,以保持 CMake 的精簡。
CMake 為構(gòu)建提供了一個統(tǒng)一而流暢的體驗。不管你是在 IDE 中構(gòu)建軟件,還是直接從命令行構(gòu)建軟件;更重要的是,它也照顧到后期的構(gòu)建階段。即使前面的所有環(huán)境都不同,你的持續(xù)集成/連續(xù)部署(CI/CD) 也可以輕松地使用相同的CMake配置,并使用單一標準構(gòu)建項目。
CMake 是如何起作用的?
你可能會有這樣的印象,CMake 是一個一端讀取源代碼,另一端生成二進制文件的工具——雖然這在原則上是正確的,但并不是全部。
事實上,CMake 并不能自己構(gòu)建任何東西——它依賴于系統(tǒng)中的其他工具來執(zhí)行實際的編譯、鏈接和其他任務(wù)。你可以將其視為構(gòu)建過程的協(xié)調(diào)者:它知道需要完成哪些步驟,最終目標是什么,以及如何為工作找到合適的工人和材料。
這個過程分為三個階段:
- 配置
- 生成
- 構(gòu)建
配置階段
這個階段 CMake 會從源代碼目錄(源碼樹)下讀取一些項目相關(guān)的信息,并為生成階段準備一個輸出目錄(構(gòu)建樹)。
CMake 首先創(chuàng)建一個空的構(gòu)建樹,并收集有關(guān)它工作環(huán)境的所有信息,例如:計算機體系結(jié)構(gòu)、可用的編譯器、鏈接器和歸檔器。此外,它檢查一個簡單的測試程序是否可以被正確編譯。
接下來,CMakeLists.txt 項目配置文件被解析并執(zhí)行(是的,CMake 項目是用 CMake 的編碼語言配置的)。這個文件是一個 CMake 項目的最低限度(源代碼文件可以稍后添加)。它告訴 CMake 關(guān)于項目結(jié)構(gòu)、目標和依賴項(庫和其他 CMake 包)的信息。在此過程中,CMake 將收集到的信息存儲在構(gòu)建樹中,例如:系統(tǒng)詳細信息、項目配置、日志和臨時文件,這些將用于下一步。具體來說,它將創(chuàng)建一個 CMakeCache.txt 文件來存儲已經(jīng)確定的變量(例如編譯器和其他工具的路徑),并在下次配置時節(jié)省時間。
生成階段
讀完項目配置之后,CMake 將為當前具體的工作環(huán)境生成一個構(gòu)建系統(tǒng)。構(gòu)建系統(tǒng)是一個為其他構(gòu)建工具提供的簡要工程模板(例如,GNU 的 Makefile 、Ninja 或者 Visual Studio IDE 的工程文件)。在這個階段,CMake 仍然可以通過計算生成器表達式對構(gòu)建配置進行一些最后的修改。
注意:
生成階段會在配置階段之后自動執(zhí)行。由于這個原因,當提到構(gòu)建系統(tǒng)的“配置”或“生成”時,本書和一些其他資料經(jīng)常指的是這兩個階段。要想顯式地只運行配置階段,可以使用 cmake-gui 程序。
構(gòu)建階段
為了生成項目中指定的最終工件,我們必須運行適當?shù)臉?gòu)建工具??梢酝ㄟ^ IDE 直接調(diào)用,或者使用 CMake 命令。接下來,這些構(gòu)建工具將執(zhí)行構(gòu)建步驟,使用編譯器、鏈接器、靜態(tài)和動態(tài)分析工具、測試框架、報告工具和其他任何您能想到的工具來生成目標。

還記得在理解基礎(chǔ)部分中我們的 hello.cpp 程序嗎?使用 CMake 也很容易構(gòu)建它。只需要加入以下的 CMakeLists.txt 并敲兩條簡單的命令:cmake -B buildtree 和 cmake --build buildtree,如下所示:
chapter01/01-hello/CMakeLists.txt:CMake 語言中的 Hello world
cmake_minimum_required(VERSION 3.20)
project(Hello)
add_executable(Hello hello.cpp)
下面是 Dockerized Linux 系統(tǒng)的輸出(注意,我們將在為不同平臺上安裝 CMake 部分討論 Docker):
root@5f81fe44c9bd:/root/examples/chapter01/01-hello# cmake
-B buildtree.
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /root/examples/
chapter01/01-hello/buildtree
root@5f81fe44c9bd:/root/examples/chapter01/01-hello# cmake
--build buildtree/
Scanning dependencies of target Hello
[ 50%] Building CXX object CMakeFiles/Hello.dir/hello.cpp.o
[100%] Linking CXX executable Hello
[100%] Built target Hello
接下來就是運行:
root@68c249f65ce2:~# ./buildtree/Hello
Hello World!
在這里,我們生成了一個 buildtree 目錄下的構(gòu)建系統(tǒng)。在此之后,我們執(zhí)行構(gòu)建階段,并生成我們最終能夠運行的二進制文件。
現(xiàn)在你看到了最終的結(jié)果,我敢肯定你一定會有很多問題:這個過程的先決條件是什么?這些命令是什么意思?為什么我們需要兩個文件?我如何編寫我自己的項目文件?不要擔心——這些問題將在下面的部分中得到解答。
獲取幫助
這本書將為您提供與當前版本的 CMake 相關(guān)的最重要的信息(在撰寫本書時是3.20)。為了向你提供最好的建議,我故意避免了任何已被棄用和不再被推薦的特性。我強烈建議至少使用 3.15 版本,它被認為是“現(xiàn)代 CMake”。如需要更多信息,你可以在網(wǎng)上找到最新的、完整的官方文檔 https://cmake.org/cmake/help/。
為不同平臺安裝 CMake
CMake 是一個用 C++ 編寫的跨平臺開源軟件。
這意味著你完全可以自己編譯它,然而,通常沒必要。因為你完全可以從官網(wǎng)(https://cmake.org/download/)上下載提前編譯好的二進制文件。
類 Unix 系統(tǒng)可以直接通過命令行來獲取安裝包。
注意
牢記 CMake 不會同編譯器一起被提供。如果你的系統(tǒng)還未安裝編譯器,你需要在使用 CMake 之前提供它,并確保將它的可執(zhí)行文件路徑添加到 PATH 環(huán)境變量了以使 CMake 能找到它。
學(xué)習本書的過程中,為了避免解決工具和依賴問題,我建議選擇第一種安裝方法:Docker。
讓我們來看看可以使用 CMake 的不同環(huán)境。
Docker
Docker(https://www.docker.com/)是一個提供操作系統(tǒng)級虛擬化的跨平臺工具,允許應(yīng)用程序以完整的包(稱為容器)的形式發(fā)布。