輸出建置目標

2007 年 4 月 26 日

在過去幾天,我一直在檢閱我的一位同事朱利安·辛普森的一篇進行中的文章,主題是重構 ant 檔案。朱利安是我們的「部署人員」,負責將我們以敏捷為導向的工作習慣應用於系統部署。在這樣做的過程中,朱利安遇到了許多難纏的 ant 建置腳本。他的文章很好地描述了他最喜歡的一些清理混亂的方法。

他的其中一個觀察特別引起我的興趣,他指出 ant 檔案傾向於分解成以任務命名的目標(例如「編譯」、「單元測試」)。這些任務通常會合併到命令式目標呼叫中

<target name="cruise" depends="clean, compile, copy-static-files, 
unit-test, publish, javadoc, tag"/>

這種風格聞起來不對,因為它與建置檔案所依據的 基於相依性的運算模型 不相符。在使用此模型時,您會查看每個目標,詢問它依賴什麼,然後將這些相依性放入目標中。因此,這裡您的 unit-test 任務應該直接依賴於 compilecopy-static-files 目標,而不應該被後面的目標提及。

當面對明顯的愚蠢行為時,最簡單的作法就是發表一些輕浮的評論,例如「也許他們的系統非常簡單,可以在不編譯的情況下執行測試」。但通常值得深入探討。

朱利安和我都有 Unix 背景,因此我們熟悉 make 建置語言。與 Ant 類似,make 使用基於相依性的程式設計。這兩個系統都會將工作分解成透過相依性關聯連結的目標。但兩者之間有一個細微的差異。在 ant 中,您會為目標命名,並指出您相依的其他目標。然而,在 make 中,您會陳述一個輸出檔案、它所相依的輸入檔案,以及如何從一個檔案轉換到另一個檔案。

因此,在 make 中編譯一個兩個檔案的 hello world(使用 C 語言)看起來會像這樣

hello : hello.o greet.o
  gcc -o hello hello.o greet.o

hello.o : hello.c 
  gcc -c hello.c

greet.o: greet.c
  gcc -c greet.c

第一行表示目標檔案 hello 相依於檔案 hello.ogreet.o。第二行告訴您如何從原始碼檔案建立目標(在實際的 make 檔案中,大部分都是一般規則,因此您不必重複它們)。然後,我們有類似的規則來顯示如何建立頂層規則的輸入。

在 ant 中(使用 Java 語言)看起來像這樣

<project name = "hello" default = "test">
  <target name = "compile">
    <javac 
      srcdir = "src"
      destdir = "build"
    />
  </target>
  <target name = "test" depends = "compile">
    <junit printsummary="on">
      <classpath location = "build"/>
      <test name = "Tester"/>
    </junit>
  </target>   
</project>

在這裡,我們表示我們有一個測試任務相依於編譯任務,這表示在我們執行測試任務之前,必須在某個時間點執行編譯任務。

這兩個片段都示範了相依性,但命名上的差異會影響我們思考它們的方式。我想知道那個命名慣例如何讓您的思緒從思考相依性轉移到思考命令式規則。

這方面的另一個面向是我們如何處理不必要的作業。建置似乎總是花費比我們預期的更長的時間,因此我們會盡量避免執行不需要執行的作業。事實上,這正是最初使用基於相依性的程式設計的重點。即使一個目標被提及多次,它也只會執行一次。

但這種避免不必要作業的渴望也延伸到執行目標本身。make 中避免作業的基本機制是比較輸出和輸入檔案的修改時間。如果輸出比最年輕的輸入還新,那麼它顯然是最新的,不需要重新建立。

Ant 沒有這麼做,而是仰賴 ant 任務進行自己的分析,以查看需要執行什麼。當編譯器需要找出在變更後需要重新建置哪些輸出類別時,這很有道理。然而,人們經常自己撰寫目標,而這些目標並未進行某種檢查。對於任何目標,你都應該確定它不會執行任何重大工作,除非某些事情可能已經變更;專注於輸出是執行此操作的好方法。

有時輸出並不顯而易見。單元測試任務的輸出是什麼?戴上我的 make 帽子,我會說「某種檔案」。你可以讓測試報告成為結果,並在任務中檢查測試報告檔案是否比任何輸入都舊,儘管這可能不如你希望的那麼容易。報告甚至不必包含任何內容,它可以只是一個 TouchFile

我的 unix-make 根源在此處是否過於誇張?有可能。但我會建議你查看你的建置檔案,並考慮

  • 根據其產品為每個目標命名(而不是它執行的活動)。
  • 確保目標在不需要時不執行任何重大工作。
  • 直接陳述每個目標的相依性,讓建置系統找出要遵循的命令順序。