以工作為基礎的建構系統

本頁面說明以工作為基礎的建構系統、其運作方式,以及工作式系統可能發生的一些小工具。殼層指令碼之後,以工作為基礎的建構系統是建構的下一個邏輯演變。

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

在以工作為基礎的建構系統中,其基本工作單位是任務。每項工作都是指令碼,可以執行任何邏輯,而工作會指定其他工作做為依附元件,這些工作必須在這些工作之前執行。目前使用的主要建構系統 (例如 Ant、Maven、Gradle、Greunt 和 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,後者依附於工作 C,而 C 會產生工作 A 所需的特定檔案做為輸出內容。工作 B 的擁有者決定不需要再依賴工作 C,導致工作 A 失敗,即使工作 B 完全不關心工作 C!
  • 新工作的開發人員不小心誤認為執行任務的機器,例如工具位置或特定環境變數的值。工作會在其機器上運作,但會在其他開發人員嘗試時失敗。
  • 工作包含非確定性的元件,例如從網際網路下載檔案,或將時間戳記新增至建構作業。現在,使用者每次執行建構作業時,都會得到不同的結果,這表示工程師不一定能夠重現及修正彼此的自動建構系統故障或錯誤。
  • 含有多個依附元件的工作可能會產生競爭條件。如果工作 A 同時依賴工作 B 和工作 C,且工作 B 和 C 都修改了同一個檔案,則工作 A 會根據 B 和 C 先完成的其中一項工作,取得不同的結果。

在本文所述的工作型架構中,沒有一般用途可以解決這些效能、正確性或可維護性的問題。只要工程師能編寫在建構期間執行的任意程式碼,系統往往無法取得足夠資訊,無法總是能快速正確執行建構作業。為解決問題,我們需要將工程師手中的力量放回系統手中,重新構思系統的角色,而非以執行工作,而是產生構件。

此方法促成了建立以構件為基礎的建構系統,例如 Blaze 和 Bazel。