以工作為基礎的建構系統

回報問題 查看原始碼 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

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

在以工作為基礎的建構系統中,工作的基本單位是工作。每個工作都是可執行任何類型邏輯的指令碼,且工作會將其他工作指定為必須先執行的依附元件。目前使用的大多數主要建構系統 (例如 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/*

去除語法後,Buildfile 和 Build 指令碼其實沒有太大差異。但我們已經從中獲得許多收穫。可以在其他目錄中建立新的建構檔案,並將其連結在一起。我們可以輕鬆以任意複雜的方式,新增依賴現有工作的新工作。我們只需要將單一工作的名稱傳遞至 ant 指令列工具,即可判定需要執行的所有內容。

Ant 是一款舊軟體,最初於 2000 年發布。在過去幾年,Maven 和 Gradle 等其他工具已改善 Ant 的功能,並透過新增自動管理外部依附元件和不含任何 XML 的簡潔語法等功能,實際取代了 Ant。不過,這些新系統的本質仍相同:讓工程師以原則性和模組化的方式編寫建構指令碼,做為工作,並提供工具來執行這些工作,以及管理其中的依附元件。

以任務為基礎的建構系統黑暗面

由於這些工具可讓工程師將任何指令碼定義為工作,因此功能非常強大,可讓您執行任何可想像的操作。但這項功能也有缺點,隨著建構指令碼變得越來越複雜,以工作為基礎的建構系統可能會變得難以使用。這種系統的問題在於,實際上會讓工程師擁有過多權力,而系統的權力不足。由於系統不知道指令碼正在執行的作業,因此效能會降低,因為系統必須嚴格選擇如何排定及執行建構步驟。而且系統無法確認每個指令碼都能正常運作,因此指令碼往往會複雜增加,最終會成為需要偵錯的項目。

難以平行處理建構步驟

現代開發工作站十分強大,多個核心能夠同時執行數個建構步驟。但以工作為基礎的系統通常無法平行執行工作,即使看起來似乎可以平行執行也一樣。假設工作 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 等以構件為基礎的建構系統。