以工作為基礎的建構系統

回報問題 查看原始碼 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

本頁面將介紹以工作為基礎的建構系統、其運作方式,以及以工作為基礎的系統可能發生的某些複雜問題。在殼層指令碼之後,以工作為基礎的建構系統是建構作業的下一個邏輯演進。

瞭解以工作為基礎的建構系統

在以任務為基礎的建構系統中,工作的基本單位是工作。每個工作都是可執行任何類型邏輯的指令碼,且工作會將其他工作指定為必須先執行的依附元件。目前使用的大多數主要建構系統 (例如 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」,而「task」一詞則是指「command」)。每項工作都會執行 Ant 定義的可能指令清單,包括建立和刪除目錄、執行 javac 和建立 JAR 檔案。使用者提供的外掛程式可以擴充這組指令,以涵蓋任何類型的邏輯。每項工作也可以透過 depends 屬性定義其依附的工作。這些依附元件會形成無環圖,如圖 1 所示。

顯示依附元件的加速圖

圖 1. 顯示依附元件的非循環圖

使用者可透過提供工作給 Ant 指令列工具來執行建構作業。舉例來說,當使用者輸入 ant dist 時,Ant 會執行以下步驟:

  1. 載入目前目錄中名為 build.xml 的檔案,並剖析該檔案,建立圖 1 所示的圖表結構。
  2. 尋找指令列上提供的 dist 工作,並發現該工作依附於名為 compile 的工作。
  3. 尋找名為 compile 的工作,並發現其具有名為 init 的工作的依附元件。
  4. 尋找名為 init 的工作,並發現該工作沒有依附元件。
  5. 執行 init 工作中定義的指令。
  6. 由於所有任務的依附元件皆已執行,因此執行 compile 工作中定義的指令。
  7. 在已執行該工作所有依附元件的情況下,執行 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 等其他工具在 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 等以構件為基礎的建構系統。