CMake構建學習筆記14-依賴庫管理工具

charlee44發表於2024-09-03

如果說做C/C++開發最大的痛點是什麼,那麼一定是缺少一個官方的統一的包管理器。認真的說,如果你要用C/C++乾點什麼,至少需要(Windows系統下):

  1. C/C++語言本身、標準庫、以及作業系統API幾乎幹不了什麼,除非你真的想從零開始造輪子。
  2. 開始找一些現成的實現組成依賴庫。最好看能不能找到預編譯包或者安裝包,即使找到了,由於二進位制相容的問題,你也不一定能夠使用。
  3. 如果沒找到預編譯包或者安裝包,那麼就需要自己從原始碼進行構建了。如果提供了CMake的構建方式就挺好,萬一沒有提供,就得自己想辦法組織工程進行構建。
  4. 注意,依賴庫本身是需要依賴庫的!比如,你構建GDAL的時候會發現PROJ是GDAL的必須依賴,等你開始構建PROJ的時候又發現sqlite3是PROJ必須依賴,而你準備構建sqlite3的時候發現sqlite3是不提供CMake方式的。
  5. 不談構建過程中處理的一系列問題。等你把依賴庫構建完成了,你就得考慮如何引入了。如果你使用動態連結庫,你需要進行標頭檔案、動態庫匯入庫以及動態庫相關的配置。如果標頭檔案錯了,你會發現無法編譯;如果動態庫匯入庫錯誤,你會發現無法連結;如果動態庫不正確,你會發現無法執行。
  6. 最後開始在原始碼中include標頭檔案,呼叫依賴庫相關的功能進行操作。

嗯,說實話筆者光是寫這些步驟都覺得有點汗流浹背了。這要是換成使用Python或者JavaScript進行開發,一個install指令,一個import語句就全部搞定,也難怪C/C++開發的效率一直被程式設計師詬病呢。不過,C/C++領域也不是一直在固步自封,Windows系統下也可以使用一些包管理器,例如vcpkg、Conan、Chocolatey等。個人認為,這些包管理器正在逐漸成熟過程中,不過尚需要一些時間完善,有興趣的同學可以進行試用。

另外一種方式就是像筆者一樣,嘗試組織一個屬於自己或者自己團隊的依賴庫管理工具。這樣做的原因有三:

  1. 不同環境下的C/C++包存在二進位制相容的問題。
  2. 構建Release帶除錯資訊的構建成果,以及符號庫檔案。
  3. 有些庫包很少見,通用的包管理器不一定收納。

那麼具體如何實現呢?其實不用想的太複雜,我們將所有需要的構建成果都構建到同一個目錄,並且將這個目錄設定成環境變數。這樣,在我們日常的程式中就可以依託這個環境變數配置依賴庫。這意味著所有團隊成員的程式碼工程的配置都可以是一樣的,我們就可以忽略掉不同軟硬體環境的不同,實現了程式碼專案的一致性。

所以,關鍵就是將這些常見的元件構建組織起來。不能使用CMake的GUI工具,因為不同的庫各自有自己獨特的構建選項,最好將其透過指令碼記錄。不妨將構建的指令碼寫的完善一點,自動化一點,程式碼檔案從哪裡來,最後的構建成果輸出到哪裡。例如,前面博文中構建libzip庫的完整Powershell指令碼libzip.ps1為:

param(   
    [string]$SourceAddress = "https://github.com/nih-at/libzip/archive/refs/tags/v1.10.1.zip",
    [string]$SourceZipPath = "../Source/libzip-1.10.1.zip",
    [string]$SourceLocalPath = "./libzip-1.10.1",
    [string]$Generator,
    [string]$MSBuild,
    [string]$InstallDir,
    [string]$SymbolDir 
)

# 檢查目標檔案是否存在,以判斷是否安裝
$DstFilePath = "$InstallDir/bin/zip.dll"
if (Test-Path $DstFilePath) {
    Write-Output "The current library has been installed."
    exit 1
} 

# 建立所有依賴庫的容器
. "./BuildRequired.ps1"
$Librarys = @("zlib") 
BuildRequired -Librarys $Librarys

. "./DownloadAndUnzip.ps1"
DownloadAndUnzip -SourceLocalPath $SourceLocalPath -SourceZipPath $SourceZipPath -SourceAddress $SourceAddress

# 清除舊的構建目錄
$BuildDir = $SourceLocalPath + "/build"  
if (Test-Path $BuildDir) {
    Remove-Item -Path $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir

# 轉到構建目錄
Push-Location $BuildDir

try {
    # 配置CMake  
    cmake .. -G "$Generator" -A x64 `
        -DCMAKE_BUILD_TYPE=RelWithDebInfo `
        -DCMAKE_PREFIX_PATH="$InstallDir" `
        -DCMAKE_INSTALL_PREFIX="$InstallDir" `
        -DBUILD_DOC=OFF `
        -DBUILD_EXAMPLES=OFF `
        -DBUILD_REGRESS=OFF `
        -DENABLE_OPENSSL=OFF

    # 構建階段,指定構建型別
    cmake --build . --config RelWithDebInfo

    # 安裝階段,指定構建型別和安裝目標
    cmake --build . --config RelWithDebInfo --target install

    # 複製符號庫
    $PdbFiles = @(
        "./lib/RelWithDebInfo/zip.pdb"        
    ) 
    foreach ($file in $PdbFiles) {  
        Write-Output $file
        Copy-Item -Path $file -Destination $SymbolDir
    }    
}
finally {
    # 返回原始工作目錄
    Pop-Location
}

不得不說Powershell指令碼真的很強大,你甚至可以引入第三方指令碼中的函式,例如這裡的BuildRequired表示預先安裝當前庫的依賴庫(其實就是呼叫其他庫的構建指令碼),而DownloadAndUnzip表示從遠端把原始碼下載下來並且解壓到指定資料夾。接下來建立構建資料夾、配置CMake專案、構建專案以及安裝專案。最後,我們把這個庫符號庫給移動到安裝目錄中去。

就像這樣一個一個把需要的依賴庫構建指令碼寫好。有時候需要修改一下構建選項也沒什麼關係,修改下對應的內容重新構建就好了,這就是寫指令碼的好處。不過,這樣一個一個呼叫指令碼也說不上對庫包進行管理了,對比一下一些比較完善的包管理器例如npm,可以再寫一個總的用於管理的指令碼,將以上構建指令碼管理起來,如下Powershell指令碼BuildCppDependency.ps1所示:

param(   
    [string]$Generator,
    [string]$MSBuild,
    [string]$InstallDir,
    [string]$SymbolDir,   
    [string]$Install,   
    [string]$List
)

# 建立所有的庫的容器
$LibrarySet = [System.Collections.Generic.SortedSet[string]]::new()
$LibrarySet.Add("zlib") > $null
$LibrarySet.Add("libpng") > $null
$LibrarySet.Add("libjpeg") > $null
$LibrarySet.Add("libtiff") > $null
$LibrarySet.Add("giflib") > $null
$LibrarySet.Add("freetype") > $null
$LibrarySet.Add("OpenSceneGraph") > $null
$LibrarySet.Add("eigen") > $null
$LibrarySet.Add("osgQt5") > $null
$LibrarySet.Add("osgQt") > $null
$LibrarySet.Add("minizip") > $null
$LibrarySet.Add("libzip") > $null
$LibrarySet.Add("opencv") > $null
#$LibrarySet.Add("protobuf") > $null
#$LibrarySet.Add("abseil-cpp") > $null

# 檢查是否傳遞了$Install引數
if ($PSBoundParameters.ContainsKey('Install')) {   
    # 比較時忽略大小寫
    if ($Install.ToLower() -eq "-all".ToLower()) {
        Write-Output "All libraries will be installed soon..."
        foreach ($item in $LibrarySet) {
            Write-Output "Find the library named $item and start installing..."        
            # 動態構建指令碼檔名並執行
            $BuildScript = "./$item.ps1";           
            & $BuildScript -Generator $Generator -MSBuild $MSBuild -InstallDir $InstallDir -SymbolDir $SymbolDir
        }
    }
    else {
        # 查詢某個字串
        if ($LibrarySet.Contains("$Install")) {
            Write-Output "Find the library named $Install and start installing..."        
            # 動態構建指令碼檔名並執行
            $BuildScript = "./$Install.ps1";           
            & $BuildScript -Generator $Generator -MSBuild $MSBuild -InstallDir $InstallDir -SymbolDir $SymbolDir
        }
        else {
            Write-Output "Cannot find library named $Install !"
        }
    }
}
elseif ($PSBoundParameters.ContainsKey('List')) {       
    if ($List.ToLower() -eq "-all".ToLower()) {
        Write-Output "The list of all libraries that can currently be installed in the repository is as follows:"
        foreach ($item in $LibrarySet) {
            Write-Output $item
        }
    }
}
else {
    Write-Host "Please enter the parameters!"
}

再次感嘆下Powershell指令碼的強大,你甚至可以看到System.Collections.Generic.SortedSet,沒錯,這就是.Net提供的容器,透過Powershell可以也呼叫它。上述指令碼提供了基本的檢視和安裝功能,例如檢視能安裝的庫,可使用如下指令:

./BuildCppDependency.ps1 -List -all

安裝特定的庫:

./BuildCppDependency.ps1 -Generator "Visual Studio 16 2019" `
-MSBuild "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" `
-InstallDir "$env:GISBasic" `
-SymbolDir "$env:GISBasic/symbols" `
-Install libzip

安裝所有的庫:

./BuildCppDependency.ps1 -Generator "Visual Studio 16 2019" `
-MSBuild "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" `
-InstallDir "$env:GISBasic" `
-SymbolDir "$env:GISBasic/symbols" `
-Install -all

其實一個完善的包管理工具需要的功能還是很多的,也非常複雜。例如包的安裝是很容易,如何進行解除安裝呢?如何升級如何降級呢?是不是可以與IDE進行結合,自動匯入依賴庫並且進行配置呢?這些問題,就留待以後考慮吧。

最後,就將寫的依賴庫管理工具構建指令碼貢獻給大家參考吧:地址

相關文章