天天看點

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

在軟體開發的世界裡,建構系統扮演着至關重要的角色,它不僅決定了項目的建構效率,還直接影響到團隊協作的流暢度。對于許多 C++ 開發者而言,CMake 因其強大的功能和廣泛的相容性成為了建構自動化流程的首選工具。

原文連結:https://journal.hexmos.com/cmake-survial-guide/

聲明:未經允許,禁止轉載。

作者 | Shrijith Venkatramana翻譯 | 鄭麗媛出品 | CSDN(ID:CSDNnews)

最近我一直在用 C++ 處理一些程式設計挑戰,其中管理 C++ 項目的一個重要方面就是依賴管理。

如今,我們在很多程式設計生态系統中享受着即時包管理器的便利:

● 在 Node.js/JavaScript 中使用 npm

● 在 Rust 中使用 cargo

● 在 Python 中使用 pip

而在 C++ 中,盡管有像 Conan 這樣的包管理器,但處理實際項目時,你通常會發現 CMake 是繞不開的選擇。是以如果你想在 C++ 生态系統中工作,學習如何使用 CMake 就不是可選項,而是必修課。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”
“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

CMake 到底是什麼,為什麼要學它?

CMake 是一個跨平台的建構系統生成器。跨平台這一點非常重要,因為 CMake 能夠在一定程度上抽象出不同平台之間的差異。

例如在類 Unix 系統上,CMake 會生成 makefile 檔案,然後用這些檔案來建構項目。而在 Windows 系統中,CMake 會生成 Visual Studio 項目檔案,随後用于建構項目。

需要注意的是,不同平台通常都有各自的編譯和調試工具鍊:Unix 使用 gcc,macOS 使用clang 等等。

在 C++ 生态系統中,另一個重要方面是能同時處理可執行檔案和庫。

可執行檔案可基于以下不同因素:

● 目标 CPU 架構

● 目标作業系統

● 其他因素

對于庫來說,連結方式也有不同的選擇(連結是指在代碼中使用另一個代碼庫的功能,而無需了解其具體實作):

● 靜态連結

● 動态連結

我曾在一些内部原型項目中,需要調用底層作業系統 API 來執行某些任務,唯一可行的高效方法就是基于一些 C++ 庫來進行建構。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

CMake 是如何工作的:三個階段

1. 配置階段

CMake 會讀取所有的 CMakeLists.txt 檔案,并建立一個中間結構來确定後續步驟(如列出源檔案、收集要連結的庫等)。

2. 生成階段

基于配置階段的中間輸出,CMake 會生成特定平台的建構檔案(如在 Unix 系統上生成 makefiles 等)。

3. 建構階段

使用特定平台的工具(如 make 或 ninja)來建構可執行檔案或庫檔案。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

一個簡單的 CMake 項目示例(Hello World!)

假設你有一個用于計算數字平方根的 C++ 源檔案。

tutorial.cxx

// A simple program that computes the square root of a number              #include <cmath>              #include <cstdlib> // TODO 5: Remove this line              #include <iostream>              #include <string>                  // TODO 11: Include TutorialConfig.h                  int main(int argc, char* argv[])              {              if (argc < 2) {              // TODO 12: Create a print statement using Tutorial_VERSION_MAJOR              // and Tutorial_VERSION_MINOR              std::cout << "Usage: " << argv[0] << " number" << std::endl;              return 1;              }                  // convert input to double              // TODO 4: Replace atof(argv[1]) with std::stod(argv[1])              const double inputValue = atof(argv[1]);                  // calculate square root              const double outputValue = sqrt(inputValue);              std::cout << "The square root of " << inputValue << " is " << outputValue              << std::endl;              return 0;              }           

CMakeLists.txt

project(Tutorial)              add_executable(tutorial tutorial.cxx)           

上述兩行是生成一個可執行檔案所需的最少指令。理論上,我們還應該指定 CMake 的最低版本号,省略 CMake 會預設使用某個版本(暫時跳過這部分)。

嚴格來說,project 指令并非必需,但我們還是保留它。是以最重要的代碼行是:

add_executable(tutorial tutorial.cxx)           

這行代碼指定了目标二進制檔案 tutorial 以及源檔案 tutorial.cxx。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

如何建構

以下是一組用于建構項目和測試二進制檔案的指令,稍後會詳細解釋:

mkdir build              cd build/              cmake ..              ls -l # inspect generated build files              cmake --build .              ./tutorial 10 # test the binary           

從上面的步驟可以看到,整個建構過程大約涉及 5-6 個步驟。

首先,在 CMake 中,我們應該将建構相關的内容與源代碼分開,是以先建立一個建構目錄:

mkdir build           

然後我們可以在建構目錄中進行所有與建構相關的操作:

cd build           

從這一步開始,我們将執行多個建構相關的任務。

先是生成配置檔案:

cmake ..           

在這一步中,CMake 會生成平台特定的配置檔案。在我的 Ubuntu 系統中,我看到了生成的makefile,這些檔案相當冗長,但目前我不需要擔心它們。

接下來,我根據新生成的檔案觸發建構:

cmake --build .           

這一步使用生成的建構檔案,生成目标二進制檔案 tutorial。

最後,我可以通過以下指令驗證二進制檔案是否如預期運作:

./tutorial 16           

我得到了預期的答案,這說明建構過程運作正常!

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

在 C++ 項目中注入變量

CMake 通過 Config.h.in 提供了一種機制,允許你在 CMakeLists.txt 中指定變量,這些變量可以在你的 .cpp 檔案中使用。

下面是一個示例,我們在 CMakeLists.txt 中定義了項目的版本号,并在程式中使用。

Config.h.in

在這個檔案中,來自 CMakeLists.txt 的變量将以 @VAR_NAME@ 的形式出現。

#pragma once                  #define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@              #define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@              #define AUTHOR_NAME "@AUTHOR_NAME@"           

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)              project(Tutorial)                  # Define configuration variables              set(PROJECT_VERSION_MAJOR 1)              set(PROJECT_VERSION_MINOR 0)              set(AUTHOR_NAME "Jith")                  # Configure the header file              configure_file(Config.h.in Config.h)                  # Add the executable              add_executable(tutorial tutorial.cxx)                  # Include the directory where the generated header file is located              target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           

請注意,我們添加了 cmake_minimum_required 來指定所需的最低 CMake 版本,這是編寫 CMakeLists.txt 檔案時的一個良好習慣。

然後,我們使用多個 set() 語句來定義所需的變量名。接着,指定配置檔案 Config.h.in,通過該檔案來使用上述設定的變量。

最後,CMake 會在變量占位被填充後生成頭檔案,這些動态生成的頭檔案需要被包含到項目中。

在我們的示例中,Config.h 檔案将被放置在 ${CMAKE_BINARY_DIR} 目錄中,是以我們隻需指定該路徑即可。

你可能會對以下這一行的 PRIVATE 标簽感到好奇:

target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           
“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

了解 CMake 的兩個關鍵概念:可見性修飾符和目标

在 CMake 中,有三個可見性修飾符:PRIVATE、PUBLIC、INTERFACE。

這些修飾符可以在指令中使用,例如:target_include_directories 和 target_link_libraries 等。

這些修飾符是在目标(Targets)的上下文中指定的。目标是 CMake 中的一種抽象概念,表示某種類型的輸出:

● 可執行目标(通過 add_executable)生成二進制檔案

● 庫目标(通過 add_library)生成庫檔案

● 自定義目标(通過 add_custom_target)通過腳本等生成任意檔案

所有上述的目标都會産生具體的檔案或工件作為輸出。庫目标的一個特殊情況是接口目标(Interface Target)。接口目标的定義如下:

add_library(my_interface_lib INTERFACE)              target_include_directories(my_interface_lib INTERFACE include/)           

在這裡,my_interface_lib 并不會立即生成任何檔案。但在後續階段,一些具體的目标可能會依賴于 my_interface_lib。這意味着,接口目标中指定的 include 目錄也會被依賴。是以,INTERFACE 庫可以看作是建構依賴關系樹的一種便利機制。

了解了目标和依賴的概念之後,我們就回到可見性修飾符的概念。

PRIVATE 可見性

1target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")           

PRIVATE 表示目标 tutorial 将使用指定的包含目錄。但如果在後續階段其他目标連結到 tutorial,包含目錄将不會傳遞給那些依賴項。

PUBLIC 可見性

1target_include_directories(tutorial PUBLIC "${CMAKE_BINARY_DIR}")           

使用 PUBLIC 修飾符意味着目标 tutorial 需要使用該包含目錄,并且任何依賴于 tutorial 的其他目标也會繼承這個包含目錄。

INTERFACE 可見性

1target_include_directories(tutorial INTERFACE "${CMAKE_BINARY_DIR}")           

INTERFACE 修飾符表示 tutorial 本身不需要該包含目錄,但任何依賴于 tutorial 的其他目标會繼承這個包含目錄。

簡單總結,可見性修飾符的工作原理如下:

● PRIVATE:源檔案和依賴關系隻傳遞給目前目标;

● PUBLIC:源檔案和依賴關系傳遞給目前目标及其依賴的目标;

INTERFACE:源檔案和依賴關系不傳遞給目前目标,但會傳遞給依賴于它的目标。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

将項目建構劃分為庫和目錄

随着項目規模不斷增長,通常需要子產品化來組織項目并管理複雜性。

在 CMake 中,可以用子目錄來指定獨立的子產品及其自定義的建構流程。我們可以擁有一個主 CMake 配置,它能觸發多個庫(子目錄)的建構,最後将所有子產品連結在一起。

這是一個經過簡化後的示例。我們将建立一個名為 MathFunctions 的子產品/庫,它将建構為一個靜态庫(在 Unix 系統上生成 MathFunctions.a),最後再把它連結到我們的主程式中。

首先是源檔案部分(代碼較為簡單):

MathFunctions.h

#pragma once                  namespace mathfunctions {              double sqrt(double x);              }           

MathFunctions.cxx

#include "MathFunctions.h"              #include "mysqrt.h"                  namespace mathfunctions {              double sqrt(double x)              {              return detail::mysqrt(x);              }              }           

mysqrt.h

#pragma once                  namespace mathfunctions {              namespace detail {              double mysqrt(double x);              }              }           

mysqrt.cxx

#include "mysqrt.h"                  #include <iostream>                  namespace mathfunctions {              namespace detail {              // a hack square root calculation using simple operations              double mysqrt(double x)              {              if (x <= 0) {              return 0;              }                  double result = x;                  // do ten iterations              for (int i = 0; i < 10; ++i) {              if (result <= 0) {              result = 0.1;              }              double delta = x - (result * result);              result = result + 0.5 * delta / result;              std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;              }              return result;              }              }              }           

以上這些代碼片段,引入了一個名為 mathfunctions 的命名空間,其中包含了一個自定義的 sqrt 函數實作。這樣我們就可以在項目中定義自己的平方根函數,而不會與其他版本的 sqrt 沖突。

接下來,如何将該檔案夾建構為 Unix 二進制檔案?我們需要為該子產品/庫建立一個自定義的 CMake 子配置:

MathFunctions/CMakeLists.txt

add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)           

通過這條簡單的 add_library 指令,我們指定了需要編譯的 .cxx 檔案來生成庫檔案。

但這還不夠,解決方案的核心在于如何将這個子目錄或庫連結到我們的主項目中:

tutorial.cxx(使用庫/子產品版本)

#include "Config.h"              #include "MathFunctions.h"              #include <cmath>              #include <cstdlib>               #include <iostream>              #include <string>                  int main(int argc, char* argv[])              {              std::cout << "Project Version: " << PROJECT_VERSION_MAJOR << "." << PROJECT_VERSION_MINOR << std::endl;              std::cout << "Author: " << AUTHOR_NAME << std::endl;                  if (argc < 2) {              std::cout << "Usage: " << argv[0] << " number" << std::endl;              return 1;              }                  const double inputValue = atof(argv[1]);                  // use library function              const double outputValue = mathfunctions::sqrt(inputValue);              std::cout << "The square root of " << inputValue << " is " << outputValue              << std::endl;              return 0;              }           

在這個檔案中,我們導入了 MathFunctions.h,并使用命名空間 mathfunctions 來調用自定義的 sqrt 函數。我們都知道 MathFunctions.h 位于子目錄中,但可以直接引用它,就像它在根目錄中似的,這是怎麼做到的?答案在于修訂後的主 CMake 配置檔案中:

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)              project(Tutorial)                  # Define configuration variables              set(PROJECT_VERSION_MAJOR 1)              set(PROJECT_VERSION_MINOR 0)              set(AUTHOR_NAME "Jith")                  # Configure the header file              configure_file(Config.h.in Config.h)                  add_subdirectory(MathFunctions)                  add_executable(tutorial tutorial.cxx)                      target_include_directories(tutorial PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")                  target_link_libraries(tutorial PUBLIC MathFunctions)           

這裡有幾條新指令:

● add_subdirectory 指定了一個子目錄建構,CMake 将負責處理該子目錄中的建構任務。

● target_include_directories 告訴 CMake MathFunctions 檔案夾的路徑,這樣我們可以在 tutorial.cxx 中直接引用 MathFunctions.h。

● target_link_libraries 将 MathFunctions 庫連結到主程式 tutorial 中。

當我在 Linux 上建構這個項目時,我看到 build/MathFunctions 目錄下生成了 libMathFunctions.a 檔案,這是一個靜态連結的庫檔案,它已經成為主程式的一部分。

現在,我們還可以随意移動生成的 tutorial 可執行檔案,它将繼續正常運作,因為 libMathFunctions.a 已經被靜态連結進主程式中。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

下一步是什麼?

學習 CMake 的基本工作原理和如何用它完成一些基本任務确實很有意思。

CMake 解決了我現在在 C++ 打包方面遇到的大部分問題。同時,探索 Conan 和 vcpkg 以簡化 C++ 中的依賴管理也是一件有趣的事情。未來有機會的話,我應該會進一步了解和嘗試這些工具。

“5 分鐘 CMake 使用指南,解決我的 C++ 打包問題!”

繼續閱讀