1. 程式人生 > >C/C++ 基於 Jenkins、Conan 和 Artifactory 的持續交付

C/C++ 基於 Jenkins、Conan 和 Artifactory 的持續交付

C/C++在很多重要的行業都有應用,比如作業系統、嵌入式系統、金融系統、科研系統、汽車製造、機器人及遊戲等等。在這些行業裡,效能是非常關鍵的考量因素,而其他的語言又無法滿足要求。作為一個如此重要的語言,C/C++ 的生態面臨著一些嚴峻的挑戰:

  • 大型工程 - 當代碼行數達到百萬級別時,如果沒有現代化的工具,將很難管理大型工程。

  • 應用二進位制介面不相容 - 為了確保一個庫與其他庫、整個應用的相容性,必須在通過各種配置來描述具體依賴資訊,比如作業系統、架構和編譯器等。

  • 編譯構建慢 - 由於標頭檔案包含和預處理,以及上面提到的這些挑戰,需要額外的機制來提升編譯效率,保證只編譯那些需要重新編譯的程式碼。

  • 程式碼連結和內嵌 - 一個靜態的C/C++庫能夠被另一個庫通過標頭檔案包含的方式引用。一個共享庫也能嵌入一個靜態庫。在兩種情形中,當任何依賴變更時,都必須管理哪些庫是需要重新構建的。

  • 生態系統的快速發展 - 針對不同平臺、不同構建任務及應用場景的編譯器、構建系統層出不窮。

這篇文章會介紹如何通過Jenkins CI、Conan C/C++包管理器以及JFrog Artifactory通用製品倉庫實現C/C++持續交付的最佳實踐。

Conan - 強大的 C/C++ 包管理器

Conan 就是為了解決這些問題而誕生的。

Conan使用一個基於Python的清單(譯者注:通常譯為配方,表達更為準確),這個清單描述瞭如何通過顯式呼叫任意構建系統來構建一個庫,並描述庫使用者所需的資訊(包括目錄,庫名等)。為了管理不同的配置和ABI相容性,Conan使用”Setting”(作業系統,架構,編譯器…),當”Setting”發生變化的時候,Conan會為同一個庫生成不同的二進位制版本:

Conan

構建的二進位制檔案可以上傳到JFrog Artifactory或Bintray,與您的團隊或整個社群共享。團隊中的開發人員不需要再次重建庫,Conan將僅從配置的遠端倉庫(分散式模型)中獲取與使用者配置匹配的所需二進位制包。這個過程中仍有一些挑戰需要解決:

  • 如何管理C/C++專案的開發和釋出過程?
  • 如何分發您的C/C++庫?
  • 如何測試您的C/C++專案?
  • 如何為不同的配置生成多個包?
  • 當其中一個庫發生變化時, 如何管理庫的重建?

Conan 生態系統

Conan生態系統正在快速增長,C/C++語言的DevOps現已成為現實:

  • JFrog Artifactory管理完整的開發和釋出週期。
  • JFrog Bintray是通用分發中心。
  • Jenkins自動化專案測試,生成Conan包的不同二進位制配置,並自動化重建庫。

這裡寫圖片描述

Jenkins Artifactory 外掛

  • 提供Conan DSL,這是一種非常通用但功能強大的方法,可以從Jenkins Pipeline指令碼中呼叫Conan。
  • 使用Artifactory例項管理遠端配置,隱藏身份驗證詳細資訊。
  • 收集任何Conan操作(安裝/上傳包)所涉及的工件資訊,以生成buildInfo並將其上傳到Artifactory。

這裡寫圖片描述

這是帶有Artifactory外掛的Conan DSL的示例。首先, 我們配置Artifactory倉庫,然後檢索依賴項並最終構建它:

def artifactory_name = "artifactory"
def artifactory_repo = "conan-local"
def repo_url = 'https://github.com/memsharded/example-boost-poco.git'
def repo_branch = 'master'
node {
   def server
   def client
   def serverName
    stage("Get project"){
        git branch: repo_branch, url: repo_url
    }
    stage("Configure Artifactory/Conan"){
        server = Artifactory.server artifactory_name
        client = Artifactory.newConanClient()
        serverName = client.remote.add server: server, repo: artifactory_repo
    }
    stage("Get dependencies and publish build info"){
        sh "mkdir -p build"
        dir ('build') {
            def b = client.run(command: "install ..")
            server.publishBuildInfo b
        }
    }
    stage("Build/Test project"){
        dir ('build') {
            sh "cmake ../ && cmake --build ."
        }
    }
}

您可以在上面的示例中看到Conan DSL非常高效。它對常見操作有很大幫助,但也允許強大的自定義整合。這對於C/C++專案非常重要,因為每個公司都有非常具體的專案結構,定製化的整合方式等。

複雜的Jenkins Pipeline操作:託管和並行化庫的構建

正如我們在本博文開頭所看到的那樣,在構建C/C++專案時節省時間至關重要。以下是優化流程的幾種方法:

  • 只重新構建需要重建的庫。這些是受其依賴的庫的變更所影響到的庫。
  • 如果可能的話,並行構建。如果專案圖中的兩個或多個庫之間沒有關係,則可以並行構建它們。
  • 並行構建不同的配置(os,編譯器等)。如果需要,使用不同的從節點。

讓我們看一個使用Jenkins Pipeline特性的例子:

這裡寫圖片描述

上圖表示我們的專案P及其依賴項(A-G)。我們希望為兩個不同的體系結構x86和x86_64分發專案。

如果我們修改庫A會發生什麼

如果我們將A的版本變為v1,不會有什麼問題,我們可以更新B的依賴資訊,並且也將B的版本變為v1,以此類推。整個完整的流程如下:

  • 推送A(v1)版本到Git, Jenkins 會構建x86和x86_64庫。Jenkins 會上傳所有包到Artifactory。
  • 手動將B的版本升級到v1, 現在依賴A1, 推送到Git, Jenkins 會使用Artifactory返回的A(v1)來構建x86和x86_64架構下的B(v1)。
  • 為C, D, F, G重複相同的流程,最終完成整個專案的構建。

但是如果我們在開發環境對應的倉庫中開發我們的庫, 在每一次git push的時候,我們或許要依賴最新版本的A或者覆蓋A(v0)包, 並且我們想自動化地重新構建受影響的包, 在這裡指的就是 B, D, F, G 和 P。

如何通過 Jenkins Pipelines 實現

首先我們需要知道哪些庫需要重新被構建。命令”conan info –build_order” 能夠識別專案修改了哪些庫,並且告訴我們哪些可以並行構建。

因此, 我們建立兩個Jenkins Pipeline任務:

  • “簡單構建” 任務構建每一個需要單獨構建的庫。類似上面第一個使用 Conan DSL 和 Jenkins Artifactory 外掛的例子。它是一個帶引數的構建任務, 引數就是需要構建的包。

  • “多工構建” 任務編排和啟動”簡單構建”任務, 如果可以並行構建則通過並行的方式執行。

我們還有一個存放配置檔案(configuration yml)的倉庫,Jenkins 任務會通過這個yml檔案來知曉每個庫的”配方”的所在,以及即將使用的不同profile,這裡的profile指的是x86x86_64

leaves:
  PROJECT:
    profiles:
       - ./profiles/osx_64
       - ./profiles/osx_32
artifactory:
  name: artifactory
  repo: conan-local
repos:
 LIB_A/1.0:
   url: https://github.com/lasote/skynet_example.git
   branch: master
   dir: ./recipes/A
LIB_B/1.0:
 url: https://github.com/lasote/skynet_example.git
 branch: master
 dir: ./recipes/b
…
PROJECT:
 url: https://github.com/lasote/skynet_example.git
 branch: master
 dir: ./recipes/PROJECT

如果我們改變並推送庫A到製品庫, “多工構建” 任務會被觸發。首先會通過”conan info”命令檢查哪些庫需要重建。Conan 會返回如下列表: [B, [D, F], G]

這意味著我們需要先重新構建B, 然後我們才可以並行重新構建 D 和 F, 最後我們重新構建G。我們注意到C並不需要重新構建,因為A的變化並沒有影響到它。

“多工構建” Jenkins pipeline 指令碼會建立一個包含併發呼叫”簡單構建”任務的閉包, 最終以併發的形式啟動這個組內的任務。

//for each group
tasks = [:]
    // for each dep in group
    tasks[label] = { -> build(job: "SimpleBuild",
                            parameters: [
                               string(name: "build_label", value: label),
                               string(name: "channel", value: a_build["channel"]),
                               string(name: "name_version", value: a_build["name_version"]),
                               string(name: "conf_repo_url", value: conf_repo_url),
                               string(name: "conf_repo_branch", value: conf_repo_branch),
                               string(name: "profile", value: a_build["profile"])
                            ]
                        )
                    }
    parallel(tasks)

最終將呈現如下效果:

  • 兩個”簡單構建”任務將被觸發, 都是為了構建庫B, 一個是為x86架構,另一個是x86_64架構。

這裡寫圖片描述

  • 當 “A” 和 “B” 構成成功, “F” 和 “D” 的構建將被觸發, 4 個”簡單構建”任務將會以併發的方式執行(分別對應x86, x86_64架構)。

這裡寫圖片描述

  • 最終 “G” 會被構建出來。 因此 2 個”簡單構建”會以併發形式執行。

Jenkins 任務執行各階段檢視如下所示:

MultiBuild

這裡寫圖片描述

SimpleBuild

這裡寫圖片描述

我們可以將”基本構建”任務配置為在不同的節點(Windows, OSX, Linux…)上執行,並且可以在Jenkins配置中控制任務執行執行緒數。

結論

很多企業C/C++的DevOps仍然處於摸索階段。它需要投入大量的時間,但是從長遠來看,在開發和釋出生命週期中能夠節省大量的時間。
從更高的維度看,它可以提升C/C++專案的交付質量和可靠性。在不久的將來,落地C/C++專案的DevOps將是剛需。

這篇部落格中描述的Jenkins案例很好地闡述瞭如何僅通過Groovy程式碼和簡便的yml檔案來控制庫的併發構建。其強大之處不在於這個案例和程式碼本身,而在於為個性化定製構建流程提供了可能性。感謝Jenkins Pipeline, Conan 和 JFrog Artifactory提供的強大功能。