本頁面說明以工作為基礎的建構系統、運作方式,以及以工作為基礎的系統所發生的一些複雜問題。在殼層指令碼之後,以工作為基礎的建構系統是建築物的下一邏輯邏輯演進。
瞭解以工作為基礎的建構系統
在以工作為基礎的建構系統中,工作的基本單位為工作。每項工作都是能夠執行任何邏輯的指令碼,而工作則是指定其他工作前必須先執行的依附元件。目前使用中大部分的主要建構系統 (例如 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 定義的可能指令清單,當中包含建立和刪除目錄、執行 javac
及建立 JAR 檔案。這組指令可由使用者提供的外掛程式來擴充,以涵蓋任何類型的邏輯。每項工作也能透過相依屬性定義其相依的工作。這些依附元件構成了循環圖,如圖 1 所示。
圖 1:顯示依附元件的循環圖
使用者可以將工作指派給 Ant 指令列工具,藉此執行建構作業。例如,當使用者輸入 ant dist
時,Ant 請執行下列步驟:
- 在目前的目錄中載入名為
build.xml
的檔案,然後加以剖析,以建立圖 1 所示的圖形結構。 - 尋找在指令列中提供的名為
dist
的工作,並發現這個工作與名為compile
的工作有相依關係。 - 尋找名為
compile
的工作,並且發現它與名為init
的工作有相依關係。 - 找出名為
init
的工作,並發現這個依附元件沒有依附元件。 - 執行
init
工作中定義的指令。 - 執行
compile
工作中定義的指令,前提是該工作的所有依附元件都已執行完畢。 - 執行
dist
工作中定義的指令,前提是該工作的所有依附元件都已執行完畢。
最後,Ant 執行 dist
工作時執行的程式碼相當於下列 shell 指令碼:
./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 等其他工具則是在中間多年不斷改善的,而且基本上是透過新增外部依附元件自動管理和更清晰的語法等功能取代了 XML。但這些新系統的性質則保持不變:工程師能夠以工作 (以模組化) 的模式編寫建構作業指令碼,並提供工具來執行這些工作,並管理這些依附元件之間的依附元件。
任務型建構系統的黑暗面
由於這些工具基本上能讓工程師將任何工作定義為工作,因此功能非常強大,可讓您執行足以想像這些概念的一切。然而,前述的提取功能帶來缺點,而隨著建構指令碼越來越複雜,工作導向的建構系統也會變得難以處理。這類系統所面臨的問題,就是最終導致為工程師提供足夠的電力,而對系統而言,沒有足夠的電力。由於系統不知道指令碼的用途,因此效能會降低,因為排程的排程和執行步驟必須非常保守。而且系統也沒辦法確認每個指令碼是否能正確執行,因此指令碼的複雜性通常會變得越來越複雜,最終會需要偵錯。
平行處理建構步驟的步驟
現代化開發工作站非常強大,多個核心可以同時執行多個建構步驟。然而,以工作為基礎的系統,通常難以平行處理工作,即使似乎應該要如做。假設工作 A 取決於工作 B 和 C。由於工作 B 和 C 彼此之間沒有相依關係,因此可以同時執行這些工作,這樣一來,系統就能更快地完成工作 A?例如,他們若未輕觸任何資源 但也未必會發生,因為兩者都會使用相同的檔案來追蹤狀態,而且同時執行。不過,系統通常沒辦法知道系統是否面臨這類衝突的風險 (例如,在極少數情況下可能導致偵錯問題),或是必須對整個專案設下限制。以單一程序在單一執行緒中執行。這可能會是強大的開發人員機器的大量廢棄物,而它可以完全排除分散於多部機器的可能性。
無法順利執行漸進式建構作業
完善的建構系統可讓工程師執行可靠的漸進式建構作業,這樣一來,只要小幅變更即可,不必從頭重頭建構整個程式碼集。如果建構系統速度緩慢,且因上述原因無法針對建構步驟進行平行處理,這點尤其重要。然而,以工作為基礎的建構系統也非常困難。由於工作可以執行任何動作,所以一般而言,您無法檢查這些工作是否已完成。許多工作只會擷取一組來源檔案並執行編譯器,以建立一組二進位檔;因此,如果基礎來源檔案並未變更,就不必重新執行這些套件。但是,如果沒有其他資訊,系統可能就無法確定這點,例如工作會下載可能變更的檔案,或是寫入每次執行作業可能不同的時間戳記。為了確保準確度,系統通常必須在每次建構作業期間重新執行每項工作。部分建構系統會允許工程師指定必須重新執行工作的條件,藉此啟用增量建構作業。這個做法有時可行,但這是最棘手的問題。例如,如果使用 C++ 之類的語言來允許其他檔案直接納入檔案,則在沒有剖析輸入來源的情況下,系統無法確定必須查看變更的整個檔案組合。工程師通常會使用捷徑,這些捷徑可能會導致罕見且令人不悅的問題,因為重複使用工作結果應該不會重複使用。當這種情況經常發生時,工程師會在每次建構前養成清爽的執行環境,接著取得新狀態,完全打破初始建構的目的。要找出需要重新執行的工作,雖然非常少見,且機器的機器處理能力優於真人。
維護和偵錯指令碼困難
最後,工作型建構系統所建構的建構指令碼通常非常困難。雖然這些應用程式的開發通常較少,但建構指令碼跟系統建構程式碼一樣簡單,而且很容易隱藏。以下為使用工作型建構系統時常見的錯誤示例:
- 工作 A 仰賴工作 B 產生特定檔案做為輸出。工作 B 的擁有者並未意識到其他工作仰賴該工作,因此他們變更了工作,在其他位置產生輸出。除非有人嘗試執行工作 A 並發現工作失敗,否則系統不會偵測到這個情況。
- 工作 A 仰賴工作 B,工作 B 取決於工作 C,而產生的工作是工作 A 所需的輸出。工作 B 的擁有者決定不需要進一步依賴工作 C,導致工作 A 完全無法處理工作 C!
- 新工作的開發人員不小心對執行工作的機器做出假設,例如工具的位置或特定環境變數的值。這個工作會在機器上運作,但在其他開發人員嘗試執行時失敗。
- 工作中包含確定性元件,例如從網際網路下載檔案,或為建構作業新增時間戳記。現在,每次執行建構作業時,結果都可能會產生不同的結果,因此工程師不一定能重現並修正自動化建構作業系統上發生的故障或失敗情況。
- 包含多個依附元件的工作會建立競爭條件。如果工作 A 同時依賴工作 B 和工作 C,而工作 B 和 C 都修改同一個檔案,工作 A 就會得到不同的結果,這取決於工作 B 和 C 先完成了哪些工作。
這裡所述的工作導向架構無法解決這些效能、正確性或永續發展的問題,並非一般用途。只要工程師能夠建構在建構期間執行的任意程式碼,系統就無法取得足夠的資訊,讓系統隨時都能正確、正確地執行建構。為解決這個問題,我們必須從工程師的手中取出一些權力,然後將其放回系統手中,並以視覺化方式說明系統的角色,而非執行工作,而非產生成果。
這個方法導致建立以成果為基礎的建構系統,例如 Blaze 和 Bazel。