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

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

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

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

[復(fù)制鏈接]

440

主題

440

帖子

3269

積分

四級會(huì)員

Rank: 4

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

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

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

3、一個(gè)簡單的 CMake 項(xiàng)目示例(Hello World!)
假設(shè)你有一個(gè)用于計(jì)算數(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)上述兩行是生成一個(gè)可執(zhí)行文件所需的最少指令。理論上,我們還應(yīng)該指定 CMake 的最低版本號,省略 CMake 會(huì)默認(rèn)使用某個(gè)版本(暫時(shí)跳過這部分)。
    嚴(yán)格來說,project 指令并非必需,但我們還是保留它。所以最重要的代碼行是:
  • add_executable(tutorial tutorial.cxx)這行代碼指定了目標(biāo)二進(jìn)制文件 tutorial 以及源文件 tutorial.cxx。

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

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

    7、將項(xiàng)目構(gòu)建劃分為庫和目錄
    隨著項(xiàng)目規(guī)模不斷增長,通常需要模塊化來組織項(xiàng)目并管理復(fù)雜性。
    在 CMake 中,可以用子目錄來指定獨(dú)立的模塊及其自定義的構(gòu)建流程。我們可以擁有一個(gè)主 CMake 配置,它能觸發(fā)多個(gè)庫(子目錄)的構(gòu)建,最后將所有模塊鏈接在一起。
    這是一個(gè)經(jīng)過簡化后的示例。我們將創(chuàng)建一個(gè)名為 MathFunctions 的模塊/庫,它將構(gòu)建為一個(gè)靜態(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;}}}以上這些代碼片段,引入了一個(gè)名為 mathfunctions 的命名空間,其中包含了一個(gè)自定義的 sqrt 函數(shù)實(shí)現(xiàn)。這樣我們就可以在項(xiàng)目中定義自己的平方根函數(shù),而不會(huì)與其他版本的 sqrt 沖突。
    接下來,如何將該文件夾構(gòu)建為 Unix 二進(jìn)制文件?我們需要為該模塊/庫創(chuàng)建一個(gè)自定義的 CMake 子配置:
    MathFunctions/CMakeLists.txt
  • add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)通過這條簡單的 add_library 指令,我們指定了需要編譯的 .cxx 文件來生成庫文件。
    但這還不夠,解決方案的核心在于如何將這個(gè)子目錄或庫鏈接到我們的主項(xiàng)目中:
    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;}在這個(gè)文件中,我們導(dǎo)入了 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è)子目錄構(gòu)建,CMake 將負(fù)責(zé)處理該子目錄中的構(gòu)建任務(wù)。
    ● target_include_directories 告訴 CMake MathFunctions 文件夾的路徑,這樣我們可以在 tutorial.cxx 中直接引用 MathFunctions.h。
    ● target_link_libraries 將 MathFunctions 庫鏈接到主程序 tutorial 中。
    當(dāng)我在 Linux 上構(gòu)建這個(gè)項(xiàng)目時(shí),我看到 build/MathFunctions 目錄下生成了 libMathFunctions.a 文件,這是一個(gè)靜態(tài)鏈接的庫文件,它已經(jīng)成為主程序的一部分。
    現(xiàn)在,我們還可以隨意移動(dòng)生成的 tutorial 可執(zhí)行文件,它將繼續(xù)正常運(yùn)行,因?yàn)?libMathFunctions.a 已經(jīng)被靜態(tài)鏈接進(jìn)主程序中。

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

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


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

  • 發(fā)表回復(fù)

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

    本版積分規(guī)則

    關(guān)閉

    站長推薦上一條 /1 下一條


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