CMake構(gòu)建學(xué)習(xí)筆記15-組建第一個(gè)程序項(xiàng)目
1 概述
在前文中論述的都是如何使用CMake構(gòu)建第三方依賴庫,不過這些庫都是別人的程序項(xiàng)目,那么如何使用CMake組織構(gòu)建一個(gè)屬于自己的C/C++程序項(xiàng)目呢?本文我們就來實(shí)現(xiàn)一個(gè)使用CMake組建的C/C++項(xiàng)目。
2 具體案例
2.1 代碼編寫
就不去寫很簡(jiǎn)單的打印HelloWorld案例了,那種簡(jiǎn)單的案例實(shí)用的意義并不大,至少我們得使用調(diào)用一個(gè)第三方的依賴庫的例子。正好筆者寫過一個(gè)使用libzip壓縮文件和文件夾的例子,源代碼文件main.cpp如下所示:
#include <zip.h>
#include <filesystem>
#include <fstream>
#include <iostream>
using namespace std;
void CompressFile2Zip(std::filesystem::path unZipFilePath,
const char* relativeName, zip_t* zipArchive) {
std::ifstream file(unZipFilePath, std::ios::binary);
file.seekg(0, std::ios::end);
size_t bufferSize = file.tellg();
char* bufferData = (char*)malloc(bufferSize);
file.seekg(0, std::ios::beg);
file.read(bufferData, bufferSize);
//第四個(gè)參數(shù)如果非0,會(huì)自動(dòng)托管申請(qǐng)的資源,直到zip_close之前自動(dòng)銷毀。
zip_source_t* source =
zip_source_buffer(zipArchive, bufferData, bufferSize, 1);
if (source) {
if (zip_file_add(zipArchive, relativeName, source, ZIP_FL_OVERWRITE) < 0) {
std::cerr << "Failed to add file " << unZipFilePath
<< " to zip: " << zip_strerror(zipArchive) << std::endl;
zip_source_free(source);
}
} else {
std::cerr << "Failed to create zip source for " << unZipFilePath << ": "
<< zip_strerror(zipArchive) << std::endl;
}
}
void CompressFile(std::filesystem::path unZipFilePath,
std::filesystem::path zipFilePath) {
int errorCode = 0;
zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),
ZIP_CREATE | ZIP_TRUNCATE, &errorCode);
if (zipArchive) {
CompressFile2Zip(unZipFilePath, unZipFilePath.filename().string().c_str(),
zipArchive);
errorCode = zip_close(zipArchive);
if (errorCode != 0) {
zip_error_t zipError;
zip_error_init_with_code(&zipError, errorCode);
std::cerr << zip_error_strerror(&zipError) << std::endl;
zip_error_fini(&zipError);
}
} else {
zip_error_t zipError;
zip_error_init_with_code(&zipError, errorCode);
std::cerr << "Failed to open output file " << zipFilePath << ": "
<< zip_error_strerror(&zipError) << std::endl;
zip_error_fini(&zipError);
}
}
void CompressDirectory2Zip(std::filesystem::path rootDirectoryPath,
std::filesystem::path directoryPath,
zip_t* zipArchive) {
if (rootDirectoryPath != directoryPath) {
if (zip_dir_add(zipArchive,
std::filesystem::relative(directoryPath, rootDirectoryPath)
.generic_u8string()
.c_str(),
ZIP_FL_ENC_UTF_8) < 0) {
std::cerr << "Failed to add directory " << directoryPath
<< " to zip: " << zip_strerror(zipArchive) << std::endl;
}
}
for (const auto& entry : std::filesystem::directory_iterator(directoryPath)) {
if (entry.is_regular_file()) {
CompressFile2Zip(
entry.path().generic_u8string(),
std::filesystem::relative(entry.path(), rootDirectoryPath)
.generic_u8string()
.c_str(),
zipArchive);
} else if (entry.is_directory()) {
CompressDirectory2Zip(rootDirectoryPath, entry.path().generic_u8string(),
zipArchive);
}
}
}
void CompressDirectory(std::filesystem::path directoryPath,
std::filesystem::path zipFilePath) {
int errorCode = 0;
zip_t* zipArchive = zip_open(zipFilePath.generic_u8string().c_str(),
ZIP_CREATE | ZIP_TRUNCATE, &errorCode);
if (zipArchive) {
CompressDirectory2Zip(directoryPath, directoryPath, zipArchive);
errorCode = zip_close(zipArchive);
if (errorCode != 0) {
zip_error_t zipError;
zip_error_init_with_code(&zipError, errorCode);
std::cerr << zip_error_strerror(&zipError) << std::endl;
zip_error_fini(&zipError);
}
} else {
zip_error_t zipError;
zip_error_init_with_code(&zipError, errorCode);
std::cerr << "Failed to open output file " << zipFilePath << ": "
<< zip_error_strerror(&zipError) << std::endl;
zip_error_fini(&zipError);
}
}
int main() {
//壓縮文件
//CompressFile("C:/Data/Builder/Demo/view.tmp", "C:/Data/Builder/Demo/view.zip");
//壓縮文件夾
CompressDirectory("C:/Data/Builder/Demo", "C:/Data/Builder/Demo.zip");
return 0;
}
接下來就開始編寫CMake構(gòu)建系統(tǒng)的核心配置文件CMakeLists.txt。都說CMake的語法比較爛,但其實(shí)編寫一個(gè)CMakeLists.txt并算不太難。無論是在Windows下使用Microsoft Visual Studio創(chuàng)建MSVC工程,還是Linux下編寫Makefile文件,無非也是定義了項(xiàng)目的源代碼、庫依賴、編譯選項(xiàng)以及一些特別的構(gòu)建細(xì)節(jié),CMakeLists.txt中的內(nèi)容也是如此。只不過CMakeLists.txt中的一些寫法抹平的不同操作系統(tǒng)之間的差異,使得編譯器和鏈接器能夠相同的邏輯進(jìn)行工作。你可以這樣簡(jiǎn)單的理解,CMakeLists.txt是不同操作系統(tǒng)下不同構(gòu)建平臺(tái)定義的項(xiàng)目文件的再抽象,在進(jìn)行構(gòu)建工作的時(shí)候CMakeLists.txt會(huì)轉(zhuǎn)譯成相應(yīng)平臺(tái)下的程序項(xiàng)目。
這里CMakeLists.txt的內(nèi)容如下所示:
# 輸出cmake版本提示
message(STATUS "The CMAKE_VERSION is ${CMAKE_VERSION}.")
# cmake的最低版本要求
cmake_minimum_required (VERSION 3.9)
# 工程名稱、版本、語言
project (ZipTest VERSION 0.1 LANGUAGES CXX)
# cpp17支持
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找依賴庫
find_package(libzip REQUIRED)
# 將源代碼添加到此項(xiàng)目的可執(zhí)行文件。
add_executable (${PROJECT_NAME} "main.cpp")
# 鏈接依賴庫
target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip)
可以看到內(nèi)容并不多,逐行進(jìn)行解析:
- message指令是用來在CMake構(gòu)建的配置階段輸出的,這個(gè)指令非常有用,可以用來檢查一些配置變量。
- cmake_minimum_required表示cmake的最低版本要求,CMake的很多特性是隨著版本逐漸增加的,需要保證使用的CMake特性滿足最低版本的要求。
- project定義工程名稱、版本和編程語言。
- 一些構(gòu)建配置已經(jīng)被CMake給統(tǒng)一好了,例如是否使用std標(biāo)準(zhǔn)庫,使用std標(biāo)準(zhǔn)庫的版本,這里使用C++17的版本:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
- find_package查找依賴庫指令,這里查找的就是libzip庫。只要libzip庫按照我們前文中的方式正確安裝,通過該指令就可以找到該依賴庫。
- add_executable則是將源代碼文件添加到項(xiàng)目中,這個(gè)指令具體定義了有哪些源代碼文件。
- target_link_libraries指令的意思是鏈接依賴庫,將libzip庫鏈接到該程序中。
2.2 構(gòu)建配置
一些構(gòu)建的配置已經(jīng)被CMake修改成通用性配置,例如上面提到了使用C++17的std標(biāo)準(zhǔn)庫。但是如果有一些針對(duì)不同平臺(tái)的特殊配置怎么辦呢?其實(shí)也很簡(jiǎn)單,就像C/C++寫跨平臺(tái)代碼一樣,識(shí)別不同的平臺(tái)進(jìn)行處理。如下構(gòu)建代碼所示,可以先檢測(cè)編譯器是Clang、GUN、Intel還是MSVC;如果是MSVC平臺(tái)的話,就去掉一些警告,增加一些預(yù)編譯頭。
# 判斷編譯器類型
message("CMAKE_CXX_COMPILER_ID: ${CMAKE_CXX_COMPILER_ID}")
# 判斷編譯器類型
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
message(">> using Clang")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
message(">> using GCC")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel")
message(">> using Intel C++")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
message(">> using Visual Studio C++")
# 禁用特定警告
add_compile_options(/wd4996 /wd4251)
# 設(shè)置預(yù)編譯宏
add_definitions("-DUNICODE" "-D_UNICODE" "-DNOMINMAX")
else()
message(">> unknow compiler.")
endif()
在上述構(gòu)建代碼中,4996、4251警告是MSVC經(jīng)常提示的警告,但是作用并不是很大,因此很多MSVC項(xiàng)目會(huì)將其去掉;UNICODE和_UNICODE預(yù)處理宏是告訴MSVC使用Unicode字符集;NOMINMAX預(yù)處理宏則是取消Win32的最大最小函數(shù),避免函數(shù)命令沖突。這些都是MSVC項(xiàng)目的常用配置,我們只需要識(shí)別到MSVC平臺(tái),并將其應(yīng)用到CMake指令中即可。
其實(shí),構(gòu)建的最關(guān)鍵的步驟就在于編譯和鏈接這兩步,不同的編譯器和鏈接器有不同的命令行參數(shù),使用MSVC的GUI去設(shè)置工程的屬性本質(zhì)上也是取不同的命令行進(jìn)行執(zhí)行。也就是說,上述配置代碼是一種通用的寫法,剩下的我們就只用查找資料找到相應(yīng)編譯器和鏈接器的命令行參數(shù)即可。
2.3 依賴庫配置
在上例中可以看到,我們引入依賴庫libzip似乎很容易,find_package一下,target_link_libraries一下似乎就可以了。這是因?yàn)槲覀兪褂昧薈Make的目標(biāo)鏈接(Target-based linking)機(jī)制,這也是目前現(xiàn)代CMake的最佳實(shí)踐,Boost、Qt、OpenCV 等項(xiàng)目都提供了這種方式的支持。
不過,使用這種方式引入依賴庫也是有一定條件的。具體來說,我們?cè)谑褂肅Make構(gòu)建安裝依賴庫的時(shí)候,會(huì)生成諸如“XXXConfig.cmake”的配置文件到安裝目錄,文件中存在諸如add_library或add_executable等命令,就說明該依賴庫的目標(biāo)導(dǎo)出,支持這種目標(biāo)鏈接機(jī)制。當(dāng)然,這種方式比較新,不是所有的庫項(xiàng)目都提供了這種機(jī)制。
如果沒有提供目標(biāo)鏈接的方式,那么就可以考慮使用傳統(tǒng)的頭文件和庫文件的引入方式,最簡(jiǎn)單無腦的方式就是使用絕對(duì)路徑了:
# 輸出cmake版本提示
message(STATUS "The CMAKE_VERSION is ${CMAKE_VERSION}.")
# cmake的最低版本要求
cmake_minimum_required (VERSION 3.9)
# 工程名稱、版本、語言
project (ZipTest VERSION 0.1 LANGUAGES CXX)
# cpp17支持
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加頭文件的搜索路徑
include_directories("C:/Work/3rdparty/include")
# 將源代碼添加到此項(xiàng)目的可執(zhí)行文件。
add_executable (${PROJECT_NAME} "main.cpp")
# 鏈接依賴庫
target_link_libraries(${PROJECT_NAME} PRIVATE "C:/Work/3rdparty/lib/zip.lib")
其中include_directories是添加庫的頭文件所在的路徑,target_link_libraries則直接鏈接到庫的地址。不過這種使用絕對(duì)路徑的方式實(shí)在太蠢了,不是支持跨平臺(tái),單平臺(tái)的環(huán)境變化都不能支持。稍微方便的一點(diǎn)的方式是將依賴庫的安裝目錄設(shè)置成環(huán)境變量,例如將“C:/Work/3rdparty”設(shè)置成環(huán)境變量GISBasic,那么就可以簡(jiǎn)寫成:
# ...
# 添加頭文件的搜索路徑
include_directories($ENV{GISBasic}/include)
# 將源代碼添加到此項(xiàng)目的可執(zhí)行文件。
add_executable (${PROJECT_NAME} "main.cpp")
# 鏈接依賴庫
target_link_libraries(${PROJECT_NAME} PRIVATE $ENV{GISBasic}/lib/zip.lib)
這樣做至少可以做到配置的一致性,即使開發(fā)團(tuán)隊(duì)成員每個(gè)人的安裝目錄都不一樣,也能保證工程正常構(gòu)建,只要將GISBasic環(huán)境變量設(shè)置正確。但是這樣做其實(shí)也不能保證跨平臺(tái),很顯然Liunx環(huán)境下并不是.lib文件而是.so文件,而且通常有l(wèi)ib前綴。那么就可以根據(jù)不同操作系統(tǒng)使用不同的變量值進(jìn)行構(gòu)建就可以了,改進(jìn)如下所示:
# 添加頭文件的搜索路徑
include_directories($ENV{GISBasic}/include)
# 將源代碼添加到此項(xiàng)目的可執(zhí)行文件。
add_executable (${PROJECT_NAME} "main.cpp")
# 動(dòng)態(tài)庫前綴與后綴
IF(CMAKE_SYSTEM_NAME MATCHES "Linux")
set(LibraryPrefix lib)
set(LibraryPostfix so)
ELSEIF(CMAKE_SYSTEM_NAME MATCHES "Windows")
set(LibraryPrefix )
set(LibraryPostfix lib)
ENDIF()
# 鏈接依賴庫
target_link_libraries(${PROJECT_NAME} PRIVATE $ENV{GISBasic}/lib/${LibraryPrefix}zip.${LibraryPostfix})
可以看到使用鏈接目標(biāo)的方式更加簡(jiǎn)潔一點(diǎn),傳統(tǒng)的使用傳統(tǒng)的頭文件和庫文件的引入方式要達(dá)到跨平臺(tái)的效果需要配置更多的內(nèi)容。其實(shí)CMake的依賴庫配置遠(yuǎn)不止這么一點(diǎn)內(nèi)容,不過比較推薦的和比較底層的兩種方式就以上兩種了。其實(shí)不管是哪一種編程語言的項(xiàng)目,依賴庫的配置永遠(yuǎn)是最麻煩的,以后有機(jī)會(huì)再開一章具體講講CMake關(guān)于依賴庫的配置。
3 構(gòu)建結(jié)果
上述簡(jiǎn)單的項(xiàng)目的代碼結(jié)構(gòu)如下所示:
ZipTest
│ main.cpp
│ CMakeLists.txt
還是使用之前構(gòu)建依賴庫的方式使用腳本進(jìn)行構(gòu)建,將構(gòu)建腳本放置到ZipTest目錄下,運(yùn)行如下腳本:
param(
[string]$SourceLocalPath = ".",
[string]$Generator = "Visual Studio 16 2019"
)
# 清除舊的構(gòu)建目錄
$BuildDir = $SourceLocalPath + "/build"
if (Test-Path $BuildDir) {
Remove-Item -Path $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir
# 轉(zhuǎn)到構(gòu)建目錄
Push-Location $BuildDir
try {
# 配置CMake
cmake .. -G "$Generator" -A x64 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="$InstallDir"
# 構(gòu)建階段,指定構(gòu)建類型
cmake --build . --config RelWithDebInfo
}
finally {
# 返回原始工作目錄
Pop-Location
}
構(gòu)建的exe成果就在Build目錄的子目錄中。其實(shí)現(xiàn)在已經(jīng)可以用IDE可視化構(gòu)建CMake組建的工程了,具體的過程我們放到下一篇再進(jìn)行介紹,這一篇的關(guān)鍵在于我們要如何去寫CMakeLists.txt文件。其實(shí)筆者也認(rèn)為CMake的語法很繁瑣,大寫字母加上下劃線的寫法一點(diǎn)也不美觀,初學(xué)的時(shí)候看到一堆的宏變量頭都大了。不過正如本系列博文一開始就說的,其實(shí)可以不用去關(guān)注這些細(xì)節(jié),也不用去系統(tǒng)的學(xué)習(xí)什么,CMake畢竟只是幫助我們進(jìn)行構(gòu)建工具而已。比如最重要的引用依賴庫的功能,開始的時(shí)候我們只需要知道include_directories包含頭文件,target_link_libraries鏈接庫文件,哪怕寫一堆條件語句,一堆絕對(duì)路徑也沒什么,我們?cè)跇?gòu)建的過程中自然會(huì)思考如何讓我們的構(gòu)建過程更有效率,從而理解CMake的設(shè)計(jì)思路,就知道如何去寫CMakeLists.txt文件了。

浙公網(wǎng)安備 33010602011771號(hào)