本頁面將介紹以工作為基礎的建構系統、其運作方式,以及以工作為基礎的系統可能發生的某些複雜問題。在殼層指令碼之後,以任務為基礎的建構系統是下一個建構邏輯演變。
瞭解以任務為基礎的建構系統
在以工作為基礎的建構系統中,工作的基本單位是工作。每項工作都是一個可執行任何邏輯邏輯的指令碼,而工作會指定其他工作做為依附元件,這些工作必須事先執行。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」,而「task」一詞則是指「command」)。每項工作都會執行 Ant 定義的可能指令清單,包括建立和刪除目錄、執行 javac
和建立 JAR 檔案。這組指令可透過使用者提供的外掛程式擴充,涵蓋任何類型的邏輯。每項工作也可以透過 depends 屬性定義其依附的工作。這些依附元件會形成無環圖,如圖 1 所示。
圖 1. 顯示依附元件的非循環圖
使用者可透過提供工作給 Ant 指令列工具來執行建構作業。舉例來說,當使用者輸入 ant dist
時,Ant 會執行以下步驟:
- 載入目前目錄中名為
build.xml
的檔案,並剖析該檔案,建立圖 1 所示的圖表結構。 - 尋找指令列上提供的
dist
工作,並發現該工作依附於名為compile
的工作。 - 尋找名為
compile
的工作,並發現該工作會依附名為init
的工作。 - 尋找名為
init
的工作,並發現該工作沒有依附元件。 - 執行
init
工作中定義的指令。 - 在已執行該工作所有依附元件的情況下,執行
compile
工作中定義的指令。 - 由於所有任務的依附元件皆已執行,因此執行
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 等以構件為基礎的建構系統。