本頁面說明以任務為基礎的建構系統、其運作方式,以及與工作式系統可能發生的一些小工具。在殼層指令碼之後,以任務為基礎的建構系統是下一個建構邏輯演變。
瞭解以任務為基礎的建構系統
在以任務為基礎的建構系統中,工作的基本單位是工作。每項工作都是一個可執行任何邏輯的指令碼,而工作會指定其他工作做為依附元件,這些工作會必須先執行。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>
buildfile 是以 XML 編寫,並定義了一些與建構作業相關的簡單中繼資料,以及工作清單 (XML 中的 <target>
標記)。(Ant 使用「target」一詞來代表「任務」,並使用 task 一詞來參照指令)。每項工作都會執行 Ant 定義的可能指令清單,其中包括建立及刪除目錄、執行 javac
,以及建立 JAR 檔案。使用者提供的外掛程式可以擴充這組指令,以涵蓋任何類型的邏輯。每項工作也可透過相依屬性定義工作依附的工作。這些依附元件形成非循環圖,如圖 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/*
如果去除語法,建構檔案和建構指令碼實際上也不會有差異。不過這種做法已大幅提升。可以在其他目錄中建立新的建構檔案,並將其連結在一起。我們可以輕鬆新增依賴現有工作的新工作,而且這些工作會以任意且複雜的方式進行。我們只需要將單一工作的名稱傳遞至 ant
指令列工具,即可判定需要執行的所有內容。
Ant 是 2000 年發行的老舊軟體,Maven 和 Gradle 等其他工具在 Ant 的過渡時期中做了改善,基本上就是新增了多項功能,例如自動管理外部依附元件,以及在沒有 XML 的情況下採用更簡潔的語法。但這些新系統的性質則維持不變:工程師能依工作原則和模組化方式來撰寫建構指令碼,並提供用來執行工作及管理依附元件的工具。
以任務為基礎的建構系統黑暗面
這些工具基本上可讓工程師將任何指令碼定義為工作,因此功能極為強大,可讓您執行各種想像。但這種功能有其缺點,而以任務為基礎的建構系統可能會因建構指令碼變得更加複雜,而難以使用。這類系統的問題在於,這類系統實際上會帶給系統太多能力,導致系統缺乏足夠電力。由於系統不知道指令碼正在執行的作業,因此效能會降低,因為系統必須嚴格選擇如何排定及執行建構步驟。而且系統無法確認每個指令碼都能正常運作,因此指令碼往往會變得複雜而增加,最後是需要偵錯的另一個項目。
難以平行處理建構步驟
現代開發工作站十分強大,多個核心能夠同時執行數個建構步驟。但是,以任務為基礎的系統通常無法平行執行任務執行作業,即使看起來應該能夠平行執行。假設工作 A 依附工作 B 和 C。由於工作 B 和 C 之間沒有依附元件,所以同時執行這些工作是否安全,系統能否更快進入工作 A?或許他們沒有碰到相同的資源但或許不會,兩者都可能用同一個檔案來追蹤狀態,並同時執行兩者,這樣會造成衝突。一般來說,系統沒有辦法得知,因此必須發生這些衝突 (造成建構問題極少但難以偵錯),或是限制整個建構作業在單一程序中的單一執行緒執行。這可能會浪費效能強大的開發人員機器,而且完全排除將建構分散到多部機器的可能性。
難以執行漸進式建構作業
理想的建構系統可讓工程師執行可靠的漸進式建構作業,即使小幅變更,也不需要從頭開始重新建構整個程式碼集。如果建構系統速度緩慢,且因為上述原因無法平行處理建構步驟,這一點就格外重要。但可惜的是,以任務為基礎的建構系統也很難解決。由於工作可以執行任何作業,因此通常您無法檢查這些工作是否已完成。許多工作只需使用一組來源檔案並執行編譯器,即可建立一組二進位檔;因此,如果基礎來源檔案沒有變更,也不必重新執行這些二進位檔。但如果沒有額外資訊,系統無法確定此名稱,可能是工作下載的檔案可能已變更,或者所寫入的時間戳記可能在每次執行時都不同。為了確保正確性,系統通常必須在每次建構期間重新執行每項工作。有些建構系統會讓工程師指定需要重新執行任務的條件,藉此嘗試啟用漸進式版本。有時這可以避免,但這往往不容易。舉例來說,在允許其他檔案直接納入檔案的 C++ 等語言中,在不剖析輸入來源的情況下,就無法判斷必須觀察變更的整組檔案。工程師最後會佔用捷徑,而這些快速鍵可能會導致重複使用的工作結果時,很少有人使用,並對其造成困擾。如果這種情況經常發生,工程師會在每次版本前先養成清理的習慣,才能取得全新狀態,完全擊敗了一開始使用漸進式建構作業的目的。瞭解何時需要重新執行工作會變得非常細微,而且機器比人類更能妥善處理工作。
難以維護指令碼及偵錯
最後,以工作為基礎的建構系統所附帶的建構指令碼通常很難使用。雖然建構指令碼通常較不會經過審查,但建構指令碼就像建構系統的程式碼,是容易隱藏的簡易位置。以下是使用以任務為基礎的建構系統時常見的錯誤範例:
- 工作 A 仰賴工作 B 產生特定檔案做為輸出。工作 B 的擁有者沒有意識到其他工作需要依賴該工作,所以會變更工作 B 來在不同位置產生輸出。只有在有人嘗試執行任務 A 且發現失敗時,才能偵測此工作。
- 工作 A 依附工作 B,而工作 B 依附工作 C,而工作 C 會產生特定檔案做為工作 A 所需的輸出內容。工作 B 的擁有者認為這項工作不需要再依附工作 C,這將導致工作 A 失敗,即使工作 B 完全不在意工作 C!
- 新工作的開發人員不小心假設執行工作的機器,例如工具位置或特定環境變數的值。這項工作可以在他們的機器上運作,但每當其他開發人員嘗試執行該工作時就會失敗。
- 工作包含非確定性的元件,例如從網際網路下載檔案,或為建構作業新增時間戳記。現在,使用者每次執行建構時都會得到不同的結果,也就是說,工程師不一定能夠重現並修正在自動化建構系統上發生的故障或故障情形。
- 具有多個依附元件的工作可能會產生競爭狀況。如果工作 A 同時仰賴工作 B 和工作 C,而工作 B 和 C 都修改了相同檔案,則工作 A 會根據 B 和 C 其中一項最先完成的工作,產生不同的結果。
在本文以工作為基礎的架構中,並沒有一般用途可以解決這些效能、正確性或可維護性問題。只要工程師可以編寫在建構期間執行的任意程式碼,系統就無法取得足夠的資訊,一律以快速正確的方式執行建構作業。如要解決這個問題,我們必須拆解工程師的操作權限,重新將其放回系統手中,重新將系統角色的概念視為執行中工作,而非執行中工作。
這種做法導致必須建立 Blaze 和 Bazel 等以構件為基礎的建構系統。