本頁面說明以工作為基礎的建構系統及其運作方式,以及工作型系統可能發生的一些複雜問題。在殼層指令碼之後,以工作為基礎的建構系統是下一個邏輯的演進。
瞭解以工作為基礎的建構系統
在以工作為基礎的建構系統中,工作的基本單位是工作。每個任務都是可執行任何邏輯的指令碼,而工作則指定其他工作,必須在這些工作之前執行。目前使用中的多數建構系統 (例如 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/*
去除語法時,buildfile 和建構指令碼實際上不會太大。但也透過這種方式達成目標我們可以在其他目錄中建立新的建構檔案並將其相互連結。我們可以輕鬆地以任意且複雜的方式,新增依附於現有工作的新增工作。我們只需要將單一工作的名稱傳遞至 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,這樣即使工作 B 完全不在工作 C,工作 A 也會失敗!
- 新工作的開發人員會意外假設執行工作的機器,例如工具的位置或特定環境變數的值。這項工作在機器上執行,但當其他開發人員嘗試時,此工作也會失敗。
- 工作包含不確定性元件,例如從網際網路下載檔案,或為建構作業新增時間戳記。現在,使用者每次執行建構作業時,都有可能產生不同的結果,這表示工程師不一定能夠重現並修正自動建構系統上發生的故障或失敗問題。
- 具有多個依附元件的工作可能會建立競爭狀況。如果工作 A 依附於工作 B 和工作 C,工作 B 和 C 都修改了同一個檔案,則工作 A 會根據工作 B 和 C 中先完成哪一個,而獲得不同的結果。
因此,以一般架構的方式,可按照本文所述的工作架構解決這些效能、正確性或維護性的問題。只要工程師能編寫在建構期間執行的任意程式碼,系統就無法取得足夠的資訊,以確保能夠快速且正確執行建構作業。為瞭解決這個問題,我們需要從工程師手中拿出一些力量,然後重新放回系統的手中,並重構系統的角色,而非將工作視為運作中的工作。
這個方法會建立一個以構件為基礎的建構系統,如 Blaze 與 Bazel。