電子產(chǎn)業(yè)一站式賦能平臺

PCB聯(lián)盟網(wǎng)

搜索
查看: 42|回復: 0
收起左側(cè)

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

[復制鏈接]

454

主題

454

帖子

3643

積分

四級會員

Rank: 4

積分
3643
跳轉(zhuǎn)到指定樓層
樓主
發(fā)表于 2024-11-6 09:01:00 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式
本文經(jīng)授權(quán)轉(zhuǎn)自公眾號CSDN(ID:CSDNnews)
作者 | Shrijith Venkatramana翻譯 | 鄭麗媛
在軟件開發(fā)的世界里,構(gòu)建系統(tǒng)扮演著至關重要的角色,它不僅決定了項目的構(gòu)建效率,還直接影響到團隊協(xié)作的流暢度。對于許多 C++ 開發(fā)者而言,CMake 因其強大的功能和廣泛的兼容性成為了構(gòu)建自動化流程的首選工具。
原文鏈接:https://journal.hexmos.com/cmake-survial-guide/

最近我一直在用 C++ 處理一些編程挑戰(zhàn),其中管理 C++ 項目的一個重要方面就是依賴管理。
如今,我們在很多編程生態(tài)系統(tǒng)中享受著即時包管理器的便利:
● 在 Node.js/JavaScript 中使用 npm
● 在 Rust 中使用 cargo
● 在 Python 中使用 pip
而在 C++ 中,盡管有像 Conan 這樣的包管理器,但處理實際項目時,你通常會發(fā)現(xiàn) CMake 是繞不開的選擇。因此如果你想在 C++ 生態(tài)系統(tǒng)中工作,學習如何使用 CMake 就不是可選項,而是必修課。
1、CMake 到底是什么,為什么要學它?
CMake 是一個跨平臺的構(gòu)建系統(tǒng)生成器?缙脚_這一點非常重要,因為 CMake 能夠在一定程度上抽象出不同平臺之間的差異。
例如在類 Unix 系統(tǒng)上,CMake 會生成 makefile 文件,然后用這些文件來構(gòu)建項目。而在 Windows 系統(tǒng)中,CMake 會生成 Visual Studio 項目文件,隨后用于構(gòu)建項目。
需要注意的是,不同平臺通常都有各自的編譯和調(diào)試工具鏈:Unix 使用 gcc,macOS 使用clang 等等。
在 C++ 生態(tài)系統(tǒng)中,另一個重要方面是能同時處理可執(zhí)行文件和庫。
可執(zhí)行文件可基于以下不同因素:
● 目標 CPU 架構(gòu)
● 目標操作系統(tǒng)
● 其他因素
對于庫來說,鏈接方式也有不同的選擇(鏈接是指在代碼中使用另一個代碼庫的功能,而無需了解其具體實現(xiàn)):
● 靜態(tài)鏈接
● 動態(tài)鏈接
我曾在一些內(nèi)部原型項目中,需要調(diào)用底層操作系統(tǒng) API 來執(zhí)行某些任務,唯一可行的高效方法就是基于一些 C++ 庫來進行構(gòu)建。

2、CMake 是如何工作的:三個階段
1. 配置階段
CMake 會讀取所有的 CMakeLists.txt 文件,并創(chuàng)建一個中間結(jié)構(gòu)來確定后續(xù)步驟(如列出源文件、收集要鏈接的庫等)。
2. 生成階段
基于配置階段的中間輸出,CMake 會生成特定平臺的構(gòu)建文件(如在 Unix 系統(tǒng)上生成 makefiles 等)。
3. 構(gòu)建階段
使用特定平臺的工具(如 make 或 ninja)來構(gòu)建可執(zhí)行文件或庫文件。

3、一個簡單的 CMake 項目示例(Hello World!)
假設你有一個用于計算數(shù)字平方根的 C++ 源文件。
tutorial.cxx
  • // A simple program that computes the square root of a number#include #include  // TODO 5: Remove this line#include #include
    // 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: " 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 " " is "             std::endl;  return 0;}CMakeLists.txt
  • project(Tutorial)add_executable(tutorial tutorial.cxx)上述兩行是生成一個可執(zhí)行文件所需的最少指令。理論上,我們還應該指定 CMake 的最低版本號,省略 CMake 會默認使用某個版本(暫時跳過這部分)。
    嚴格來說,project 指令并非必需,但我們還是保留它。所以最重要的代碼行是:
  • add_executable(tutorial tutorial.cxx)這行代碼指定了目標二進制文件 tutorial 以及源文件 tutorial.cxx。

    4、如何構(gòu)建
    以下是一組用于構(gòu)建項目和測試二進制文件的命令,稍后會詳細解釋:
  • mkdir buildcd build/cmake ..ls -l # inspect generated build filescmake --build ../tutorial 10 # test the binary從上面的步驟可以看到,整個構(gòu)建過程大約涉及 5-6 個步驟。
    首先,在 CMake 中,我們應該將構(gòu)建相關的內(nèi)容與源代碼分開,所以先創(chuàng)建一個構(gòu)建目錄:
  • mkdir build然后我們可以在構(gòu)建目錄中進行所有與構(gòu)建相關的操作:
  • cd build從這一步開始,我們將執(zhí)行多個構(gòu)建相關的任務。
    先是生成配置文件:
  • cmake ..在這一步中,CMake 會生成平臺特定的配置文件。在我的 Ubuntu 系統(tǒng)中,我看到了生成的makefile,這些文件相當冗長,但目前我不需要擔心它們。
    接下來,我根據(jù)新生成的文件觸發(fā)構(gòu)建:
  • cmake --build .這一步使用生成的構(gòu)建文件,生成目標二進制文件 tutorial。
    最后,我可以通過以下命令驗證二進制文件是否如預期運行:
  • ./tutorial 16我得到了預期的答案,這說明構(gòu)建過程運行正常!

    5、在 C++ 項目中注入變量
    CMake 通過 Config.h.in 提供了一種機制,允許你在 CMakeLists.txt 中指定變量,這些變量可以在你的 .cpp 文件中使用。
    下面是一個示例,我們在 CMakeLists.txt 中定義了項目的版本號,并在程序中使用。
    Config.h.in
    在這個文件中,來自 CMakeLists.txt 的變量將以 @VAR_NAME@ 的形式出現(xiàn)。
  • #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 variablesset(PROJECT_VERSION_MAJOR 1)set(PROJECT_VERSION_MINOR 0)set(AUTHOR_NAME "Jith")
    # Configure the header fileconfigure_file(Config.h.in Config.h)
    # Add the executableadd_executable(tutorial tutorial.cxx)
    # Include the directory where the generated header file is locatedtarget_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")請注意,我們添加了 cmake_minimum_required 來指定所需的最低 CMake 版本,這是編寫 CMakeLists.txt 文件時的一個良好習慣。
    然后,我們使用多個 set() 語句來定義所需的變量名。接著,指定配置文件 Config.h.in,通過該文件來使用上述設置的變量。
    最后,CMake 會在變量占位被填充后生成頭文件,這些動態(tài)生成的頭文件需要被包含到項目中。
    在我們的示例中,Config.h 文件將被放置在 ${CMAKE_BINARY_DIR} 目錄中,所以我們只需指定該路徑即可。
    你可能會對以下這一行的 PRIVATE 標簽感到好奇:
  • target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")
    6、理解 CMake 的兩個關鍵概念:可見性修飾符和目標
    在 CMake 中,有三個可見性修飾符:PRIVATE、PUBLIC、INTERFACE。
    這些修飾符可以在命令中使用,例如:target_include_directories 和 target_link_libraries 等。
    這些修飾符是在目標(Targets)的上下文中指定的。目標是 CMake 中的一種抽象概念,表示某種類型的輸出:
    ● 可執(zhí)行目標(通過 add_executable)生成二進制文件
    ● 庫目標(通過 add_library)生成庫文件
    ● 自定義目標(通過 add_custom_target)通過腳本等生成任意文件
    所有上述的目標都會產(chǎn)生具體的文件或工件作為輸出。庫目標的一個特殊情況是接口目標(Interface Target)。接口目標的定義如下:
  • add_library(my_interface_lib INTERFACE)target_include_directories(my_interface_lib INTERFACE include/)在這里,my_interface_lib 并不會立即生成任何文件。但在后續(xù)階段,一些具體的目標可能會依賴于 my_interface_lib。這意味著,接口目標中指定的 include 目錄也會被依賴。因此,INTERFACE 庫可以看作是構(gòu)建依賴關系樹的一種便利機制。
    理解了目標和依賴的概念之后,我們就回到可見性修飾符的概念。
    PRIVATE 可見性
  • 1target_include_directories(tutorial PRIVATE "${CMAKE_BINARY_DIR}")PRIVATE 表示目標 tutorial 將使用指定的包含目錄。但如果在后續(xù)階段其他目標鏈接到 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 的其他目標會繼承這個包含目錄。
    簡單總結(jié),可見性修飾符的工作原理如下:
    ● PRIVATE:源文件和依賴關系只傳遞給當前目標;
    ● PUBLIC:源文件和依賴關系傳遞給當前目標及其依賴的目標;
    INTERFACE:源文件和依賴關系不傳遞給當前目標,但會傳遞給依賴于它的目標。

    7、將項目構(gòu)建劃分為庫和目錄
    隨著項目規(guī)模不斷增長,通常需要模塊化來組織項目并管理復雜性。
    在 CMake 中,可以用子目錄來指定獨立的模塊及其自定義的構(gòu)建流程。我們可以擁有一個主 CMake 配置,它能觸發(fā)多個庫(子目錄)的構(gòu)建,最后將所有模塊鏈接在一起。
    這是一個經(jīng)過簡化后的示例。我們將創(chuàng)建一個名為 MathFunctions 的模塊/庫,它將構(gòu)建為一個靜態(tài)庫(在 Unix 系統(tǒng)上生成 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
    namespace mathfunctions {namespace detail {// a hack square root calculation using simple operationsdouble 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 " " to be " std::endl;  }  return result;}}}以上這些代碼片段,引入了一個名為 mathfunctions 的命名空間,其中包含了一個自定義的 sqrt 函數(shù)實現(xiàn)。這樣我們就可以在項目中定義自己的平方根函數(shù),而不會與其他版本的 sqrt 沖突。
    接下來,如何將該文件夾構(gòu)建為 Unix 二進制文件?我們需要為該模塊/庫創(chuàng)建一個自定義的 CMake 子配置:
    MathFunctions/CMakeLists.txt
  • add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)通過這條簡單的 add_library 指令,我們指定了需要編譯的 .cxx 文件來生成庫文件。
    但這還不夠,解決方案的核心在于如何將這個子目錄或庫鏈接到我們的主項目中:
    tutorial.cxx(使用庫/模塊版本)
  • #include "Config.h"#include "MathFunctions.h"#include #include  #include #include
    int main(int argc, char* argv[]){  std::cout "Project Version: " "." std::endl;  std::cout "Author: " std::endl;
      if (argc 2) {    std::cout "Usage: " 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 " " is "             std::endl;  return 0;}在這個文件中,我們導入了 MathFunctions.h,并使用命名空間 mathfunctions 來調(diào)用自定義的 sqrt 函數(shù)。我們都知道 MathFunctions.h 位于子目錄中,但可以直接引用它,就像它在根目錄中似的,這是怎么做到的?答案在于修訂后的主 CMake 配置文件中:
    CMakeLists.txt
  • cmake_minimum_required(VERSION 3.10)project(Tutorial)
    # Define configuration variablesset(PROJECT_VERSION_MAJOR 1)set(PROJECT_VERSION_MINOR 0)set(AUTHOR_NAME "Jith")
    # Configure the header fileconfigure_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 指定了一個子目錄構(gòu)建,CMake 將負責處理該子目錄中的構(gòu)建任務。
    ● target_include_directories 告訴 CMake MathFunctions 文件夾的路徑,這樣我們可以在 tutorial.cxx 中直接引用 MathFunctions.h。
    ● target_link_libraries 將 MathFunctions 庫鏈接到主程序 tutorial 中。
    當我在 Linux 上構(gòu)建這個項目時,我看到 build/MathFunctions 目錄下生成了 libMathFunctions.a 文件,這是一個靜態(tài)鏈接的庫文件,它已經(jīng)成為主程序的一部分。
    現(xiàn)在,我們還可以隨意移動生成的 tutorial 可執(zhí)行文件,它將繼續(xù)正常運行,因為 libMathFunctions.a 已經(jīng)被靜態(tài)鏈接進主程序中。

    8、下一步是什么?
    學習 CMake 的基本工作原理和如何用它完成一些基本任務確實很有意思。
    CMake 解決了我現(xiàn)在在 C++ 打包方面遇到的大部分問題。同時,探索 Conan 和 vcpkg 以簡化 C++ 中的依賴管理也是一件有趣的事情。未來有機會的話,我應該會進一步了解和嘗試這些工具。
    本文轉(zhuǎn)自公眾號“CSDN”,ID:CSDNnews——EOF——你好,我是飛宇。日常分享C/C++、計算機學習經(jīng)驗、工作體會,歡迎點擊此處查看我以前的學習筆記&經(jīng)驗&分享的資源。
    我組建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起進群交流。

    歡迎你添加我的微信,我拉你進技術(shù)交流群。此外,我也會經(jīng)常在微信上分享一些計算機學習經(jīng)驗以及工作體驗,還有一些內(nèi)推機會。


    加個微信,打開另一扇窗
    經(jīng)常遇到有讀者后臺私信想要一些編程學習資源,這里分享 1T 的編程電子書、C/C++開發(fā)手冊、Github上182K+的架構(gòu)路線圖、LeetCode算法刷題筆記等精品學習資料,點擊下方公眾號會回復"編程"即可免費領取~
    感謝你的分享,點贊,在看三  

  • 回復

    使用道具 舉報

    發(fā)表回復

    您需要登錄后才可以回帖 登錄 | 立即注冊

    本版積分規(guī)則


    聯(lián)系客服 關注微信 下載APP 返回頂部 返回列表