5 分鐘 CMake 使用指南,解決我的 C-- 打包問題!
本文經授權轉自公衆號 CSDN(ID:CSDNnews)
作者 | Shrijith Venkatramana
翻譯 | 鄭麗媛
在軟件開發的世界裏,構建系統扮演着至關重要的角色,它不僅決定了項目的構建效率,還直接影響到團隊協作的流暢度。對於許多 C++ 開發者而言,CMake 因其強大的功能和廣泛的兼容性成爲了構建自動化流程的首選工具。
原文鏈接:https://journal.hexmos.com/cmake-survial-guide/
最近我一直在用 C++ 處理一些編程挑戰,其中管理 C++ 項目的一個重要方面就是依賴管理。
如今,我們在很多編程生態系統中享受着即時包管理器的便利:
● 在 Node.js/JavaScript 中使用 npm
● 在 Rust 中使用 cargo
● 在 Python 中使用 pip
而在 C++ 中,儘管有像 Conan 這樣的包管理器,但處理實際項目時,你通常會發現 CMake 是繞不開的選擇。因此如果你想在 C++ 生態系統中工作,學習如何使用 CMake 就不是可選項,而是必修課。
1、CMake 到底是什麼,爲什麼要學它?
CMake 是一個跨平臺的構建系統生成器。跨平臺這一點非常重要,因爲 CMake 能夠在一定程度上抽象出不同平臺之間的差異。
例如在類 Unix 系統上,CMake 會生成 makefile 文件,然後用這些文件來構建項目。而在 Windows 系統中,CMake 會生成 Visual Studio 項目文件,隨後用於構建項目。
需要注意的是,不同平臺通常都有各自的編譯和調試工具鏈:Unix 使用 gcc,macOS 使用 clang 等等。
在 C++ 生態系統中,另一個重要方面是能同時處理可執行文件和庫。
可執行文件可基於以下不同因素:
● 目標 CPU 架構
● 目標操作系統
● 其他因素
對於庫來說,鏈接方式也有不同的選擇(鏈接是指在代碼中使用另一個代碼庫的功能,而無需瞭解其具體實現):
● 靜態鏈接
● 動態鏈接
我曾在一些內部原型項目中,需要調用底層操作系統 API 來執行某些任務,唯一可行的高效方法就是基於一些 C++ 庫來進行構建。
2、CMake 是如何工作的:三個階段
- 配置階段
CMake 會讀取所有的 CMakeLists.txt 文件,並創建一箇中間結構來確定後續步驟(如列出源文件、收集要鏈接的庫等)。
- 生成階段
基於配置階段的中間輸出,CMake 會生成特定平臺的構建文件(如在 Unix 系統上生成 makefiles 等)。
- 構建階段
使用特定平臺的工具(如 make 或 ninja)來構建可執行文件或庫文件。
3、一個簡單的 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。
4、如何構建
以下是一組用於構建項目和測試二進制文件的命令,稍後會詳細解釋:
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、在 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}")
6、理解 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:源文件和依賴關係不傳遞給當前目標,但會傳遞給依賴於它的目標。
7、將項目構建劃分爲庫和目錄
隨着項目規模不斷增長,通常需要模塊化來組織項目並管理複雜性。
在 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 已經被靜態鏈接進主程序中。
8、下一步是什麼?
學習 CMake 的基本工作原理和如何用它完成一些基本任務確實很有意思。
CMake 解決了我現在在 C++ 打包方面遇到的大部分問題。同時,探索 Conan 和 vcpkg 以簡化 C++ 中的依賴管理也是一件有趣的事情。未來有機會的話,我應該會進一步瞭解和嘗試這些工具。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8rXQo2CkO9YgiE8JvwokSg