以構件為基礎的建構系統

回報問題 查看原始碼 夜間 7.2 7.1 7.0 6.5 6.4

本頁面說明以構件為基礎的建構系統,以及其背後的理念 建立。Bazel 是一個以構件為基礎的建構系統。以任務為基礎的建構時 系統確實勝過建構指令碼 但其實能大幅降低 可讓個別工程師定義自己的工作。

以構件為基礎的建構系統是由系統定義的少量工作 工程師可以透過有限的方式進行設定工程師還是會告訴系統 要建構的內容,但建構系統會決定建構的「方式」。如同 以任務為基礎的建構系統,以及 Bazel 等以構件為基礎的建構系統 這些建構檔案的內容,但它們的內容非常不同。與其 比起使用圖語完整指令碼語言,成為一系列迫切指令的指令 說明如何產生輸出內容 Bazel 中的建構檔案是宣告式的 資訊清單,說明要建構的一組構件、其依附元件及 且能影響建構方式的有限選項工程師執行 bazel 的時間 並在指令列上指定一組要建構的目標 (也就是「內容」),以及 Bazel 負責設定、執行及排定編譯作業 (方法)。由於建構系統現在可以完全掌控 可讓您事半功倍 更有效率,同時確保正確性

功能性的觀點

容易在以構件為基礎的建構系統和功能之間進行類比 程式設計傳統的命令式程式設計語言 (例如 Java、C 和 Python) 指定在 以任務為基礎的建構系統可讓程式設計師定義一系列的步驟 適合執行的版本功能程式設計語言 (如 Haskell 和 ML) 對比,則這些概念的結構更像是一系列數學方程式於 程式語言,程式設計師會描述要執行的運算作業 會將執行該運算的時間與確切方式 編碼器-解碼器架構

這相當於在以構件為基礎的建構系統中宣告資訊清單的概念 讓系統知道如何執行建構作業導致許多問題 很容易透過功能式程式設計加以表達 語言通常能盡量平行 並充分保證其正確性 根本做不到的事如要以最簡單的方式 功能程式設計,就是只需將某部分元素 運用一系列規則或函式將資料轉化為另一個的答案恰到好處 什麼是建構系統:整個系統實際上是數學功能 將來源檔案 (以及編譯器等工具) 做為輸入,然後 做為輸出內容建構建構時,我們並不意外 也就是功能性程式設計的原則

瞭解以構件為基礎的建構系統

Google 的建構系統 Blaze 是第一個以構件為基礎的建構系統。Bazel 是 Blaze 的開放原始碼版本

以下是 Bazel 中的建構檔案 (通常命名為 BUILD) 的外觀:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

在 Bazel 中,BUILD 檔案會定義目標—這裡提供的兩種目標類型 java_binaryjava_library。每個目標都會對應至一項構件 二進位檔目標會產生 和程式庫目標產生的程式庫 二進位檔或其他程式庫每個目標都有:

  • name:在指令列和其他上參照目標的方式 目標
  • srcs:要編譯的來源檔案,以便為目標建立構件
  • deps:其他目標必須先建立,然後連結至這個目標 該資料來源

依附元件可以位於同一套件中 (例如 MyBinary 的 依附於 :mylib),或是位於同一來源階層中的其他套件 (例如 mylib//java/com/example/common 的依附元件)。

與以任務為基礎的建構系統一樣,您必須使用 Bazel 的指令列執行建構作業 如果偏好在終端機視窗中工作 可使用 Google Cloud CLI gcloud 指令列工具如要建構 MyBinary 目標,請執行 bazel build :MyBinary。更新後 在乾淨的存放區中初次輸入該指令 Bazel:

  1. 剖析工作區中的每個 BUILD 檔案,以建立依附元件圖表 導致效能降低
  2. 使用圖表判斷 MyBinary 的遞移依附元件;並 就是 MyBinary 相依的每個目標,以及這些目標 目標將以遞迴方式依賴
  3. 依序建構這些依附元件。Bazel 會先建構 沒有其他依附元件的目標,並追蹤哪些依附元件 但仍須為每個目標建構應用程式達到所有目標後 Bazel 就會開始建構這個目標。這項程序 會持續到 MyBinary 的所有遞移依附元件為止 。
  4. 建構 MyBinary 以產生最終的可執行二進位檔,並在所有 當中包含步驟 3 中建構的依附元件。

基本上,在現實生活中可能沒那麼重要 而不是使用以任務為基礎的建構系統時的情況事實上, 最終結果相同的二進位檔 透過許多步驟找出依附元件,然後執行 依序新增各個步驟但兩者之間仍有重大差異。第一項會顯示 步驟 3:因為 Bazel 知道每個目標只會產生 Java 程式庫 且知道只需執行 Java 編譯器,而不是執行 使用者定義的指令碼,因此可以安全同時執行這些步驟。 這可能會導致建築物效能上大幅提升。 而且只會在多核心機器上逐一指定 根據構件的做法,建構系統將負責自行執行作業 ,以提高平行處理能力的保證。

然而,平行處理的優點卻能超越平行處理原則。接下來 這種方法可讓我們明顯看出,開發人員在未進行任何變更的情況下第二次輸入 bazel build :MyBinary 時:Bazel 會在更少的時間內結束 然後顯示訊息指出目標是最新狀態。這是 可能源自於我們先前介紹的功能 - Bazel 知道每個目標只是執行 Java 動作的結果 編譯器,且知道 Java 編譯器的輸出內容僅取決於 只要輸入內容維持不變,即可重複使用輸出內容。 這項分析適用於各個層級如果 MyBinary.java 發生變更,Bazel 知道 重建 MyBinary,但重複使用 mylib。如果來源檔案 //java/com/example/common 改變,Bazel 知道要重新建構該程式庫。 mylibMyBinary,但重複使用 //java/com/example/myproduct/otherlib。 由於 Bazel 知道每個步驟執行的工具屬性, 它每次只能重新建構最低限度的構件 確保不會產生過時的建構作業。

根據構件 (而非任務) 重構建構程序 但功能強大藉由降低對程式設計師所能擁有的靈活性, 系統就能深入瞭解 在建構每個步驟執行的作業這項服務可以 可以利用這項知識,透過平行處理建構作業讓建構作業的效率提高 並重複使用其輸出內容但這只是第一步 並重複運用這些平行處理的構成元素 以及具備高擴充性的建構系統

其他巧妙的 Bazel 技巧

構件式建構系統能透過平行處理機制,徹底解決問題 並在任務式建構系統中重複使用相同的內容不過, 還有我們尚未解決的問題Bazel 十分聰明 並討論這些做法,再繼續進行後續步驟。

做為依附元件的工具

我們先前遇到的問題之一是版本依賴於安裝的工具 而且要跨系統重現建構作業有時並不容易 不同的工具版本或位置更難解決問題 專案使用的語言需要不同工具時, 與其他平台 (例如 Windows 和 Linux) 上的平台 這些平台需要一套略為不同的工具組合 並非如此

Bazel 會將工具視為依附元件,以解決本問題的第一部分, 每個目標。工作區中的每個 java_library 都隱含依附於 Java 編譯器預設採用知名編譯器。每當 Bazel 建構 java_library 會檢查,確認指定的編譯器可供使用 發出。和其他依附元件一樣 所有相依的構件都會重新建構

Bazel 希望將平台獨立性 建構設定。與其 目標對象直接取決於工具的設定類型:

  • 主機設定:建構在建構期間執行的工具
  • 目標設定:建構您最終要求的二進位檔

擴充建構系統

Bazel 提供數種熱門程式設計語言的目標 但工程師總是希望可以做到更多,這屬於工作導向 能靈活支援任何類型的建構程序 這樣就比較適合在以構件為基礎的建構系統中。 幸好,Bazel 允許將支援的目標類型擴充至 新增自訂規則

如要在 Bazel 中定義規則,規則作者會宣告規則的輸入內容 需要 (在 BUILD 檔案中傳遞的屬性格式) 和 規則會產生的輸出內容作者也定義了 所產生的收益每個動作都會宣告其輸入和輸出內容 或將特定字串寫入檔案,且 透過輸入和輸出內容與其他動作相互通訊也就是說 是建構系統中最低層級的可組合函式,而動作則可 只使用宣告的輸入和輸出內容,而且它能使用這些素材 Bazel 會視情況安排動作排程並快取其結果。

由於無法阻止行動開發人員 例如導入非確定性程序 他們的動作。然而,這種情況並不容易在實務上發生, 從我們來看,濫用的可能性大幅降低 發生錯誤的機率支援許多常用語言和工具的規則 而且大多數專案都不需要自行定義 不過,編寫這類演算法並不容易 因為我們無法寫出所有可能的規則即使這麼做,您只須在一項規則中定義規則定義 集中存放於存放區,這代表大部分的工程師都能使用 不必擔心這些規則的導入過程

隔離環境

動作就像在其他應用程式中工作一樣 系統無法將同時寫入相同 檔案如何導致彼此衝突?實際上,Bazel 會將這些要求 使用沙箱機制無法發生衝突。支援 因此,每個動作都透過檔案系統隔離。 沙箱。實際上,每個動作都只能看到 其中包含已宣告的輸入內容,以及已宣告的任何輸出內容 產生的結果。這由 LXC 等系統強制在 Linux 執行 執行 Docker 映像檔也就是說,當動作與預定政策衝突時, 因為他們無法讀取非聲明的檔案 系統會在使用者寫入但未宣告的檔案時 完成。Bazel 也會使用沙箱限制動作,禁止透過 網路。

使外部依附元件具有確定性

還有一個問題待解決:建構系統通常需要下載 來自外部來源的依附元件 (無論是工具或程式庫) 並直接建構這類元件透過 @com_google_common_guava_guava//jar 依附元件,用於下載 JAR 檔案 從 Maven 取得的映像檔

視目前工作區外的檔案而定,可能會有風險。這些檔案 可能會需要建構系統持續檢查 都能隨時保持不變如果遠端檔案在沒有相應的變更的情況下發生變更 導致無法重現的版本。 如未註意到發生不明原因,則可能會在某日的時間內失敗 依附元件變更。最後,外部依附元件可能會帶來 風險歸功於第三方擁有的風險:如果攻擊者能夠滲透 可以將依附元件檔案替換成 可能讓開發人員完全掌控您的建構作業 資源與輸出內容

根本問題在於,我們希望建構系統能瞭解 而不必將這些檔案簽入原始碼控管系統更新依附元件 都值得考慮,但應該集中在一處 而非由個別工程師管理 有些人會將 Cloud Storage 視為檔案系統 但實際上不是這是因為即使採用「在頭頂」模型,我們仍然需要各種版本 具有確定性,意味著如果您從上次 週後,您的依附關係應該就會如常 您的兒時朋友不久後就會遭到解僱 並要求您暫時保密

Bazel 和某些其他建構系統會要求提供 完整工作區資訊清單檔案,會列出每個外部的加密編譯雜湊 容器依附元件雜湊是一種簡潔易見的方式 而不會將整個檔案簽入原始碼控制中。每當新的 從工作區參照外部依附元件,依附元件的雜湊為 手動或自動加入資訊清單。當 Bazel 執行 就會根據預期,檢查快取依附元件的實際雜湊碼 而只有在雜湊不同的情況下,才會重新下載資訊清單。

如果我們下載的構件含有與 如果資訊清單中的雜湊值,則建構將會失敗。這個 可自動執行,但該變更必須經過核准並簽收 在版本開始之前,對原始碼控管內容會接受新的依附元件。也就是說 會記錄依附元件的更新時間 除非工作區來源出現相對應的變更,否則依附元件無法變更。 也就是說,當您查看舊版本的原始碼時 保證會保證使用當時使用的依附元件 使用者使用該版本檢查時 (否則如果這些依附關係,就會失敗 )。

當然,當遠端伺服器無法使用時, 開始提供毀損的資料,這可能會導致所有建構作業開始失敗 如果找不到該依附元件的其他副本為了避免這種情況 我們建議一舉完整地說明整個專案 部署於可信任及控管的伺服器或服務上否則, 因此您的建構系統一律須由第三方主導 可用性。