本頁說明以工作為基礎的建構系統、運作方式,以及工作型系統可能會發生的一些小工具。在殼層指令碼之後,工作型建構系統是新一代的建構邏輯。
瞭解以工作為基礎的建構系統
在以工作為基礎的建構系統中,工作的基本單位是任務。每項工作都是可以執行任何邏輯的指令碼,而工作會將其他工作指定為依附元件,工作必須在此工作之前執行。現今使用的大多數主要建構系統 (例如 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 會執行下列步驟:
- 在目前的目錄中載入名為
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 的擁有者不知道其他工作依賴此工作,因此會將其變更為在其他位置產生輸出內容。只有在有人嘗試執行工作 A 並發現失敗時,才能偵測到此情況。
- 工作 A 則依附於工作 B,而工作 B 則視工作 C 而定,該工作會產生工作 A 所需的輸出內容做為輸出內容。工作 B 的擁有者決定不需要再依賴工作 C,這會導致工作 A 失敗,即使工作 B 完全不在意工作 C 也不例外!
- 新工作的開發人員不小心推斷了執行工作的機器,例如工具位置或特定環境變數的值。任務可在其機器上執行,但因其他開發人員嘗試失敗而失敗。
- 工作包含非確定性的元件,例如從網際網路下載檔案,或是在版本中新增時間戳記。現在,人們每次執行建構作業時,都會得到截然不同的結果,這表示工程師不一定能重現彼此的故障情形,及修正在自動化建構系統中發生的錯誤或故障問題。
- 含有多個依附元件的工作可能會產生競爭狀況。如果工作 A 仰賴工作 B 和工作 C,而工作 B 和 C 都修改了同一個檔案,則工作 A 會收到不同的結果,具體取決於 B 和 C 完成的工作。
此處的任務型架構無法解決這些效能、正確性或可維護性問題,並沒有一般用途。只要工程師可以編寫在建構期間執行的任意程式碼,系統就沒有足夠的資訊能夠隨時快速正確執行建構作業。如要解決這個問題,我們需要將工程師的力量放回系統手中,改將系統的角色重塑為執行中的工作,而非產生構件。
這種方法可以建立以構件為基礎的建構系統,例如 Blaze 和 Bazel。