以構件為基礎的建構系統

回報問題 查看來源 夜間 7.3 7.2 7.1 7.0 6.5

本頁面說明以構件為基礎的建構系統,以及其背後的理念 建立。Bazel 是一個以構件為基礎的建構系統。雖然以工作為基礎的建構系統比建構指令碼更進步,但讓個別工程師定義自己的工作,會讓他們擁有過多權力。

以構件為基礎的建構系統是由系統定義的少量工作 工程師可以透過有限的方式進行設定工程師仍會告知系統要建構的內容,但建構系統會決定建構方式。如同 以任務為基礎的建構系統,以及 Bazel 等以構件為基礎的建構系統 這些建構檔案的內容,但它們的內容非常不同。與 Turing 完整指令碼語言中的指令集不同,Bazel 中的 buildfile 是宣告式資訊清單,可說明要建構的構件集、其依附元件,以及影響建構方式的有限選項集。工程師在指令列上執行 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 時,都會檢查指定的編譯器是否可在已知位置使用。和其他依附元件一樣,如果 Java 編譯器有所變更,系統會重新建構所有依附於該編譯器的構件。

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

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

擴充建構系統

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

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

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

隔離環境

動作似乎可能會遇到其他系統中的工作遇到的問題,也就是說,寫入相同檔案的動作會不會彼此衝突?實際上,Bazel 會將這些要求 使用沙箱機制無法發生衝突。支援 因此,每個動作都透過檔案系統隔離。 沙箱。實際上,每個動作都只能看到 其中包含已宣告的輸入內容,以及已宣告的任何輸出內容 產生的結果。這由 LXC 等系統強制在 Linux 執行 執行 Docker 映像檔這表示動作無法與其他動作發生衝突,因為它們無法讀取未宣告的任何檔案,且未宣告的任何寫入檔案會在動作完成時遭到捨棄。Bazel 也會使用沙箱限制動作,禁止透過 網路。

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

還有一個問題待解決:建構系統通常需要下載 來自外部來源的依附元件 (無論是工具或程式庫) 並直接建構這類元件您可以在範例中透過 @com_google_common_guava_guava//jar 依附元件看到這一點,該依附元件會從 Maven 下載 JAR 檔案。

依賴目前工作區外部的檔案存在風險。這些檔案 可能會需要建構系統持續檢查 都能隨時保持不變如果遠端檔案變更,但工作區原始碼中沒有相對應的變更,也會導致無法重現的建構作業,也就是說,建構作業可能在某天運作,但在下一個工作天因未察覺的依附元件變更而無故失敗。最後,如果外部依附元件屬於第三方,可能會帶來巨大的安全風險:如果攻擊者能夠滲透該第三方伺服器,就能將依附元件檔案取代為他們自行設計的內容,進而完全掌控您的建構環境及其輸出內容。

基本問題是,我們希望建構系統能夠瞭解這些檔案,而不需要將這些檔案檢查至原始碼控管。更新依附元件 都值得考慮,但應該集中在一處 而非由個別工程師管理 有些人會將 Cloud Storage 視為檔案系統 但實際上不是這是因為即使使用「Live at Head」模型,我們仍希望建構作業具有決定性,也就是說,如果您檢查上週的版本,應會看到當時的依附元件,而非目前的依附元件。

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

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

當然,如果遠端伺服器無法使用或開始提供損毀的資料,這仍可能會造成問題,如果您沒有該依附元件的其他副本,這可能會導致所有版本開始失敗。為避免發生這種問題,建議您針對任何非簡單的專案,將所有依附元件鏡像到您信任且可控的伺服器或服務。否則,即使已簽入的雜湊可確保建構系統的安全性,您仍必須仰賴第三方提供建構系統。