從 0 到 1 學(xué) CMake:開(kāi)啟高效跨平臺(tái)構(gòu)建之旅
在當(dāng)今軟件開(kāi)發(fā)生態(tài)系統(tǒng)中,構(gòu)建工具宛如幕后的 “大管家”,掌控著從源代碼到可執(zhí)行程序或庫(kù)的復(fù)雜流程。它們不僅要應(yīng)對(duì)不同編程語(yǔ)言的特性,還需適配各類(lèi)操作系統(tǒng)與編譯器組合,其重要性不言而喻。今天,我們要聚焦一款在跨平臺(tái)構(gòu)建領(lǐng)域熠熠生輝的工具 ——CMake。
你是否曾為在 Windows、Linux 和 macOS 等不同平臺(tái)上,將代碼順利轉(zhuǎn)化為可運(yùn)行程序而絞盡腦汁?是否在面對(duì)復(fù)雜項(xiàng)目中眾多源文件、庫(kù)依賴時(shí),感到構(gòu)建過(guò)程猶如一團(tuán)亂麻,無(wú)從下手?CMake 的誕生,正是為了化解這些棘手難題。它就像一位經(jīng)驗(yàn)豐富、足智多謀的項(xiàng)目經(jīng)理,能有條不紊地管理項(xiàng)目構(gòu)建的全過(guò)程,讓你的代碼跨越平臺(tái)的界限,在各種環(huán)境中都能精準(zhǔn)無(wú)誤地編譯和運(yùn)行。
無(wú)論是初涉編程的新手,還是久經(jīng)沙場(chǎng)的開(kāi)發(fā)老兵,尤其是那些投身 C++、C 等編譯型語(yǔ)言項(xiàng)目的開(kāi)發(fā)者,掌握 CMake 都將成為你提升開(kāi)發(fā)效率、邁向更高層次的關(guān)鍵一步。接下來(lái),就讓我們一同深入 CMake 的奇妙世界,開(kāi)啟這場(chǎng)從 0 到 1 的探索之旅 。
一、CMake簡(jiǎn)介
1. 什么是CMake
CMake是個(gè)一個(gè)開(kāi)源的跨平臺(tái)自動(dòng)化建構(gòu)系統(tǒng),用來(lái)管理軟件建置的程序,并不相依于某特定編譯器;并可支持多層目錄、多個(gè)應(yīng)用程序與多個(gè)庫(kù)。它用配置文件控制建構(gòu)過(guò)程(build process)的方式和Unix的make相似,只是CMake的配置文件取名為CMakeLists.txt。
CMake并不直接建構(gòu)出最終的軟件,而是產(chǎn)生標(biāo)準(zhǔn)的建構(gòu)檔(如Unix的Makefile或Windows Visual C++的projects/workspaces),然后再依一般的建構(gòu)方式使用。這使得熟悉某個(gè)集成開(kāi)發(fā)環(huán)境(IDE)的開(kāi)發(fā)者可以用標(biāo)準(zhǔn)的方式建構(gòu)他的軟件,這種可以使用各平臺(tái)的原生建構(gòu)系統(tǒng)的能力是CMake和SCons等其他類(lèi)似系統(tǒng)的區(qū)別之處。它首先允許開(kāi)發(fā)者編寫(xiě)一種平臺(tái)無(wú)關(guān)的CMakeList.txt 文件來(lái)定制整個(gè)編譯流程,然后再根據(jù)目標(biāo)用戶的平臺(tái)進(jìn)一步生成所需的本地化 Makefile 和工程文件,如 Unix的 Makefile 或 Windows 的 Visual Studio 工程。從而做到“Write once, run everywhere”。
顯然,CMake 是一個(gè)比上述幾種 make 更高級(jí)的編譯配置工具?!癈Make”這個(gè)名字是"Cross platform MAke"的縮寫(xiě)。雖然名字中含有"make",但是CMake和Unix上常見(jiàn)的“make”系統(tǒng)是分開(kāi)的,而且更為高端。它可與原生建置環(huán)境結(jié)合使用,例如:make、蘋(píng)果的Xcode與微軟的Visual Studio。
2. 為什么選擇 CMake?
在軟件開(kāi)發(fā)的構(gòu)建領(lǐng)域中,CMake 憑借眾多顯著優(yōu)點(diǎn)脫穎而出,成為眾多開(kāi)發(fā)者的首選。
(1) 跨平臺(tái)性:在如今多樣化的開(kāi)發(fā)環(huán)境下,一款軟件往往需要在不同的操作系統(tǒng)上運(yùn)行,如 Windows、Linux、macOS 等,甚至還可能涉及到移動(dòng)平臺(tái)如 Android 。CMake 強(qiáng)大的跨平臺(tái)特性就像是一位萬(wàn)能的翻譯,它允許開(kāi)發(fā)者編寫(xiě)一次構(gòu)建腳本,然后就能在各種主流操作系統(tǒng)上生成對(duì)應(yīng)的構(gòu)建文件,確保軟件在不同平臺(tái)上都能順利編譯和運(yùn)行 。以一個(gè)簡(jiǎn)單的 C++ 項(xiàng)目為例,無(wú)論是在 Windows 系統(tǒng)下使用 Visual Studio 編譯器,還是在 Linux 系統(tǒng)下使用 GCC 編譯器,只需一份相同的 CMakeLists.txt 文件,CMake 就能根據(jù)不同的平臺(tái)環(huán)境生成合適的構(gòu)建腳本,大大提高了開(kāi)發(fā)效率和代碼的可移植性。
(2) 模塊化:當(dāng)項(xiàng)目規(guī)模逐漸增大,代碼量不斷增多時(shí),合理的模塊劃分對(duì)于項(xiàng)目的管理和維護(hù)至關(guān)重要。CMake 支持模塊化的項(xiàng)目配置,開(kāi)發(fā)者可以將項(xiàng)目按照功能、模塊等維度進(jìn)行拆分,每個(gè)模塊都有自己獨(dú)立的 CMakeLists.txt 文件 。這樣一來(lái),各個(gè)模塊的構(gòu)建規(guī)則和依賴關(guān)系都能清晰地定義和管理,不僅方便了開(kāi)發(fā)過(guò)程中的協(xié)作,也使得項(xiàng)目的結(jié)構(gòu)更加清晰,易于維護(hù)和擴(kuò)展。例如,在一個(gè)大型游戲開(kāi)發(fā)項(xiàng)目中,可能會(huì)將游戲的渲染模塊、邏輯模塊、網(wǎng)絡(luò)模塊等分別獨(dú)立管理,每個(gè)模塊都可以獨(dú)立編譯、測(cè)試和更新,而不會(huì)影響到其他模塊的正常運(yùn)行。
(3) 可擴(kuò)展性:隨著項(xiàng)目需求的不斷變化和發(fā)展,構(gòu)建系統(tǒng)也需要具備一定的靈活性和可擴(kuò)展性。CMake 允許開(kāi)發(fā)者編寫(xiě)自定義的模塊和宏,以滿足項(xiàng)目特定的構(gòu)建需求 。比如,在一些對(duì)編譯優(yōu)化有特殊要求的項(xiàng)目中,開(kāi)發(fā)者可以編寫(xiě)自定義的 CMake 模塊,添加特定的編譯選項(xiàng)和鏈接庫(kù);在一些需要自動(dòng)化部署的項(xiàng)目中,開(kāi)發(fā)者可以通過(guò)編寫(xiě)宏來(lái)實(shí)現(xiàn)自動(dòng)化的打包和發(fā)布流程。這種高度的可擴(kuò)展性使得 CMake 能夠適應(yīng)各種復(fù)雜的項(xiàng)目場(chǎng)景,無(wú)論是小型的個(gè)人項(xiàng)目,還是大型的企業(yè)級(jí)項(xiàng)目,都能發(fā)揮出它的優(yōu)勢(shì)。
(4) 自動(dòng)依賴管理:在項(xiàng)目開(kāi)發(fā)過(guò)程中,依賴關(guān)系的管理往往是一個(gè)繁瑣且容易出錯(cuò)的環(huán)節(jié)。CMake 能夠自動(dòng)檢測(cè)項(xiàng)目所依賴的庫(kù)文件和頭文件,并在構(gòu)建過(guò)程中自動(dòng)處理這些依賴關(guān)系 。它通過(guò) find_package 等命令,可以在系統(tǒng)中查找指定的依賴庫(kù),并設(shè)置相應(yīng)的變量,確保項(xiàng)目在編譯和鏈接時(shí)能夠正確地找到這些依賴項(xiàng)。例如,當(dāng)項(xiàng)目依賴于 OpenCV 庫(kù)時(shí),只需在 CMakeLists.txt 文件中使用 find_package (OpenCV REQUIRED) 命令,CMake 就會(huì)自動(dòng)查找系統(tǒng)中安裝的 OpenCV 庫(kù),并將其包含路徑和庫(kù)文件路徑設(shè)置好,開(kāi)發(fā)者無(wú)需手動(dòng)去配置這些復(fù)雜的路徑信息,大大減少了因依賴管理不當(dāng)而導(dǎo)致的錯(cuò)誤。
(5) 簡(jiǎn)化構(gòu)建過(guò)程:相較于傳統(tǒng)的構(gòu)建方式,如手動(dòng)編寫(xiě) Makefile 文件,CMake 使用一種簡(jiǎn)潔、直觀的語(yǔ)法來(lái)描述項(xiàng)目的構(gòu)建規(guī)則 。開(kāi)發(fā)者只需要在 CMakeLists.txt 文件中使用簡(jiǎn)單的命令,如 project 定義項(xiàng)目名稱(chēng)、add_executable 生成可執(zhí)行文件、add_library 生成庫(kù)文件等,就能清晰地定義項(xiàng)目的構(gòu)建過(guò)程。而且,CMake 還提供了豐富的命令和選項(xiàng),方便開(kāi)發(fā)者進(jìn)行各種配置,如設(shè)置編譯選項(xiàng)、添加鏈接庫(kù)、定義自定義目標(biāo)等。這使得構(gòu)建過(guò)程更加自動(dòng)化和規(guī)范化,降低了開(kāi)發(fā)者的學(xué)習(xí)成本和工作量。
與傳統(tǒng)的構(gòu)建方式相比,CMake 的優(yōu)勢(shì)更加明顯。在傳統(tǒng)的 Makefile 構(gòu)建方式中,對(duì)于不同的平臺(tái)和編譯器,可能需要編寫(xiě)不同的 Makefile 文件,而且Makefile的語(yǔ)法較為復(fù)雜,對(duì)于復(fù)雜的項(xiàng)目依賴關(guān)系處理起來(lái)也比較困難。而 CMake 通過(guò)統(tǒng)一的構(gòu)建腳本和強(qiáng)大的功能,解決了這些問(wèn)題,讓構(gòu)建過(guò)程變得更加簡(jiǎn)單、高效和可靠 。
二、CMake使用教程
1. 安裝 CMake
CMake 的安裝過(guò)程相對(duì)簡(jiǎn)單,下面將分別介紹在 Windows、Linux 和 macOS 系統(tǒng)下的安裝步驟。
(1) Windows 系統(tǒng):
- 首先,前往CMake 官方網(wǎng)站下載 Windows 版本的安裝包,通常是.msi 文件。
- 下載完成后,雙擊.msi 文件,按照安裝向?qū)У闹甘具M(jìn)行安裝。在安裝過(guò)程中,務(wù)必勾選 “Add CMake to the system PATH for all users” 選項(xiàng),這樣可以將 CMake 添加到系統(tǒng)的 PATH 環(huán)境變量中,方便后續(xù)在命令行中直接使用 cmake 命令。
- 安裝完成后,打開(kāi)命令提示符(CMD)或 PowerShell,輸入cmake --version,如果能正確顯示 CMake 的版本信息,說(shuō)明安裝成功。例如:cmake version 3.26.4 。
(2) Linux 系統(tǒng):不同的 Linux 發(fā)行版安裝方式略有不同。
(3) Debian 和 Ubuntu 系統(tǒng):打開(kāi)終端,輸入以下命令進(jìn)行安裝:
sudo apt-get update
sudo apt-get install cmake
(4) Fedora 系統(tǒng):在終端中執(zhí)行:
sudo dnf install cmake
(5) Arch Linux 系統(tǒng):使用以下命令安裝:
sudo pacman -S cmake
安裝完成后,在終端輸入cmake --version驗(yàn)證安裝是否成功。
(6) macOS 系統(tǒng):
通過(guò) Homebrew 安裝:如果你的macOS 系統(tǒng)安裝了 Homebrew 包管理器,打開(kāi)終端,執(zhí)行以下命令即可安裝 CMake:
brew install cmake
通過(guò)官方安裝包:訪問(wèn)CMake官方網(wǎng)站,選擇 macOS 版本的.dmg 文件進(jìn)行下載。下載完成后,運(yùn)行.dmg 文件,將 CMake 圖標(biāo)拖動(dòng)到應(yīng)用程序文件夾。
安裝成功后,命令都在/Applications/CMake.app/Contents/bin目錄下,需要將環(huán)境變量添加到.bash_profile文件中。使用 vim 進(jìn)行編輯:
vim ~/.bash_profile
將以下內(nèi)容添加到文件末尾:
export PATH="/Applications/CMake.app/Contents/bin:$PATH"
添加完成后,執(zhí)行source ~/.bash_profile或者重新啟動(dòng)終端。最后,在終端輸入cmake --version確認(rèn) CMake 已正確安裝。在使用 CMake 構(gòu)建項(xiàng)目之前,需要了解幾個(gè)重要的概念:
- CMakeLists.txt 文件:這是 CMake 構(gòu)建系統(tǒng)的核心配置文件,每個(gè)項(xiàng)目或子目錄都可以有一個(gè) CMakeLists.txt 文件。它包含了一系列的 CMake 命令和指令,用于描述項(xiàng)目的構(gòu)建過(guò)程,例如指定項(xiàng)目名稱(chēng)、源文件位置、鏈接庫(kù)、編譯選項(xiàng)等。通過(guò)編寫(xiě) CMakeLists.txt 文件,開(kāi)發(fā)者可以以一種平臺(tái)無(wú)關(guān)的方式定義項(xiàng)目的構(gòu)建規(guī)則,CMake 會(huì)根據(jù)這些規(guī)則生成適合不同平臺(tái)的構(gòu)建文件 。
- 生成器:CMake 本身并不直接構(gòu)建項(xiàng)目,而是通過(guò)生成器(Generator)來(lái)生成構(gòu)建文件。生成器是一種模板驅(qū)動(dòng)的機(jī)制,它根據(jù) CMakeLists.txt 文件和用戶的配置,生成特定平臺(tái)和構(gòu)建工具所需的構(gòu)建文件,如 Unix 系統(tǒng)下的 Makefile、Windows 系統(tǒng)下 Visual Studio 的項(xiàng)目文件(.sln 和.vcxproj)、Xcode 項(xiàng)目文件(.xcodeproj)等 。不同的生成器適用于不同的開(kāi)發(fā)環(huán)境和構(gòu)建需求,開(kāi)發(fā)者可以根據(jù)項(xiàng)目的實(shí)際情況選擇合適的生成器。
- 構(gòu)建目錄:構(gòu)建目錄是存放生成的構(gòu)建文件以及編譯過(guò)程中產(chǎn)生的中間文件和最終目標(biāo)文件的地方。為了保持項(xiàng)目源代碼的整潔,通常建議將構(gòu)建目錄與源代碼目錄分開(kāi)。在構(gòu)建項(xiàng)目時(shí),首先進(jìn)入構(gòu)建目錄,然后執(zhí)行 CMake 命令生成構(gòu)建文件,最后使用構(gòu)建工具(如 Make、Ninja 等)在該目錄下進(jìn)行項(xiàng)目的編譯和鏈接 。例如,在一個(gè)名為my_project的項(xiàng)目中,可以在項(xiàng)目根目錄下創(chuàng)建一個(gè)build目錄作為構(gòu)建目錄,所有的構(gòu)建相關(guān)文件都將在這個(gè)目錄中生成和處理。
2. 基礎(chǔ)的構(gòu)建步驟(步驟1)
一個(gè)最常用的基礎(chǔ)項(xiàng)目是從源碼中構(gòu)建一個(gè)可執(zhí)行文件。對(duì)于一個(gè)簡(jiǎn)單的項(xiàng)目?jī)尚?CMakeLists.txt 文件就能搞定:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)
上面的例子中使用了小寫(xiě)的命令,事實(shí)上,CMakeLists.txt 文件并不區(qū)分命令的大小寫(xiě)。tutorial.cxx源碼是用來(lái)計(jì)算一個(gè)數(shù)的算數(shù)平方根,下面是其一個(gè)簡(jiǎn)單的版本:
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
system("pause");
return 0;
}
譯者這里在 16 行附加了一行system("pause");,是為了程序執(zhí)行完畢后不會(huì)立刻關(guān)閉窗口。后面的代碼的示例中并不會(huì)再添加此行,如果用戶需要暫停的話,可以在自己的代碼中加入該行。
(1) 添加版本號(hào)和配置頭文件
第一個(gè)要添加的特性就是給我們的可執(zhí)行文件和項(xiàng)目提供一個(gè)版本號(hào)。你可以在你的源碼之外做到這一點(diǎn),在 CMakeLists.txt 文件中做這些會(huì)更加靈活。為了添加一個(gè)版本號(hào)我們修改我們的 CMakeList.txt 文件如下:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")
# add the executable
add_executable(Tutorial tutorial.cxx)
因?yàn)榕渲梦募?huì)被寫(xiě)入到生成路徑(binary tree) 中,所以我們必須將該文件夾添加到頭文件搜索路徑中。接下來(lái)我們?cè)谠创a中創(chuàng)建一個(gè)包含以下內(nèi)容的 http://TutorialConfig.h.in 文件:
// the configured options and settings for Tutorial #define Tutorial_VERSION_MAJOR
@Tutorial_VERSION_MAJOR@ #define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
當(dāng) CMake 配置這個(gè)頭文件的時(shí)候,@Tutorial_VERSION_MAJOR@ 和 @ Tutorial_VERSION_MINOR@ 就會(huì)用CMakeLists.txt 文件中對(duì)應(yīng)的值替換。接下來(lái)我們修改 tutorial.cxx 源碼包含配置頭文件并使用版本號(hào),修改后的源碼如下:
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n",
argv[0],
Tutorial_VERSION_MAJOR,
Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
double outputValue = sqrt(inputValue);
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}
(2) 構(gòu)建項(xiàng)目并執(zhí)行文件
官方并沒(méi)有給出如何構(gòu)建項(xiàng)目,這里以 VS 為例介紹如何構(gòu)建以上項(xiàng)目并編譯執(zhí)行。在該目錄下面建立 build 文件夾,并新建 run.cmd 文件,編寫(xiě)內(nèi)容如下:
echo off
echo build:
cmake -G "Visual Studio 15 2017 Win64" ..
echo compile:
devenv Tutorial.sln /build "Debug|x64"
echo run:
start ./Debug/Tutorial.exe %1
上面腳本中 echo命令主要是用來(lái)輸出提示信息,可以忽略。剩下一共有三行代碼。第3行代碼為使用 CMake 構(gòu)建工程文件.-G 參數(shù)用來(lái)指定編譯器,如果不寫(xiě)這里會(huì)找到一個(gè)默認(rèn)的編譯器。我這里默認(rèn)的編譯器就是 VS2017,但是默認(rèn)構(gòu)建的程序?yàn)?32 位程序,我這里顯示的指定使用 VS2017 構(gòu)建 64 位程序。
第5行代碼是使用命令行的形式編譯 VS 的 .sln 文件。關(guān)于命令行構(gòu)建 VS 項(xiàng)目這里不做過(guò)多介紹,有興趣可以參考微軟官方給出的 Devenv command line switches。當(dāng)然我們也可以使用 VS 打開(kāi) .sln 文件,然后手動(dòng)點(diǎn)擊 生成 。第7行代碼為運(yùn)行程序。
3. 添加一個(gè)庫(kù)文件(步驟2)
現(xiàn)在我們將會(huì)給我們的項(xiàng)目添加一個(gè)庫(kù)文件。這個(gè)庫(kù)文件包含了我們自己實(shí)現(xiàn)的開(kāi)方運(yùn)算。可執(zhí)行文件使用這個(gè)庫(kù)替代編譯器提供的標(biāo)準(zhǔn)開(kāi)方運(yùn)算。本教程中我們將其放到 MathFunctions 文件夾下,該文件夾下還有一個(gè)包含下面一行代碼的 CMakeLists.txt 文件。
add_library(MathFunctions mysqrt.cxx)
mysqrt.cxx 文件只有一個(gè)名為 mysqrt 的函數(shù),其提供了和標(biāo)準(zhǔn)庫(kù) sqrt 相同的功能。內(nèi)容如下(官網(wǎng)官方例程中可以找到):
#include "MathFunctions.h"
#include <stdio.h>
// a hack square root calculation using simple operations
double mysqrt(double x)
{
if (x <= 0) {
return 0;
}
double result;
double delta;
result = x;
// do ten iterations
int i;
for (i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
delta = x - (result * result);
result = result + 0.5 * delta / result;
fprintf(stdout, "Computing sqrt of %g to be %g\n", x, result);
}
return result;
}
對(duì)應(yīng)的頭文件為 MathFunction.h,其內(nèi)容如下:
double mysqrt(double x);
為了構(gòu)建并使用新的庫(kù)文件,我們需要在頂層 CMakeList.txt 文件添加 add_subdirectory 語(yǔ)句。我們需要添加額外的頭文件包含路徑,以便將包含函數(shù)原型的 MathFunctions/MathFunctions.h 頭文件包含進(jìn)來(lái)。最后我們還需要給可執(zhí)行文件添加庫(kù)。
最終頂層 CMakeList.txt 文件的最后幾行如下所示:
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)
現(xiàn)在我們考慮將 MathFunctions 庫(kù)作為一個(gè)可選項(xiàng)。雖然在這里并沒(méi)有什么必要,但是如果庫(kù)文件很大或者庫(kù)文件依賴第三方庫(kù)你可能就希望這么做了。首先先在頂層 CMakeLists.txt 文件添加一個(gè)選項(xiàng):
# should we use our own math functions?
option (USE_MYMATH
"Use tutorial provided math implementation" ON)
這個(gè)選項(xiàng)會(huì)在 CMake GUI 中顯示并會(huì)將默認(rèn)值設(shè)置為 ON,用戶可以根據(jù)需求修改該值。這個(gè)設(shè)置會(huì)本保存下來(lái),所以用戶無(wú)需在每次運(yùn)行 CMake 時(shí)都去設(shè)置。接下來(lái)就是將構(gòu)建和連接 MathFunctions 設(shè)置為可選項(xiàng)。修改頂層的 CMakeLists.txt 文件如下所示:
# add the MathFunctions library?
#
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})
這里使用 USE_MYMATH 來(lái)決定是否編譯并使用 MathFunctions 。注意收集可執(zhí)行文件的可選連接庫(kù)所使用的變量(這里為 EXTRA_LIBS)的使用方法。這種方法在保持有許多可選組件的大型項(xiàng)目整潔時(shí)經(jīng)常使用。對(duì)應(yīng)的我們修改源碼如下:
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif
int main (int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stdout,"%s Version %d.%d\n", argv[0],
Tutorial_VERSION_MAJOR,
Tutorial_VERSION_MINOR);
fprintf(stdout,"Usage: %s number\n",argv[0]);
return 1;
}
double inputValue = atof(argv[1]);
#ifdef USE_MYMATH
double outputValue = mysqrt(inputValue);
#else
double outputValue = sqrt(inputValue);
#endif
fprintf(stdout,"The square root of %g is %g\n",
inputValue, outputValue);
return 0;
}
在源碼中我們同樣使用了 USE_MYMATH 宏。這個(gè)宏由 CMake 通過(guò)在配置文件 TutorialConfig.h 添加以下代碼傳遞給源碼:
#cmakedefine USE_MYMATH
構(gòu)建、編譯和運(yùn)行使用的代碼和上一節(jié)相同。
4. 安裝和測(cè)試(步驟3)
下一步我們將給我們的項(xiàng)目添加安裝規(guī)則和測(cè)試。安裝規(guī)則簡(jiǎn)單明了。對(duì)于 MathFunctions 庫(kù)的安裝,我們通過(guò)在 MathFunction 的 CMakeLists.txt 文件中添加以下兩行來(lái)設(shè)置其庫(kù)和頭文件的安裝。
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
對(duì)于本文這個(gè)應(yīng)用通過(guò)在頂層 CMakeLists.txt 添加以下內(nèi)容來(lái)安裝可執(zhí)行文件和配置的頭文件:
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)
以上就是安裝的全部步驟?,F(xiàn)在你應(yīng)該可以編譯本教程了。輸入 make install(或在 IDE 中編譯 install 項(xiàng)目),對(duì)應(yīng)的頭文件、庫(kù)文件和可執(zhí)行文件就會(huì)被安裝。CMake 的 CMAKE_INSTALL_PREFIX 參數(shù)可以指定安裝文件的根目錄(之前還可以加上 -D 參數(shù),具體意義可以參考 what does the parameter "-D" mean)。 添加測(cè)試過(guò)程同樣簡(jiǎn)單明了。
在頂層 CMakeLists.txt 文件的最后我們可以添加一個(gè)基礎(chǔ)測(cè)試數(shù)據(jù)來(lái)驗(yàn)證該應(yīng)用程序是否正常運(yùn)行。
include(CTest)
# does the application run
add_test (TutorialRuns Tutorial 25)
# does it sqrt of 25
add_test (TutorialComp25 Tutorial 25)
set_tests_properties (TutorialComp25 PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")
# does it handle negative numbers
add_test (TutorialNegative Tutorial -25)
set_tests_properties (TutorialNegative PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")
# does it handle small numbers
add_test (TutorialSmall Tutorial 0.0001)
set_tests_properties (TutorialSmall PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number")
在編譯完成之后,我們可以運(yùn)行 "ctest" 命令行工具來(lái)執(zhí)行測(cè)試。第一個(gè)測(cè)試簡(jiǎn)單的驗(yàn)證了程序是否工作,是否有嚴(yán)重錯(cuò)誤并且返回0.這是 Ctest 測(cè)試的基礎(chǔ)。接下來(lái)的一些測(cè)試都使用了 PASS_REGULAR_EXPRESSION 測(cè)試屬性(正則表達(dá)式)來(lái)驗(yàn)證輸出中是否包含了特定的字符串。這里驗(yàn)證開(kāi)方是否正確并且在計(jì)算錯(cuò)誤時(shí)輸出輸出對(duì)應(yīng)信息。如果你希望添加很多的測(cè)試來(lái)測(cè)試不同的輸入值,你可以考慮定義一個(gè)像下面這樣的宏:
#define a macro to simplify adding tests, then use it
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)
# do a bunch of result based tests
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
每一次調(diào)用 do_test 就會(huì)根據(jù)指定的信息生成一個(gè)新的測(cè)試。
該步驟對(duì)應(yīng)的 build 文件夾下的構(gòu)建和運(yùn)行腳本 run.cmd 內(nèi)容如下:
echo off
echo build:
cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_INSTALL_PREFIX=D:\project\cpp\cmake\tutorial\step3\install ..
echo compile:
devenv Tutorial.sln /build "Debug|x64"
echo install:
devenv Tutorial.sln /build "Debug|x64" /project INSTALL
echo test:
devenv Tutorial.sln /build "Debug|x64" /project RUN_TESTS
安裝位置根據(jù)自己的需要進(jìn)行調(diào)整。
5. 添加系統(tǒng)自檢(步驟 4)
接下來(lái)我們考慮給我們的項(xiàng)目添加一些取決于目標(biāo)平臺(tái)是否有一些特性的代碼。這里我們將添加一些取決于目標(biāo)平臺(tái)是否有 log 和 exp 函數(shù)的代碼。當(dāng)然對(duì)于大多數(shù)平臺(tái)都會(huì)有這些函數(shù),但這里我們認(rèn)為這并不常見(jiàn)。如果平臺(tái)有 log 函數(shù)我們將在 mysqrt 函數(shù)中使用它計(jì)算平方根。我們首先在頂層 CMakeLists.txt 文件中使用 CheckFunctionExists 宏測(cè)試這些函數(shù)是否可用,代碼如下:
# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
一定要在使用 configure_file 生成 TutorialConfig.h 之前測(cè)試 log 和 exp。因?yàn)?configure_file 命令會(huì)立刻使用當(dāng)前 CMake 的設(shè)置配置文件。最后根據(jù) log 和 exp 是否在我們的平臺(tái)上可用我們給 mysqrt 函數(shù)提供一個(gè)可選的實(shí)現(xiàn),代碼如下:
// if we have both log and exp then use them
#if defined (HAVE_LOG) && defined (HAVE_EXP)
result = exp(log(x)*0.5);
#else // otherwise use an iterative approach
. . .
6. 添加一個(gè)生成的文件和生成器(步驟 5)
在這一章節(jié)我們將會(huì)展示如何在構(gòu)建一個(gè)應(yīng)用的過(guò)程中添加一個(gè)生成的源文件。在本例中我們將創(chuàng)建一個(gè)預(yù)先計(jì)算的平方根表作為構(gòu)建過(guò)程的一部分,然后將其編譯到我們的應(yīng)用中。為了做到這一點(diǎn)我們首先需要一個(gè)能產(chǎn)生這張表的程序。在 MathFunctions 文件夾下創(chuàng)建一個(gè)新的名為 MakeTable.cxx 的文件,內(nèi)容如下:
// A simple program that builds a sqrt table
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[])
{
int i;
double result;
// make sure we have enough arguments
if (argc < 2)
{
return 1;
}
// open the output file
FILE *fout = fopen(argv[1],"w");
if (!fout)
{
return 1;
}
// create a source file with a table of square roots
fprintf(fout,"double sqrtTable[] = {\n");
for (i = 0; i < 10; ++i)
{
result = sqrt(static_cast<double>(i));
fprintf(fout,"%g,\n",result);
}
// close the table with a zero
fprintf(fout,"0};\n");
fclose(fout);
return 0;
}
注意到這張表使用 C++ 代碼生成且文件的名字通過(guò)輸入?yún)?shù)指定。下一步通過(guò)在 MathFunctions 的CMakeLists.txt 中添加合適的代碼來(lái)構(gòu)建 MakeTable 可執(zhí)行文件,并將它作為構(gòu)建的一部分運(yùn)行。只需要一點(diǎn)代碼就能實(shí)現(xiàn)這個(gè)功能:
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)
# add the binary tree directory to the search path for
# include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h )
首先添加的 MakeTable 可執(zhí)行文件和其它可執(zhí)行文件相同。接下來(lái)我們添加一個(gè)自定義的命令來(lái)指定如何通過(guò)運(yùn)行 MakeTable 生成 Table.h 文件。接下來(lái)我們必須讓 CMake 知道 mysqrt.cxx 依賴于生成的 Table.h 文件。這一點(diǎn)通過(guò)將生成的 Table.h 文件添加到 MathFunctions 庫(kù)的源文件列表實(shí)現(xiàn)。
我們同樣必須將當(dāng)前二進(jìn)制文件路徑添加到包含路徑中,以保證 Table.h 文件被找到并被 mysqrt.cxx 包含。該項(xiàng)目在構(gòu)建時(shí)會(huì)首先構(gòu)建 MakeTable 可執(zhí)行文件。接下來(lái)會(huì)運(yùn)行該可執(zhí)行文件并生成 Table.h 文件。最后它將會(huì)編譯包含 Table.h 的 mysqrt.cxx 文件并生成 MathFunctions 庫(kù)。此時(shí)包含了所有我們添加的特性的頂層 CMakeLists.txt 文件應(yīng)該像下面這樣:
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
include(CTest)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
# should we use our own math functions
option(USE_MYMATH
"Use tutorial provided math implementation" ON)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories ("${PROJECT_BINARY_DIR}")
# add the MathFunctions library?
if (USE_MYMATH)
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions)
set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial ${EXTRA_LIBS})
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include)
# does the application run
add_test (TutorialRuns Tutorial 25)
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
PROPERTIES
PASS_REGULAR_EXPRESSION "Usage:.*number"
)
#define a macro to simplify adding tests
macro (do_test arg result)
add_test (TutorialComp${arg} Tutorial ${arg})
set_tests_properties (TutorialComp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endmacro (do_test)
# do a bunch of result based tests
do_test (4 "4 is 2")
do_test (9 "9 is 3")
do_test (5 "5 is 2.236")
do_test (7 "7 is 2.645")
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
do_test (0.0001 "0.0001 is 0.01")
TutorialConfig.h 文件如下:
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP
最后 MathFunctions 的 CMakeLists.txt 文件如下:
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
# add the binary tree directory to the search path
# for include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)
7. 構(gòu)造一個(gè)安裝器(步驟 6)
接下來(lái)假設(shè)我們想將我們的項(xiàng)目發(fā)布給其他人以便供他們使用。我們想提供在不同平臺(tái)上的二進(jìn)制文件和源碼的發(fā)布版本。這一點(diǎn)和我們?cè)谥鞍惭b和測(cè)試章節(jié)(步驟3)略有不同,步驟三安裝的二進(jìn)制文件是我們從源碼構(gòu)建的。這里我們將構(gòu)建一個(gè)支持二進(jìn)制文件安裝的安裝包和可以在 cygwin,debian,RPMs 等中被找到的安裝管理特性。為了實(shí)現(xiàn)這一點(diǎn)我們將使用 CPack 來(lái)創(chuàng)建在 Packaging with CPack 章節(jié)中介紹過(guò)的平臺(tái)特定安裝器(platform specific installers)。我們需要在頂層 CMakeLists.txt 文件添加以下幾行內(nèi)容:
# build a CPack driven installer package
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
"${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include (CPack)
首先我們添加了 InstallRequiredSystemLibraries。該模塊會(huì)包含我們項(xiàng)目在當(dāng)前平臺(tái)所需的所有運(yùn)行時(shí)庫(kù)(runtime libraries)。接下來(lái)我們?cè)O(shè)置了一些 CPack 變量來(lái)指定我們項(xiàng)目的許可文件和版本信息。版本信息使用我們?cè)谥霸O(shè)置的內(nèi)容。最后我們包含 CPack 模塊,它會(huì)使用這些變量和其它你安裝一個(gè)應(yīng)用程序所需的系統(tǒng)屬性。
接下來(lái)就是正常編譯你的項(xiàng)目然后使用 CPack 運(yùn)行它,為了編譯二進(jìn)制發(fā)布版本你需要運(yùn)行:
cpack --config CPackConfig.cmake
創(chuàng)建一個(gè)源文件發(fā)布版本你應(yīng)該使用下面命令:
cpack --config CPackSourceConfig.cmake
Windows 平臺(tái)下CMake 默認(rèn)會(huì)使用 NSIS 創(chuàng)建安裝包,因此我們?cè)趫?zhí)行上面命令前需要安裝該軟件。當(dāng)然我們也可以使用 WiX 包安裝工具,只需要在 include(CPack) 之前加上 set(CPACK_GENERATOR WIX) 即可。
8. 添加表盤(pán)工具(Dashboard)支持(步驟7)
添加將我們測(cè)試結(jié)果提交到儀表盤(pán)的功能非常簡(jiǎn)單。在本教程的之前步驟中我們已經(jīng)給我們的項(xiàng)目定義了一些測(cè)試。我們只需要運(yùn)行這些測(cè)試然后提交到儀表盤(pán)即可。為了支持儀表盤(pán)功能我們需要在頂層 CMakeLists.txt 文件中增加 CTest 模塊。
# enable dashboard scripting
include (CTest)
我們同樣可以創(chuàng)建一個(gè) CTestConfig.cmake 文件來(lái)在表盤(pán)工具中指定本項(xiàng)目的名字。
set (CTEST_PROJECT_NAME "Tutorial")
CTest 會(huì)在運(yùn)行時(shí)讀取該文件。你可以在你的項(xiàng)目上運(yùn)行 CMake 來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的儀表盤(pán),切換目錄到二進(jìn)制文件夾下,然后運(yùn)行 ctest -DExperimental.你儀表盤(pán)的運(yùn)行結(jié)果會(huì)上傳到 Kitware 的公共儀表盤(pán)上 這里。
如果需要上傳的話還需要設(shè)置 Drop site ,具體細(xì)節(jié)可以參考官方的 ctest(1) 。
三、CMake常用命令詳解
在深入了解 CMake 的世界后,我們來(lái)到了一個(gè)關(guān)鍵的階段 —— 學(xué)習(xí) CMake 的常用命令。這些命令是我們編寫(xiě)CMakeLists.txt文件的基礎(chǔ),掌握它們,就如同掌握了一門(mén)新語(yǔ)言的語(yǔ)法規(guī)則,能夠讓我們自由地構(gòu)建和管理項(xiàng)目。下面,我們將詳細(xì)介紹一些 CMake 的常用命令,讓你在實(shí)際項(xiàng)目中能夠靈活運(yùn)用。
1. 工程管理命令
(1) include_directories:配置頭文件路徑
在項(xiàng)目中,頭文件是不可或缺的部分,它包含了函數(shù)、類(lèi)的聲明等重要信息。include_directories命令用于指定頭文件的搜索路徑,確保編譯器在編譯源文件時(shí)能夠找到所需的頭文件 。其基本語(yǔ)法如下:
include_directories([AFTER|BEFORE][SYSTEM] dir1 [dir2 ...])
上述示例將include目錄添加到頭文件搜索路徑中,這樣在編譯時(shí),編譯器會(huì)在include目錄中查找頭文件。不過(guò),更推薦使用target_include_directories,它針對(duì)特定目標(biāo)設(shè)置包含目錄,作用域更明確,可維護(hù)性更好 。
(2) add_executable:此命令用于創(chuàng)建一個(gè)可執(zhí)行文件目標(biāo)
通過(guò)指定可執(zhí)行文件的名稱(chēng)和源文件列表,CMake 會(huì)將這些源文件編譯鏈接成一個(gè)可執(zhí)行文件 。在一個(gè)簡(jiǎn)單的 C 項(xiàng)目中,有main.c和utils.c兩個(gè)源文件,使用add_executable命令可以這樣編寫(xiě):
add_executable(my_program main.c utils.c)
這里my_program是生成的可執(zhí)行文件的名稱(chēng),main.c和utils.c是參與編譯的源文件。如果源文件較多,也可以將源文件列表放在一個(gè)變量中,然后在add_executable中使用該變量 。
(3) add_library:用于創(chuàng)建一個(gè)庫(kù)文件目標(biāo),可以生成靜態(tài)庫(kù)(STATIC)或動(dòng)態(tài)庫(kù)(SHARED)
在一個(gè) C++ 庫(kù)項(xiàng)目中,有l(wèi)ibrary.cpp源文件,希望生成一個(gè)動(dòng)態(tài)庫(kù),命令如下:
add_library(my_library SHARED library.cpp)
其中my_library是庫(kù)的名稱(chēng),SHARED表示生成動(dòng)態(tài)庫(kù),如果要生成靜態(tài)庫(kù),將SHARED改為STATIC即可 。
(4) add_subdirectory:用于包含子目錄的 CMakeLists.txt 文件,從而將子目錄的項(xiàng)目納入到整個(gè)項(xiàng)目的構(gòu)建過(guò)程中
在一個(gè)大型項(xiàng)目中,可能會(huì)有多個(gè)模塊,每個(gè)模塊都有自己獨(dú)立的目錄和 CMakeLists.txt 文件,使用add_subdirectory可以方便地管理這些模塊的構(gòu)建 。假設(shè)項(xiàng)目目錄結(jié)構(gòu)如下:
project/
├── CMakeLists.txt
├── module1/
│ └── CMakeLists.txt
└── module2/
└── CMakeLists.txt
在項(xiàng)目根目錄的CMakeLists.txt中,可以這樣包含子目錄:
add_subdirectory(module1)
add_subdirectory(module2)
這樣,module1和module2目錄下的 CMakeLists.txt 文件會(huì)被執(zhí)行,其中定義的構(gòu)建規(guī)則和目標(biāo)會(huì)被納入到整個(gè)項(xiàng)目的構(gòu)建中 。
(5) include:用于引入其他.cmake文件,這些文件通常包含一些自定義的函數(shù)、宏或配置信息,通過(guò)include可以在當(dāng)前的 CMakeLists.txt 文件中復(fù)用這些內(nèi)容 。在項(xiàng)目中,有一個(gè)自定義的.cmake文件common.cmake,其中定義了一些常用的編譯選項(xiàng)和宏,在主 CMakeLists.txt 文件中可以這樣引入:
include(common.cmake)
引入后,common.cmake中的內(nèi)容就可以在當(dāng)前文件中使用了,這有助于提高代碼的復(fù)用性和項(xiàng)目的可維護(hù)性 。
2. 開(kāi)關(guān)選項(xiàng)命令
(1) option:該命令用于定義一個(gè)開(kāi)關(guān)選項(xiàng),在項(xiàng)目配置時(shí)可以通過(guò)命令行或 CMake 圖形界面來(lái)設(shè)置這個(gè)選項(xiàng)的值,從而控制項(xiàng)目的構(gòu)建行為 。在一個(gè)項(xiàng)目中,可能希望有一個(gè)選項(xiàng)來(lái)控制是否啟用某個(gè)功能模塊,使用option命令可以這樣定義:
option(ENABLE_FEATURE "Enable a specific feature" OFF)
if(ENABLE_FEATURE)
# 啟用功能模塊的相關(guān)配置
add_definitions(-DENABLE_FEATURE)
include_directories(feature_include)
add_executable(my_program main.c feature.c)
else()
# 不啟用功能模塊的配置
add_executable(my_program main.c)
endif()
上述示例中,ENABLE_FEATURE是定義的選項(xiàng)名稱(chēng),"Enable a specific feature"是選項(xiàng)的描述信息,OFF表示該選項(xiàng)的默認(rèn)值為關(guān)閉。在項(xiàng)目配置時(shí),可以通過(guò)cmake -DENABLE_FEATURE=ON ..來(lái)啟用該功能模塊 。
(2) add_definition:用于在源碼中定義宏,這些宏可以在代碼中通過(guò)#ifdef等預(yù)處理指令進(jìn)行條件編譯 。在一個(gè) C++ 項(xiàng)目中,希望定義一個(gè)DEBUG宏來(lái)控制調(diào)試信息的輸出,使用add_definition命令可以這樣編寫(xiě):
add_definitions(-DDEBUG)
這樣在編譯時(shí),DEBUG宏會(huì)被定義,在代碼中就可以通過(guò)#ifdef DEBUG來(lái)判斷是否處于調(diào)試模式,并進(jìn)行相應(yīng)的代碼處理 。
3. 調(diào)試信息命令
(1) message:用于在 CMake 執(zhí)行過(guò)程中向終端輸出信息,它有多種輸出模式,不同的模式用于不同的目的 。
(2) STATUS:輸出的信息會(huì)被發(fā)送到 CMake 的狀態(tài)消息流,通常用于輸出構(gòu)建過(guò)程中的狀態(tài)信息,在命令行上,這些消息會(huì)被顯示出來(lái),幫助開(kāi)發(fā)者了解構(gòu)建的進(jìn)度和當(dāng)前狀態(tài) 。
message(STATUS "Building project...")
(3) INFO:輸出一般性的信息,類(lèi)似于STATUS,但沒(méi)有明確的區(qū)分,通常用于輸出一些開(kāi)發(fā)者希望關(guān)注的信息 。
message(INFO "This is an important information.")
(4) WARNING:輸出的信息會(huì)被發(fā)送到 CMake 的警告消息流,這些消息會(huì)被標(biāo)記為警告,通常用于提示開(kāi)發(fā)者一些可能存在的問(wèn)題,但不會(huì)導(dǎo)致構(gòu)建失敗 。
if(NOT SOME_LIBRARY_FOUND)
message(WARNING "Some library not found, some features may not be available.")
endif()
(5) FATAL_ERROR:輸出的信息會(huì)被發(fā)送到 CMake 的錯(cuò)誤消息流,并立即停止 CMake 的處理過(guò)程,通常用于表示嚴(yán)重的錯(cuò)誤,如缺少關(guān)鍵的依賴項(xiàng)或配置錯(cuò)誤 。
if(NOT REQUIRED_TOOL_FOUND)
message(FATAL_ERROR "Required tool not found, cannot continue building.")
endif()
4. 其他重要變量
(1) CMAKE_C_FLAGS、CMAKE_CXX_FLAGS:這兩個(gè)變量分別用于控制 C 和 C++ 的編譯選項(xiàng) 。在一個(gè) C++ 項(xiàng)目中,希望設(shè)置編譯選項(xiàng)為-Wall -Werror,以開(kāi)啟所有警告并將警告視為錯(cuò)誤,可以這樣設(shè)置:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
這樣在編譯 C++ 源文件時(shí),就會(huì)帶上-Wall -Werror編譯選項(xiàng) 。如果項(xiàng)目中同時(shí)有 C 和 C++ 代碼,也可以分別設(shè)置CMAKE_C_FLAGS和CMAKE_CXX_FLAGS 。
(2) CMAKE_BUILD_TYPE:該變量用于指定構(gòu)建類(lèi)型,常見(jiàn)的構(gòu)建類(lèi)型有Debug、Release、MinSizeRel(最小尺寸發(fā)布,優(yōu)化目標(biāo)是減小文件大小)和RelWithDebInfo(發(fā)布版本且包含調(diào)試信息) 。在項(xiàng)目中,可以通過(guò)以下方式設(shè)置構(gòu)建類(lèi)型:
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
上述示例中,如果沒(méi)有通過(guò)命令行或其他方式指定構(gòu)建類(lèi)型,默認(rèn)設(shè)置為Release。在命令行中也可以通過(guò)cmake -DCMAKE_BUILD_TYPE=Debug ..來(lái)指定構(gòu)建類(lèi)型為Debug 。
四、CMake高級(jí)應(yīng)用
1. 自定義構(gòu)建選項(xiàng)
在實(shí)際項(xiàng)目開(kāi)發(fā)中,根據(jù)不同的需求和場(chǎng)景進(jìn)行條件編譯是非常常見(jiàn)的操作,而 CMake 提供的option命令就為我們實(shí)現(xiàn)這一功能提供了便利。
option命令用于定義一個(gè)開(kāi)關(guān)選項(xiàng),它的基本語(yǔ)法是option(OPTION_NAME "Description" DEFAULT_VALUE),其中OPTION_NAME是選項(xiàng)的名稱(chēng),"Description"是對(duì)該選項(xiàng)的描述信息,方便用戶了解其作用,DEFAULT_VALUE則是選項(xiàng)的默認(rèn)值,可以是ON或OFF。
例如,在一個(gè)圖像處理項(xiàng)目中,可能希望有一個(gè)選項(xiàng)來(lái)控制是否啟用高級(jí)圖像算法??梢栽贑MakeLists.txt文件中這樣定義:
option(ENABLE_ADVANCED_ALGORITHM "Enable advanced image algorithm" OFF)
if(ENABLE_ADVANCED_ALGORITHM)
# 啟用高級(jí)圖像算法的相關(guān)配置
add_definitions(-DENABLE_ADVANCED_ALGORITHM)
include_directories(advanced_algorithm_include)
add_executable(image_processing main.cpp advanced_algorithm.cpp)
else()
# 不啟用高級(jí)圖像算法的配置
add_executable(image_processing main.cpp basic_algorithm.cpp)
endif()
在上述示例中,ENABLE_ADVANCED_ALGORITHM是定義的選項(xiàng)名稱(chēng),"Enable advanced image algorithm"是描述信息,OFF表示默認(rèn)不啟用該選項(xiàng)。當(dāng)通過(guò)cmake -DENABLE_ADVANCED_ALGORITHM=ON ..命令啟用該選項(xiàng)時(shí),if條件判斷為真,會(huì)添加相應(yīng)的編譯定義-DENABLE_ADVANCED_ALGORITHM,并將高級(jí)圖像算法的源文件advanced_algorithm.cpp包含到可執(zhí)行文件的構(gòu)建中;如果不啟用該選項(xiàng),else分支會(huì)被執(zhí)行,使用基本圖像算法的源文件basic_algorithm.cpp進(jìn)行構(gòu)建。
2. 查找和管理依賴庫(kù)
在項(xiàng)目開(kāi)發(fā)中,經(jīng)常會(huì)依賴一些外部庫(kù)來(lái)實(shí)現(xiàn)特定的功能,而 CMake 的find_package命令就是查找和管理這些依賴庫(kù)的重要工具。
以查找和使用 Boost 庫(kù)為例,find_package命令的基本語(yǔ)法是find_package(Boost [version] [EXACT] [QUIET] [MODULE] [REQUIRED] [COMPONENTS [components...]] [OPTIONAL_COMPONENTS [components...]] [CONFIG|NO_MODULE|NO_CONFIG|NO_POLICY_SCOPE] [NAMES name1 [...]])。
假設(shè)我們的項(xiàng)目需要使用 Boost 庫(kù)的文件系統(tǒng)和線程組件,并且要求找到的 Boost 庫(kù)版本至少為 1.70,可以在CMakeLists.txt文件中這樣編寫(xiě):
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem thread)
if(Boost_FOUND)
message(STATUS "Boost include dir: ${Boost_INCLUDE_DIRS}")
message(STATUS "Boost libraries: ${Boost_LIBRARIES}")
include_directories(${Boost_INCLUDE_DIRS})
target_link_libraries(your_target_name ${Boost_LIBRARIES})
else()
message(FATAL_ERROR "Boost library not found.")
endif()
上述代碼中,find_package(Boost 1.70 REQUIRED COMPONENTS filesystem thread)表示查找 Boost 庫(kù),要求版本至少為 1.70,并且必須找到文件系統(tǒng)和線程組件。如果找到 Boost 庫(kù),Boost_FOUND變量會(huì)被設(shè)置為真,然后通過(guò)message命令輸出 Boost 庫(kù)的包含目錄和庫(kù)文件路徑,再使用include_directories將 Boost 庫(kù)的包含目錄添加到項(xiàng)目中,使用target_link_libraries將 Boost 庫(kù)鏈接到項(xiàng)目的目標(biāo)(your_target_name)上;如果沒(méi)有找到 Boost 庫(kù),Boost_FOUND為假,通過(guò)message(FATAL_ERROR "Boost library not found.")輸出錯(cuò)誤信息并終止構(gòu)建過(guò)程。
3. 生成安裝目標(biāo)
在項(xiàng)目開(kāi)發(fā)完成后,通常需要將生成的可執(zhí)行文件、庫(kù)文件、配置文件等安裝到指定的目錄中,以便用戶能夠方便地使用和部署。CMake 的install命令就提供了這樣的功能,它可以將各種文件和目標(biāo)安裝到指定的位置。
(1)安裝可執(zhí)行文件:
add_executable(my_app main.cpp)
install(TARGETS my_app RUNTIME DESTINATION bin)
上述代碼中,add_executable創(chuàng)建了一個(gè)名為my_app的可執(zhí)行文件,install命令將my_app的運(yùn)行時(shí)文件(即生成的可執(zhí)行文件)安裝到bin目錄下。這里的bin目錄可以是絕對(duì)路徑,也可以是相對(duì)于CMAKE_INSTALL_PREFIX的相對(duì)路徑,CMAKE_INSTALL_PREFIX是 CMake 的一個(gè)預(yù)定義變量,用于指定安裝路徑的前綴,默認(rèn)情況下在 Unix 系統(tǒng)中是/usr/local,在 Windows 系統(tǒng)中是C:/Program Files (x86)/項(xiàng)目名稱(chēng) 。如果希望安裝到自定義的絕對(duì)路徑,如/home/user/my_project/bin,可以將DESTINATION設(shè)置為/home/user/my_project/bin 。
(2)安裝庫(kù)文件:
add_library(my_lib SHARED lib.cpp)
install(TARGETS my_lib LIBRARY DESTINATION lib ARCHIVE DESTINATION lib/static)
這里add_library創(chuàng)建了一個(gè)共享庫(kù)my_lib,install命令將共享庫(kù)文件安裝到lib目錄下,將歸檔文件(通常是靜態(tài)庫(kù)文件,如果有的話)安裝到lib/static目錄下。同樣,lib和lib/static可以是相對(duì)路徑或絕對(duì)路徑 。
(3)安裝普通文件:
install(FILES config.ini DESTINATION etc)
該示例將config.ini文件安裝到etc目錄下,config.ini是相對(duì)于CMakeLists.txt文件所在目錄的路徑 。
(4)安裝目錄:
install(DIRECTORY include/ DESTINATION include FILES_MATCHING PATTERN "*.h")
上述代碼會(huì)將include目錄下所有擴(kuò)展名為.h的頭文件安裝到include目錄下,這里的兩個(gè)include目錄可以根據(jù)實(shí)際需求進(jìn)行修改 。如果希望安裝整個(gè)include目錄及其所有內(nèi)容,包括子目錄,可以將FILES_MATCHING相關(guān)部分去掉 。
五、常見(jiàn)問(wèn)題及解決方案
在使用 CMake 的過(guò)程中,開(kāi)發(fā)者可能會(huì)遇到各種各樣的問(wèn)題,下面將列舉一些常見(jiàn)問(wèn)題,并提供相應(yīng)的解決方案。
1. 找不到依賴庫(kù)
在編譯項(xiàng)目時(shí),CMake 可能會(huì)提示找不到某個(gè)依賴庫(kù),這是一個(gè)比較常見(jiàn)的問(wèn)題。比如在編譯一個(gè)使用 OpenCV 庫(kù)的項(xiàng)目時(shí),可能會(huì)出現(xiàn) “Could not find a package configuration file provided by “OpenCV” with any of the following names: OpenCVConfig.cmake, opencv-config.cmake” 這樣的錯(cuò)誤提示 。
原因分析:造成這個(gè)問(wèn)題的原因可能有多種。首先,可能是依賴庫(kù)沒(méi)有正確安裝,例如沒(méi)有安裝 OpenCV 庫(kù)或者安裝路徑不正確;其次,CMake 可能無(wú)法自動(dòng)檢測(cè)到依賴庫(kù)的位置,這可能是因?yàn)閹?kù)的安裝路徑?jīng)]有被添加到系統(tǒng)的環(huán)境變量中,或者 CMake 的搜索路徑設(shè)置不正確 。
解決方案:針對(duì)這些問(wèn)題,可以采取以下解決措施。首先,確保已經(jīng)正確安裝了相應(yīng)的依賴庫(kù)。如果已經(jīng)安裝了依賴庫(kù),但 CMake 仍然找不到它,可以通過(guò)設(shè)置 CMake 變量來(lái)指定庫(kù)的路徑 。例如,使用set()命令設(shè)置CMAKE_PREFIX_PATH變量,將庫(kù)的安裝路徑添加到其中:
set(CMAKE_PREFIX_PATH "/path/to/library" ${CMAKE_PREFIX_PATH})
另外,還可以使用find_path()或find_library()函數(shù),并明確指定依賴庫(kù)的搜索路徑 。例如:
find_package(Boost REQUIRED PATHS /path/to/boost)
如果依賴是在系統(tǒng)級(jí)安裝的,確認(rèn)相關(guān)的環(huán)境變量(如LD_LIBRARY_PATH或PYTHONPATH)已經(jīng)設(shè)置正確 。對(duì)于預(yù)打包的庫(kù),檢查操作系統(tǒng)是否有對(duì)應(yīng)的軟件包,并使用系統(tǒng)的包管理工具安裝 。此外,查閱依賴庫(kù)的官方文檔,了解特定的 CMake 配置步驟也是很有必要的 。
2. 編譯選項(xiàng)錯(cuò)誤
在 CMakeLists.txt 文件中設(shè)置編譯選項(xiàng)時(shí),可能會(huì)因?yàn)樵O(shè)置錯(cuò)誤而導(dǎo)致編譯失敗。比如在設(shè)置 C++ 編譯選項(xiàng)時(shí),將CMAKE_CXX_FLAGS變量設(shè)置錯(cuò)誤,可能會(huì)出現(xiàn) “unrecognized command line option” 這樣的錯(cuò)誤提示 。
原因分析:這種錯(cuò)誤通常是由于對(duì)編譯選項(xiàng)的不熟悉或者拼寫(xiě)錯(cuò)誤導(dǎo)致的。不同的編譯器支持的編譯選項(xiàng)可能會(huì)有所不同,如果設(shè)置了不支持的編譯選項(xiàng),就會(huì)出現(xiàn)此類(lèi)錯(cuò)誤 。
解決方案:解決這個(gè)問(wèn)題,首先要仔細(xì)檢查 CMakeLists.txt 文件中編譯選項(xiàng)的設(shè)置,確保拼寫(xiě)正確,并且是目標(biāo)編譯器支持的選項(xiàng) 。例如,如果希望設(shè)置 C++ 編譯選項(xiàng)為-Wall -Werror,以開(kāi)啟所有警告并將警告視為錯(cuò)誤,可以這樣設(shè)置:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
如果不確定某個(gè)編譯選項(xiàng)是否被支持,可以查閱目標(biāo)編譯器的官方文檔 。另外,在設(shè)置編譯選項(xiàng)時(shí),可以使用條件判斷來(lái)根據(jù)不同的編譯器或構(gòu)建類(lèi)型設(shè)置不同的選項(xiàng) 。例如:
if(CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
elseif(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX")
endif()
上述代碼中,根據(jù)編譯器類(lèi)型分別設(shè)置了不同的編譯選項(xiàng),對(duì)于 GCC 編譯器,設(shè)置-Wall -Werror選項(xiàng),對(duì)于 MSVC 編譯器,設(shè)置/W4 /WX選項(xiàng) 。
3. 頭文件路徑設(shè)置錯(cuò)誤
在項(xiàng)目中,頭文件路徑設(shè)置不正確也會(huì)導(dǎo)致編譯錯(cuò)誤,例如出現(xiàn) “fatal error: xxx.h: No such file or directory” 這樣的錯(cuò)誤提示 。
原因分析:這通常是因?yàn)樵?CMakeLists.txt 文件中沒(méi)有正確設(shè)置頭文件的搜索路徑,或者設(shè)置的路徑與實(shí)際頭文件的位置不一致 。
解決方案:可以使用include_directories()命令將頭文件所在的路徑添加到編譯過(guò)程中。例如:
include_directories(include)
上述示例將include目錄添加到頭文件搜索路徑中 。不過(guò),更推薦使用target_include_directories(),它針對(duì)特定目標(biāo)設(shè)置包含目錄,作用域更明確,可維護(hù)性更好 。例如:
add_executable(my_program main.c)
target_include_directories(my_program PRIVATE include)
這里PRIVATE表示該包含目錄僅對(duì)my_program目標(biāo)可見(jiàn),如果頭文件路徑是公共的也可以使用PUBLIC關(guān)鍵字 。
4. 構(gòu)建類(lèi)型設(shè)置錯(cuò)誤
在項(xiàng)目中,如果構(gòu)建類(lèi)型設(shè)置錯(cuò)誤,可能會(huì)導(dǎo)致生成的可執(zhí)行文件或庫(kù)文件不符合預(yù)期,例如在需要生成 Release 版本的可執(zhí)行文件時(shí),卻生成了 Debug 版本 。
原因分析:這種問(wèn)題通常是由于沒(méi)有正確設(shè)置CMAKE_BUILD_TYPE變量,或者在命令行中沒(méi)有正確指定構(gòu)建類(lèi)型導(dǎo)致的 。
解決方案:可以在 CMakeLists.txt 文件中設(shè)置CMAKE_BUILD_TYPE變量的默認(rèn)值 。例如:
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
上述示例中,如果沒(méi)有通過(guò)命令行或其他方式指定構(gòu)建類(lèi)型,默認(rèn)設(shè)置為Release 。在命令行中也可以通過(guò)cmake -DCMAKE_BUILD_TYPE=Debug ..來(lái)指定構(gòu)建類(lèi)型為Debug 。
六、CMake在Clion中的配置
1. Ubuntu 下 Clion 的安裝
clion 及相關(guān)工具安裝:
# clion 所用到的工具鏈:gcc(C),g++(C++),make(連接),cmake(跨平臺(tái)建構(gòu)系統(tǒng)), gdb(debug)
sudo apt install gcc
sudo apt install g++
sudo apt install make
sudo apt install cmake
sudo apt install gdb
# Install using the Toolbox App or Standalone installation
sudo tar -xzf jetbrains-toolbox* -C /opt
# Standalone installation
sudo tar xvzf CLion-*.tar.gz -C /opt/
sh /opt/clion-*/bin/clion.sh
# create a desktop entry, do one of the following:
- On the Welcome screen, click Configure | Create Desktop Entry
- From the main menu, click Tools | Create Desktop Entry
如果沒(méi)有桌面快捷方式,嘗試用如下方法解決:
sudo vim /usr/share/applications/clion.desktop
# 插入如下內(nèi)容,保存退出即可,在 search 里面就可以找到 clion 了
[Desktop Entry]
Encoding=UTF-8
Name=CLion
Comment=clion-2020.1.2
Exec=/opt/clion-2020.1.2/bin/clion.sh
Icnotallow=/opt/clion-2020.1.2/bin/clion.svg
Categories=Application;Development;Java;IDE
Versinotallow=2020.1.2
Type=Application
#Terminal=1
2. 如何在 clion 運(yùn)行多個(gè) cpp 文件 ?
直接修改 CMakeLists.txt 即可。
新建 CPP 文件時(shí)注意:把默認(rèn)勾選的 Add to targerts 去掉(如下圖);在項(xiàng)目處右擊,選擇 Reload CMake Project,在重新加載完之后可以看到運(yùn)行框列表有了對(duì)應(yīng)的運(yùn)行選項(xiàng)
cmake_minimum_required(VERSION 3.16)
project(cpp14)
set(CMAKE_CXX_STANDARD 14)
# 遞歸遍歷項(xiàng)目當(dāng)前目錄下所有的 .cpp 文件
file(GLOB_RECURSE files *.cpp)
foreach (file ${files})
string(REGEX REPLACE ".+/(.+)\\..*" "\\1" exe ${file}) # 正則匹配,取出文件名前綴
add_executable(${exe} ${file}) # exe 文件名, file 為文件絕對(duì)路徑
message("src file name is: " ${exe})
message(STATUS "src file path is: " ${file})
endforeach (file)