本頁面說明以工作為基礎的建構系統、運作方式,以及這類系統可能發生的問題。在 Shell 指令碼之後,以工作為基礎的建構系統是建構作業的下一個邏輯演進。
瞭解以工作為基礎的建構系統
在以工作為基礎的建構系統中,基本工作單位是工作。每項工作都是可執行任何類型邏輯的指令碼,且工作會指定其他工作做為依附元件,這些依附元件必須先執行。目前使用的大部分主要建構系統 (例如 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 使用「目標」一詞代表「工作」,並使用「工作」一詞代表「指令」)。每個工作都會執行 Ant 定義的可能指令清單,包括建立和刪除目錄、執行 javac,以及建立 JAR 檔案。使用者提供的外掛程式可以擴充這組指令,涵蓋任何類型的邏輯。每項工作也可以使用 depends 屬性定義依附的工作。這些依附元件會形成無環圖,如圖 1 所示。
圖 1. 顯示依附元件的非循環圖
使用者將工作提供給 Ant 的指令列工具,即可執行建構作業。舉例來說,當使用者輸入 ant dist 時,Ant 會執行下列步驟:
- 載入目前目錄中名為 build.xml的檔案,並剖析該檔案以建立圖 1 所示的圖表結構。
- 尋找指令列中提供的 dist工作,並發現該工作依附於compile工作。
- 尋找名為 compile的工作,並發現該工作依附於名為init的工作。
- 尋找名為 init的工作,並發現該工作沒有依附元件。
- 執行 init工作中定義的指令。
- 執行 compile工作中定義的指令,前提是該工作的所有依附元件都已執行。
- 執行 dist工作中定義的指令,前提是該工作的所有依附元件都已執行。
最後,Ant 在執行 dist 工作時執行的程式碼,相當於下列殼層指令碼:
./createTimestamp.shmkdir build/javac src/* -d build/mkdir -p dist/lib/jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*
如果去除語法,建構檔案和建構指令碼其實不會有太大差異。但我們已從中獲益良多。我們可以在其他目錄中建立新的建構檔案,並將這些檔案連結在一起。我們可以輕鬆新增任意複雜的任務,這些任務會依附在現有任務上。我們只需要將單一工作的名稱傳遞至 ant 指令列工具,該工具就會判斷需要執行的所有項目。
Ant 是舊版軟體,最初於 2000 年發布。在 Ant 之後的幾年,Maven 和 Gradle 等其他工具都經過改良,並新增自動管理外部依附元件等功能,以及不含任何 XML 的簡潔語法,基本上已取代 Ant。但這些新系統的本質不變:工程師可以有原則地以模組化方式編寫建構指令碼做為工作,並提供執行這些工作及管理工作間依附元件的工具。
以工作為基礎的建構系統的陰暗面
因為工程師基本上可以將任何指令碼定義為工作,所以這些工具非常強大,幾乎能讓您完成任何想像得到的作業。但這項強大功能也有缺點,隨著建構指令碼越來越複雜,以工作為基礎的建構系統可能會變得難以使用。這類系統的問題在於,它們最終會提供過多電力給工程師,但提供給系統的電力卻不足。由於系統不瞭解指令碼的用途,因此必須非常保守地排定及執行建構步驟,導致效能不佳。系統也無法確認每個指令碼是否正常運作,因此指令碼往往會變得越來越複雜,最後又需要進行偵錯。
難以平行處理建構步驟
現代開發工作站相當強大,具備多個核心,可平行執行多個建構步驟。但即使任務導向的系統看似能夠平行執行任務,通常也無法做到。假設任務 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。
