以工作為基礎的建構系統

本頁說明以工作為基礎的建構系統、運作方式,以及工作型系統可能會發生的一些小工具。在殼層指令碼之後,工作型建構系統是新一代的建構邏輯。

瞭解以工作為基礎的建構系統

在以工作為基礎的建構系統中,工作的基本單位是任務。每項工作都是可以執行任何邏輯的指令碼,而工作會將其他工作指定為依附元件,工作必須在此工作之前執行。現今使用的大多數主要建構系統 (例如 Ant、Maven、Gradle、Grunt 和 Rake) 都是以工作為基礎。大部分的現代化建構系統並沒有殼層指令碼,而是要求工程師建立建構檔案來說明如何執行建構。

Ant 手冊為例:

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

建構檔是以 XML 編寫,並且定義了版本的一些簡單中繼資料以及工作清單 (XML 中的 <target> 標記)。(Ant 使用「target」這個詞來代表任務,並使用「task」一詞來參照「指令」)。每項工作都會執行 Ant 定義的可能指令清單,其中有建立及刪除目錄、執行 javac,以及建立 JAR 檔案。使用者提供的外掛程式可擴充這組指令,以涵蓋任何類型的邏輯。每項工作也可以透過相依屬性定義其依賴的工作。這些依附元件會形成非循環圖,如圖 1 所示。

顯示依附元件的加密圖

圖 1. 顯示依附元件的循環圖

使用者可以將工作提供給 Ant 的指令列工具,以執行建構作業。例如,當使用者輸入 ant dist 時,Ant 會執行下列步驟:

  1. 在目前的目錄中載入名為 build.xml 的檔案並進行剖析,藉此建立圖 1 所示的圖表結構。
  2. 尋找在指令列中提供的名為 dist 的工作,並發現該工作依附於名為 compile 的工作。
  3. 找到名為 compile 的工作,並發現其對名為 init 的工作有依附元件。
  4. 找到名為 init 的工作,並發現其沒有依附元件。
  5. 執行 init 工作中定義的指令。
  6. 如果已執行所有任務的依附元件,系統就會執行 compile 工作中定義的指令。
  7. 如果已執行所有任務的依附元件,系統就會執行 dist 工作中定義的指令。

最後,Ant 執行 dist 工作時執行的程式碼相當於下列殼層指令碼:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

去除語法後,建構檔案和建構指令碼實際上與不同。然而,這在過去變得很大。我們可以在其他目錄中建立新的建構檔案,然後將這些檔案連結在一起。我們可以輕鬆新增依附於現有工作的新工作,並以隨機且複雜的方式運作。我們只需將單一工作的名稱傳遞至 ant 指令列工具,就能判斷需要執行的所有內容。

Ant 是 2000 年發行的古老軟體,Maven 和 Gradle 等其他工具在這段過渡時期,都已在 Ant 上有所改進,我們基本上會加入多項功能,例如自動管理外部依附元件,以及加入更簡潔的語法,而且不含任何 XML。但這些新版系統的性質維持不變:工程師可運用原則和模組化的方式編寫建構指令碼,做為工作,並提供工具來執行這些工作並管理依附元件。

任務型建構系統的暗面

這些工具基本上能讓工程師將任何指令碼定義為一項工作,因此其功能非常強大,能夠協助您實現他們想不到的一切。但由於這項特性具有缺點,而且以工作為基礎的建構系統可能會因建構指令碼變得更加複雜,而難以使用。這類系統的問題在於,它們其實會對工程師提供極大量的電力,以及對系統的電力不足。由於系統不知道指令碼正在執行的工作,因此效能會受到影響,因為在排程和執行建構步驟時,必須格外保守。系統也無法確認每個指令碼是否執行該動作,因此指令碼往往會變得複雜,最終變成需要偵錯。

無法平行執行建構步驟

現代化的開發工作站功能非常強大,具備多個可以平行執行多個建構步驟的核心。但是,工作型系統通常無法平行執行工作,即使它們看起來應該能夠執行。假設工作 A 依附於工作 B 和 C。由於工作 B 和 C 之間沒有依附元件,因此同時執行這些工作是否可以安全,以便系統更快進入工作 A?或是那些沒有涉及到相同的資源但可能並不可行 (兩者都使用同一個檔案來追蹤狀態,且同時執行這些檔案會導致衝突)。系統通常沒有察覺,因此有風險發生這些衝突 (引發罕見但難以偵錯的建構問題),或者必須限制整個建構作業,讓整個建構作業在單一程序中在單一執行緒上執行。這會造成強大開發人員機器的大量廢棄物,並完全規則排除將版本分散到多部機器的可能性。

難以執行漸進式建構

良好的建構系統可讓工程師執行可靠的漸進式版本,這樣只要小幅變更,就不必從頭重建構整個程式碼集。如果建構系統速度緩慢,且基於上述原因而無法平行處理建構步驟,這一點就格外重要。可惜的是 以工作為基礎的建構系統也很難以處理由於工作可以執行任何動作,一般來說無法檢查工作是否已完成。許多工作只會擷取一組來源檔案並執行編譯器來建立一組二進位檔;因此,如果基礎來源檔案並未變更,則不需要重新執行這些工作。然而,如果沒有額外資訊,系統無法確認這一點,也許工作會下載可能變更的檔案,或是寫入每次執行時都可能不同的時間戳記。為了確保正確性,系統通常必須在每個建構期間重新執行每項工作。部分建構系統會嘗試讓工程師指定需要重新執行任務的條件,藉此啟用漸進式版本。有時雖然可行,但問題往往比這個還要困難。舉例來說,在允許其他檔案直接納入檔案的語言 (例如 C++) 中,您不必剖析輸入來源,就無法判斷必須監看變更的整組檔案。工程師最後通常會取用捷徑,而這些捷徑可能會導致工作結果重複使用時,非常罕見且無用無慮的問題。這種情況經常發生時,工程師會在每個建構作業前開始執行乾淨的習慣,直到取得嶄新狀態,完全抵擋了一開始使用漸進式建構作業的目的。發現需要重新執行任務的時機非常細微,比人類更妥善處理工作。

難以維護指令碼及偵錯

最後,工作型建構系統設定的建構指令碼通常很難使用。雖然建構指令碼往往較不審慎,但建構指令碼就像建構系統一樣,而且提供方便隱藏錯誤的地方。以下是使用以工作為基礎的建構系統時常見的錯誤範例:

  • 工作 A 仰賴工作 B 產生特定檔案做為輸出。工作 B 的擁有者不知道其他工作依賴此工作,因此會將其變更為在其他位置產生輸出內容。只有在有人嘗試執行工作 A 並發現失敗時,才能偵測到此情況。
  • 工作 A 則依附於工作 B,而工作 B 則視工作 C 而定,該工作會產生工作 A 所需的輸出內容做為輸出內容。工作 B 的擁有者決定不需要再依賴工作 C,這會導致工作 A 失敗,即使工作 B 完全不在意工作 C 也不例外!
  • 新工作的開發人員不小心推斷了執行工作的機器,例如工具位置或特定環境變數的值。任務可在其機器上執行,但因其他開發人員嘗試失敗而失敗。
  • 工作包含非確定性的元件,例如從網際網路下載檔案,或是在版本中新增時間戳記。現在,人們每次執行建構作業時,都會得到截然不同的結果,這表示工程師不一定能重現彼此的故障情形,及修正在自動化建構系統中發生的錯誤或故障問題。
  • 含有多個依附元件的工作可能會產生競爭狀況。如果工作 A 仰賴工作 B 和工作 C,而工作 B 和 C 都修改了同一個檔案,則工作 A 會收到不同的結果,具體取決於 B 和 C 完成的工作。

此處的任務型架構無法解決這些效能、正確性或可維護性問題,並沒有一般用途。只要工程師可以編寫在建構期間執行的任意程式碼,系統就沒有足夠的資訊能夠隨時快速正確執行建構作業。如要解決這個問題,我們需要將工程師的力量放回系統手中,改將系統的角色重塑為執行中的工作,而非產生構件。

這種方法可以建立以構件為基礎的建構系統,例如 Blaze 和 Bazel。