使用 Rake 建構語言

Rake 是一種建構語言,用途類似於 make 和 ant。與 make 和 ant 一樣,它是一種特定領域語言,但與這兩者不同的是,它是一種使用 Ruby 語言編寫的內部 DSL。在本文中,我將介紹 Rake,並描述一些從使用 Rake 建構此網站中獲得的有趣發現:相依性模型、綜合任務、自訂建構常式,以及除錯建構指令碼。

2014 年 12 月 29 日



多年來,我一直在廣泛使用 Ruby。我喜歡它簡潔但強大的語法,以及它通常寫得很好的函式庫。幾年前,我將我的大部分網站產生程式碼從 XSLT 轉換為 Ruby,並且對這個改變感到非常滿意。

如果您是我的常客,您一定不會驚訝地得知我的整個網站都是自動建構的。我最初使用 ant(在 Java 世界中很流行的建構環境)來執行此操作,因為它與 Java XSL 處理器很契合。由於我一直在使用 Ruby,因此我更常使用 Rake,這是一種由 Jim Weirich 開發的基於 Ruby 的建構語言。最近,我完全取代了建構流程,移除了所有 ant,轉而使用 Rake。

在我使用 Rake 的初期,我用類似於使用 ant 的方式來使用它。然而,在這次推動中,我嘗試以不同的方式來執行,以探索 Rake 的一些有趣功能。因此,我認為我應該寫這篇文章來深入探討這些領域。Rake 是我的第三個建置語言。我在多年前使用 make(而且已經忘記很多了)。我在過去六年左右的時間裡大量使用 ant。Rake 有許多這些語言具有的功能,以及一些對我來說是新穎的功能。儘管 Rake 是用 Ruby 編寫的,而且大量使用該語言,但你可以將它用於任何自動化建置處理。對於簡單的建置指令碼,你不需要知道 Ruby,但一旦事情開始變得更有趣,那麼你需要知道 Ruby 才能讓 Rake 發揮最佳效能。

這是一個有點偏頗的故事。我並不是試著撰寫 Rake 教學手冊 - 我將專注於我發現有趣的事情,而不是提供完整的說明。我不會假設你知道 Ruby、Rake,或任何其他建置語言。我將在進行的過程中說明 Ruby 的相關部分。希望如果你曾經使用過這些工具,或者只是對不同的運算模型感興趣,你會發現這值得一讀。

基於相依性的程式設計

等等 - 在前一段中,我說「不同的運算模型」。這對建置語言來說不是一個很宏偉的說法嗎?嗯,不是的,不是。我使用過的所有建置語言(make、ant (Nant) 和 rake)都使用基於相依性的運算風格,而不是通常的命令式風格。這讓我們以不同的方式思考如何對它們進行程式設計。大多數人不會這麼想,因為大多數建置指令碼都很短,但這實際上是一個非常深遠的差異。

是時候舉例了。想像一下我們想要撰寫一個程式來建置一個專案。我們有幾個不同的步驟來進行這個建置。

  • CodeGen:取得資料設定檔,並使用它們來產生資料庫結構和存取資料庫的程式碼。
  • 編譯:編譯應用程式程式碼。
  • 資料載入:將測試資料載入資料庫。
  • 測試:執行測試。

我們需要能夠獨立執行這些任務中的任何一個,並確保一切都正常運作。在執行所有前置步驟之前,我們無法進行測試。編譯和資料載入需要先執行 CodeGen。我們如何表達這些規則?

如果我們以命令式風格執行,它看起來像這樣,使每個任務成為一個 ruby 程序。

# this is comment in ruby
def codeGen  #def introduces a procedure or method
  # do code gen stuff
end

def compile
  codeGen
  # do compile stuff
end

def dataLoad
  codeGen
  # do data load stuff
end

def test
  compile
  dataLoad
  #run tests
end

請注意,這有一個問題。如果我呼叫 test,則會執行 codeGen 步驟兩次。這不會造成錯誤,因為 codeGen 步驟(我假設)是冪等的,也就是呼叫它多次與呼叫一次沒有不同。但這會花費時間,而建置很少有時間可以浪費。

為了修正這個問題,我可以將步驟區分為公開和內部部分,如下所示

def compile
  codeGen
  doCompile
end

def doCompile
  # do the compile
end

def dataLoad
  codeGen
  doDataLoad
end

def doDataLoad
  #do the data load stuff
end

def test
  codeGen
  doCompile
  doDataLoad
  #run the tests
end

這有效,但有點混亂。這也是依賴性系統如何提供協助的完美範例。在命令式模型中,每個常式會呼叫常式中的步驟。在依賴性系統中,我們有工作和指定先決條件(其依賴性)。當您呼叫工作時,它會查看有哪些先決條件,然後安排呼叫每個先決條件工作一次。因此,我們的簡單範例會如下所示。

task :codeGen do
  # do the code generation
end

task :compile => :codeGen do
  #do the compilation
end

task :dataLoad => :codeGen do
  # load the test data
end

task :test => [:compile, :dataLoad] do
  # run the tests
end

(希望您能理解這句話的意思,我稍後會正確說明語法。)

現在,如果我呼叫 compile,系統會查看 compile 工作,並看到它依賴於 codeGen 工作。然後,它會查看 codeGen 工作,並看到沒有先決條件。因此,它會執行 codeGen,然後執行 compile。這與命令式情況相同。

當然,有趣的情況是測試。在這裡,系統看到 compile 和 dataLoad 都依賴於 codeGen,因此它會安排工作,以便 codeGen 先執行,然後是 compile 和 dataload(順序不拘),最後是 test。基本上,實際執行的任務順序是由執行引擎在執行時決定的,而不是由撰寫建置指令碼的程式設計師在設計時決定的。

這種基於依賴性的運算模型非常適合建置流程,這就是為什麼這三者都使用它的原因。以工作和依賴性來思考建置是很自然的,建置中的大多數步驟都是冪等的,而且我們真的不希望不必要的作業減慢建置速度。我懷疑很少人敲出建置指令碼時會意識到他們正在使用一種時髦的運算模型,但這就是它。

建構的特定領域語言

我的三個建置語言都有一個共同的特徵,它們都是 特定領域語言 (DSL) 的範例。但是,它們是不同類型的 DSL。在我之前使用的術語中

  • make 是一個使用自訂語法的外部 DSL
  • ant(和 nant)是一個使用基於 XML 語法的外部 DSL
  • rake 是一個使用 Ruby 的內部 DSL。

rake 是通用語言的內部 DSL,這一點與其他兩種語言有非常重要的區別。它基本上允許我在任何需要時使用 ruby 的全部功能,代價是必須做一些奇怪的事情來確保 rake 腳本是有效的 ruby。由於 ruby 是種不引人注目的語言,因此在語法奇異性方面沒有太多問題。此外,由於 ruby 是種成熟的語言,因此我不需要退出 DSL 來做有趣的事情,而這一直是使用 make 和 ant 時經常遇到的挫折。事實上,我已經開始認為建置語言非常適合內部 DSL,因為你確實需要足夠頻繁地使用這種完整的語言功能,這才值得,而且沒有很多非程式設計人員撰寫建置腳本。

Rake 任務

Rake 定義了兩種任務。一般任務類似於 ant 中的任務,而檔案任務類似於 make 中的任務。如果這兩種說法對你來說都沒有意義,別擔心,我馬上就要解釋。

一般任務最容易解釋。以下是我的測試環境建置腳本中的一個任務。

task :build_refact => [:clean] do
  target = SITE_DIR + 'refact/'
  mkdir_p target
  require 'refactoringHome'
  OutputCapturer.new.run {run_refactoring}
end
 

第一行定義了大部分任務。在此語言中,task 實際上是一個關鍵字,用於引入任務定義。:build_refact 是任務的名稱。命名語法有點奇怪,因為我們需要以冒號開頭,這是作為內部 DSL 的後果之一。

在任務名稱之後,我們接著進入先決條件。這裡只有一個,:clean。語法是 => [:clean]。我們可以在方括號內列出多個相依性,並以逗號分隔。從前面許多的範例中,你可以看到如果只有一個任務,我們不需要方括號。如果沒有任何相依性(或確實有其他原因,這是一個迷人的主題,我稍後會提到),我們根本不需要相依性。

要定義任務主體,我們在 doend 內撰寫 ruby 程式碼。在此區塊內,我們可以放入任何我們喜歡的有效 ruby,我不會在此說明此程式碼,因為你不需要了解它就能了解任務如何運作。

rake 腳本(或 rubyist 稱之為 rakefile)的優點是,你可以清楚地將其讀為建置腳本。如果我們要撰寫等效的 ant,它看起來會像這樣

<target name = "build_refact" depends = "clean">
<-- define the task -->
</target>

現在,你可以將其視為 DSL 並遵循它,但由於它是內部 DSL,你可能想知道它如何作為有效的 ruby 運作。實際上,task 不是關鍵字,而是常式呼叫。它需要兩個引數。

第一個參數是一個雜湊(等同於一個映射或字典)。Ruby 有一個針對雜湊的特殊語法。一般來說,語法是 {key1 => value1, key2 => value2}。但是,如果只有一個雜湊,則大括號是可選的,因此在定義 rake 任務時不需要它們,這有助於簡化 DSL。那麼,什麼是鍵和值?這裡的鍵是一個符號 - 由開頭的冒號在 ruby 中標識。你可以使用其他文字,我們很快就會看到字串,你也可以使用變數和常數 - 我們會發現它們相當方便。值是一個陣列 - 這實際上等同於其他語言中的清單。這裡我們列出其他任務的名稱。如果我們不使用方括號,我們只有一個值而不是一個清單 - rake 會處理陣列或單一文字 - 我必須說,它非常適應。

那麼第二個參數在哪裡?它位於 doend 之間 - 一個區塊 - ruby 對 封閉 的稱呼。因此,當 rake 檔案執行時,它會建立這些任務物件的物件圖形,透過依賴關係連結彼此,並且每一個在適當時機執行一個區塊。一旦建立所有任務,rake 系統就可以使用依賴關係找出哪些任務需要按什麼順序執行,然後執行,按適當順序呼叫每個任務的區塊。封閉的一個關鍵屬性是它們在評估時不需要執行,它們可以儲存以供以後使用 - 即使它們參照在區塊實際執行時不在範圍內的變數。

這裡的事情是,我們看到的是合法的 ruby 程式碼,不可否認以一種非常奇怪的方式排列。但是,這種奇怪的方式讓我們擁有相當可讀的 DSL。Ruby 還透過擁有非常簡潔的語法來提供幫助 - 即使像不需要程序參數的括號這樣的小事也有助於讓這個 DSL 保持精簡。封閉也很重要 - 就像它們在撰寫內部 DSL 中經常出現的那樣,因為它們允許我們將程式碼封裝在替代控制結構中。

檔案任務

我上面提到的任務類似於 ant 中的任務。Rake 還支援一種稍微不同的任務類型,稱為檔案任務,它更接近 make 中任務的概念。以下是另一個範例,從我的網站 rakefile 中稍微簡化。

file 'build/dev/rake.html' => 'dev/rake.xml' do |t|
  require 'paper'
  maker = PaperMaker.new t.prerequisites[0], t.name
  maker.run
end

使用檔案時,你指的是實際檔案,而不是任務名稱。因此,'build/dev/rake.html' 和 'dev/rake.xml' 是實際檔案。html 檔案是此任務的輸出,而 xml 檔案是輸入。你可以將檔案任務視為告訴建置系統如何建立輸出檔案 - 事實上,這正是 make 中的概念 - 你列出你想要的輸出檔案,並告訴 make 如何建立它們。

檔案任務的一個重要部分是,除非您需要執行,否則它不會執行。建置系統會查看檔案,並且僅在輸出檔案不存在或其修改日期早於輸入檔案時才會執行任務。因此,當您逐檔案思考時,檔案任務會非常順利地運作。

此任務與眾不同的一點是,我們將任務物件本身作為參數傳遞到封閉中 - 這是 |t| 所執行的動作。我們現在可以在封閉中參照任務物件並呼叫其方法。我這樣做是為了避免重複檔案名稱。我可以使用 t.name 取得任務名稱(即輸出檔案)。類似地,我可以使用 t.prerequisites 取得先決條件清單。

Ant 沒有等同於檔案任務的功能,相反地,每個任務都會執行相同的必要性檢查。XSLT 轉換任務會取得輸入檔案、樣式檔案和輸出檔案,並且僅在輸出檔案不存在或早於任何輸入檔案時才會執行轉換。這只是一個問題,在於將此檢查的責任放在何處 - 建置系統或任務中。Ant 主要使用以 Java 編寫的罐頭任務,make 和 rake 都依賴建置撰寫者為任務撰寫程式碼。因此,讓任務撰寫者免除檢查事項是否最新的需要會更有意義。

然而,在 rake 任務中執行最新檢查實際上相當容易。以下是其外觀。

task :rakeArticle do
  src = 'dev/rake.xml'
  target = 'build/dev/rake.html'
  unless uptodate?(target, src) 
    require 'paper'
    maker = PaperMaker.new src, target 
    maker.run
  end
end

Rake 提供(透過 fileutils 套件)許多類 Unix 的簡單命令,用於檔案操作,例如 cp, mv, rm, 等。它還提供 uptodate?,這對於此類檢查來說非常完美。

因此,我們在此看到兩種執行事項的方式。我們可以使用檔案任務或搭配 uptodate? 的一般任務來決定是否執行事項 - 我們應該選擇哪一種?

我必須承認,我對這個問題沒有好的答案。這兩種策略似乎都運作得很好。我決定對我的新 rakefile 所做的,是盡可能推動細緻的檔案任務。我這樣做並不是因為我知道這是最好的做法,我主要是想看看結果會如何。當您遇到新事物時,通常過度使用它會是一個好主意,以便找出其界線。這是一個相當合理的學習策略。這也是為什麼人們在早期總是傾向於過度使用新技術或技巧。人們常常批評這一點,但這是學習的自然一部分。如果您不將某件事推到其有用性的界線之外,您要如何找出那個界線在哪裡?重要的是在相對受控的環境中執行此操作,這樣您才能在找到界線時修復事項。(畢竟,在我們嘗試之前,我認為 XML 會是建置檔案的良好語法。)

我也會說到目前為止,我還沒發現任何問題,可以將檔案工作和細緻的工作推得太遠。我可能在一兩年後會另有想法,但到目前為止,我都很滿意。

反向定義相依性

到目前為止,我大多談論 Rake 如何執行與 Ant 和 Make 中類似的任務。這是一個很好的組合,結合這兩種功能,並充分發揮 Ruby 的能力,但這本身並不足以讓我寫這篇小文章。引起我興趣的是 Rake 執行(並允許)的一些特殊事項,這些事項有點不同。第一個是允許在多個地方指定依賴關係。

在 Ant 中,您定義依賴關係的方式是將它們陳述為依賴工作的一部分。到目前為止,我也這樣處理我的 Rake 範例,如下所示。

task :second => :first do
  #second's body
end

task :first do
  #first's body
end

Rake(像 Make 一樣)允許您在最初宣告工作後,新增依賴關係。事實上,它允許您在多個地方繼續討論工作。這樣,我就可以決定在先決條件工作附近新增依賴關係,如下所示。

task :second do
  #second's body
end

task :first do
  #first's body
end
task :second => :first 

當工作在建置檔案中緊鄰彼此時,這不會產生太大差異,但在較長的建置檔案中,它確實增加了一點靈活性。基本上,它允許您以通常的方式思考依賴關係,或在新增先決條件工作時新增它們,或者將它們放在與兩者都無關的第三個位置。

一如往常,這種靈活性會產生新的問題,在哪裡定義依賴關係最好?我還沒有確定的答案,但在我的建置檔案中,我使用了兩個經驗法則。當我在想在執行另一個工作之前需要完成的工作時,我會在撰寫依賴工作時以傳統方式定義依賴關係。但是,我經常使用依賴關係將相關工作分組在一起,例如各種勘誤頁面。在將依賴關係用於分組(建置檔案結構的常見部分)時,將依賴關係放在先決條件工作中似乎很有道理。

task :main => [:errata, :articles]

#many lines of build code

file 'build/eaaErrata.html' => 'eaaErrata.xml' do
  # build logic
end
task :errata => 'build/eaaErrata.html'
    

我實際上不必使用工作關鍵字定義 :errata 工作,只要將它作為 :main 的依賴關係就足以定義工作。然後,我稍後可以新增個別勘誤檔案,並將每個檔案新增到群組工作中。對於這種群組行為,這似乎是一個合理的方法(儘管我實際上並不像這樣在我的建置檔案中執行,正如我們稍後將看到的。)

這引發了一個問題:「當依賴關係散佈在整個建置檔案中時,我們如何找出所有依賴關係?」這是一個好問題,但答案是讓 Rake 告訴您,您可以使用 rake -P 來執行此操作,它會列印出每個工作及其先決條件。

綜合任務

允許您在定義任務後新增相依性,以及讓您充分使用 Ruby,這會在建置過程中引入一些其他技巧。

不過,在我說明合成任務之前,我需要介紹一些關於建置流程的重要原則。建置指令碼往往必須執行兩種建置:乾淨建置和增量建置。當您的輸出區域為空時,就會發生乾淨建置,在這種情況下,您會從其(版本控制的)來源建置所有內容。這是建置檔案可以執行的最重要事項,而第一要務是執行正確的乾淨建置。

乾淨建置很重要,但它們確實需要時間。因此,經常執行增量建置會很有用。在這裡,您的輸出目錄中已經有東西了。增量建置需要找出如何使用最少的工作量,讓您的輸出目錄與最新來源保持最新狀態。這裡可能會發生兩個錯誤。第一個(也是最嚴重的)是遺漏重新建置,這表示應該建置的一些項目沒有建置。這很糟糕,因為會導致輸出與輸入不符(特別是輸入的乾淨建置結果)。較小的錯誤是不必要的重新建置,這會建置不需要建置的輸出元素。這是一個不太嚴重的錯誤,因為它不是正確性錯誤,但它是一個問題,因為它會增加增量建置的時間。除了時間之外,它還會增加混淆,當我執行 Rake 指令碼時,我希望只看到已變更的內容被建置,否則我會想「為什麼會變更?」

安排良好的相依性結構的重點在於確保增量建置運作良好。我只想透過輸入「rake」來執行網站的增量建置,呼叫預設任務。我希望只建置我想要的內容。

所以,這是我的需求,一個有趣的問題是如何讓它在我的 Bliki 上運作。我的 Bliki 來源是我的 Bliki 目錄中的一堆 XML 檔案。輸出是每個條目的單一輸出檔案,加上多個摘要頁面,其中 Bliki 主頁面是最重要的。我需要的是,來源檔案的任何變更都會重新觸發 Bliki 建置。

我可以透過像這樣命名所有檔案來執行此操作。

BLIKI = build('bliki/index.html')

file BLIKI => ['bliki/SoftwareDevelopmentAttitude.xml',
               'bliki/SpecificationByExample.xml',
               #etc etc
              ] do
  #logic to build the bliki
end

def build relative_path
 # allows me to avoid duplicating the build location in the build file
 return File.join('build', relative_path)
end

但顯然這會非常乏味,而且只是要求我在想要新增檔案時忘記將新檔案新增到清單中。很幸運地,我可以這樣做。

BLIKI = build('bliki/index.html')

FileList['bliki/*.xml'].each do |src|
  file BLIKI => src
end

file BLIKI do 
  #code to build the bliki
end

FileList 是 Rake 的一部分,它會根據傳入的 glob 產生檔案清單,這裡它會建立 Bliki 來源目錄中所有檔案的清單。each 方法是一個內部反覆運算器,它允許我迴圈處理它們,並將每個檔案新增為檔案任務的相依項。(each 方法是一個 集合封閉方法。)

我對 bliki 任務執行的另一件事是為它新增一個符號任務。

desc "build the bliki"
task :bliki => BLIKI

我這麼做,是因為我只要使用 rake bliki 就可以單獨建置 bliki。我不確定我是否真的需要這樣做。如果所有相依性都設定正確(就像現在這樣),我只要執行預設的 rake,就不會有不必要的重新建置。但我目前還是保留它。desc 方法可以讓你為下列任務定義簡短說明,這樣當我執行 rake -T 時,就會取得已定義說明的所有任務清單。這是查看有哪些目標可供我使用的有用方法。

如果你以前使用過 make,你可能會認為這讓人想起 make 最棒的功能之一,也就是指定模式規則以自動製作特定類型的檔案。常見的範例是,你希望透過對應的 foo.c 檔案執行 C 編譯器來建置任何 foo.o 檔案。

%.o : %.c
        gcc $< -o $@

%.c 會比對結尾為 '.c' 的每個檔案。$< 指的是來源(先決條件),而 $@ 指的是規則的目標。這個模式規則表示你不必在專案中列出每個檔案和編譯規則,模式規則會告訴 make 如何建置它需要的任何 *.o 檔案。(事實上,你甚至不需要在 make 檔案中這麼做,因為 make 內建許多像這樣的模式規則。)

Rake 其實有類似的機制。我不會討論它,只會提到它的存在,因為我還沒發現自己需要它。合成任務可以滿足我的所有需求。

區塊範圍任務

我發現使用檔案名稱和相依性時遇到的問題之一,就是你必須重複檔案名稱。請看這個範例。

file 'build/articles/mocksArentStubs.html' => 'articles/mock/mocksArentStubs.xml' do |t|
 transform t.prerequisites[0], t.name
end
task :articles => 'build/articles/mocksArentStubs.html'

在上面的範例中,'build/articles/mocksArentStubs.html' 在程式碼中被提及兩次。我可以透過使用任務物件來避免在動作區塊中重複,但我必須重複它才能設定對整體文章任務的相依性。我不喜歡這種重複,因為如果我變更檔案名稱,就會造成問題。我需要一種方法來定義它一次。我可以宣告一個常數,但這樣一來,我就是在 rakefile 中宣告一個在所有地方都可見的常數,而我只有在這個區段中使用它。我希望變數範圍盡可能小。

我可以透過使用上面提到的 FileList 類別來處理這個問題,但這次我只對一個檔案使用它。

FileList['articles/mock/mocksArentStubs.xml'].each do |src|
  target = File.join(BUILD_DIR + 'articles', 'mocksArentStubs.html')
  file target => src do
    transform src, target
  end
  task :articles => target
end

這樣一來,我定義了僅在此程式碼區塊中範圍的 src 和 target 變數。請注意,只有當我在此處定義 :articles 任務的相依性時,這才對我有幫助。如果我要在 :articles 任務的定義中定義相依性,我需要一個常數,這樣才能在整個 rakefile 中取得可見性。

當 Jim Weirich 讀到這份草稿時,他指出,如果你覺得 FileList 陳述太冗長,你可以輕鬆定義一個特別用於執行此操作的方法

  def with(value)
    yield(value)
  end

然後執行

  with('articles/mock/mocksArentStubs.xml') do |src|
    # whatever
  end

建構方法

擁有建置語言作為完整程式語言的內部 DSL 的其中一項優點是,我可以撰寫例程來處理常見情況。子例程是建構程式最基本的其中一種方式,而缺乏方便的子例程機制是 ant 和 make 的其中一個大問題,特別是當你的建置變得更複雜時。

以下是此類常見建置例程的一個範例,我使用此範例來使用 XSLT 處理器將 XML 檔案轉換成 HTML。我所有較新的寫作都使用 ruby 來執行此轉換,但我還有許多較舊的 XSLT 檔案,而且我並不急著要變更它。在撰寫各種處理 XSLT 的工作後,我很快發現有一些重複,因此我定義了一個用於此工作的例程。

def xslTask src, relativeTargetDir, taskSymbol, style
  targetDir = build(relativeTargetDir)
  target = File.join(targetDir, File.basename(src, '.xml') + '.html')
  task taskSymbol => target
  file target => [src] do |t|
    mkdir_p targetDir
    XmlTool.new.transform(t.prerequisites[0], t.name, style)
  end
end    

前兩行找出目標目錄和目標檔案。然後,我將目標檔案新增為提供的工作符號的依賴項。然後,我建立一個新的檔案工作,指示建立目標目錄(如果需要),並使用我的 XmlTool 來執行 XSLT 轉換。現在,當我想建立 XSLT 工作時,我只要呼叫此方法即可。

xslTask 'eaaErrata.xml', '.', :errata, 'eaaErrata.xsl'

此方法很好地封裝所有常見程式碼,並將變數參數化為我目前的需要。我發現將父群組工作傳遞到例程中非常有幫助,這樣例程就可以輕鬆為我建置依賴項,這是指定依賴項的彈性方式的另一個優點。我有一個類似的常見工作,用於將檔案直接從來源複製到建置目錄,我將其用於影像、PDF 等。

def copyTask srcGlob, targetDirSuffix, taskSymbol
  targetDir = File.join BUILD_DIR, targetDirSuffix
  mkdir_p targetDir
  FileList[srcGlob].each do |f|
    target = File.join targetDir, File.basename(f)
    file target => [f] do |t|
      cp f, target
    end
    task taskSymbol => target
  end
end

copyTask 稍微複雜一點,因為它允許我指定要複製的檔案 glob,這允許我複製類似這樣的檔案

copyTask 'articles/*.gif', 'articles', :articles

這會將來源中 articles 子目錄中的所有 gif 檔案複製到建置目錄的 articles 目錄中。它會為每個檔案建立一個獨立的檔案工作,並將它們全部設為 :articles 工作的依賴項。

平台相依的 XML 處理

當我使用 ant 來建置我的網站時,我使用基於 java 的 XSLT 處理器。當我開始使用 rake 時,我決定切換到原生 XSLT 處理器。我使用 Windows 和 Unix(Debian 和 MacOS)系統,這兩個系統都很容易取得 XSLT 處理器。當然,它們是不同的處理器,我需要以不同的方式呼叫它們,但當然我希望這對 rakefile 隱藏起來,而且在我呼叫 rake 時對我而言也是如此。

再次說明,擁有完整的語言可直接使用是件很棒的事。我可以輕鬆撰寫一個 Xml 處理器,它使用平台資訊來執行正確的操作。

我從工具的介面部分開始,也就是 XmlTool 類別。

class XmlTool
  def self.new
    return XmlToolWindows.new if windows?
    return XmlToolUnix.new
  end
  def self.windows?
    return RUBY_PLATFORM =~ /win32/i 
  end
end

在 ruby 中,您可以透過呼叫類別上的 new 方法來建立物件。與專制的建構函式相比,這樣做的好處是您可以覆寫這個 new 方法,甚至可以回傳不同類別的物件。因此,當我在此呼叫 XmlTool.new 時,我不會取得 XmlTool 的執行個體,而是會取得適合我在其上執行指令碼的任何平台的正確工具類型。

這兩個工具中較為簡單的是 Unix 版本。

class XmlToolUnix
  def transform infile, outfile, stylefile
    cmd = "xsltproc #{stylefile} #{infile} > #{outfile}"
    puts 'xsl: ' + infile
    system cmd
  end
  def validate filename
    result = `xmllint -noout -valid #{filename}`
    puts result unless  '' == result
  end
end

您會注意到,我這裡有兩個用於 XML 的方法,一個用於 XSLT 轉換,另一個用於 XML 驗證。對於 unix,每個方法都會呼叫命令列。如果您不熟悉 ruby,請注意使用 #{variable_name} 建構將變數插入字串的優點。事實上,您可以插入任何 ruby 表達式的結果,這非常方便。在驗證方法中,我使用反引號,它會執行命令列並回傳結果。puts 指令是 ruby 將資料列印至標準輸出的方式。

Windows 版本稍為複雜,因為它需要使用 COM,而不是命令列。

class XmlToolWindows
  def initialize
    require 'win32ole'
  end
  def transform infile, outfile, stylefile
    #got idea from http://blog.crispen.org/archives/2003/10/24/lessons-in-xslt/
    input = make_dom infile
    style = make_dom stylefile
    result = input.transformNode style
    raise "empty html output for #{infile}" if result.empty?
    File.open(outfile, 'w') {|out| out << result}
  end
  def make_dom filename, validate = false
    result = WIN32OLE.new 'Microsoft.XMLDOM'
    result.async = false
    result.validateOnParse = validate
    result.load filename
    return result
  end
  def validate filename
    dom = make_dom filename, true
    error = dom.parseError
    unless error.errorCode == 0
      puts "INVALID: code #{error.errorCode} for  #{filename} " + 
        "(line #{error.line})\n#{error.reason}"
    end
  end
end

陳述式 require 'win32ole' 會拉入 ruby 函式庫程式碼,以使用 Windows COM。請注意,這是程式的一般部分;在 ruby 中,您可以設定程式,以便僅在需要且存在時才載入函式庫。然後,我可以像使用任何其他指令碼語言一樣操作 COM 物件。

您會注意到,這三個 XML 處理類別之間沒有類型關聯。XML 操作之所以可行,是因為 Windows 和 Unix XmlTools 都實作了 transform 和 validate 方法。這就是 rubyist 所稱的 鴨子型別,如果它像鴨子一樣走路,並且像鴨子一樣嘎嘎叫,那麼它一定是鴨子。沒有編譯時間檢查來確保這些方法存在。如果方法不正確,它將在執行時間失敗,這應該透過測試來清除。我不會費心探討整個動態與靜態類型檢查的爭論,僅指出這是使用鴨子型別的一個範例。

如果您使用的是 unix 系統,您可能需要使用您擁有的任何套件管理系統來尋找並下載我正在使用的 unix xml 指令(我在 Mac 上使用 Fink)。XMLDOM DLL 通常會隨 Windows 附帶,但同樣地,根據您的設定,您可能需要下載它。

出錯

您可以保證程式設計的一件事是,事情總會出錯。無論您如何嘗試,您所說的內容與電腦所聽到的內容之間總是有某些不匹配。看看這段 rake 程式碼(簡化自實際發生在我身上的某件事)。

src = 'foo.xml'
target = build('foo.html')
task :default => target
copyTask 'foo.css', '.', target
file target => src do
  transform src, target
end

看到 bug 了嗎?我也沒有。我知道的是,即使不需要,建立 build/foo.html 的轉換總是會發生,這是不必要的重新建立。我無法理解原因。即使我非常確定目標晚於來源,兩個檔案的時間戳記都是正確的,但我仍然會重新建立。

我的第一條調查線索是使用 rake 的追蹤功能 (rake --trace)。通常這是我辨識奇怪呼叫所需要的,但這次完全沒有幫助。它只告訴我「build/foo.html」任務正在執行,但沒有說明原因。

在這個時候,有人可能會傾向於責怪 Jim 沒有提供偵錯工具。也許咒罵至少能讓我感覺好一點:「你的母親是克里夫蘭的母狼,你的父親是濕透的胡蘿蔔。」

但我有更好的替代方案。Rake 是 ruby,而任務只是物件。我可以取得這些物件的參考並詢問它們。Jim 可能沒有將這個偵錯程式碼放入 rake 中,但我可以輕鬆地自行加入。

class Task 
  def investigation
    result = "------------------------------\n"
    result << "Investigating #{name}\n" 
    result << "class: #{self.class}\n"
    result <<  "task needed: #{needed?}\n"
    result <<  "timestamp: #{timestamp}\n"
    result << "pre-requisites: \n"
    prereqs = @prerequisites.collect {|name| Task[name]}
    prereqs.sort! {|a,b| a.timestamp <=> b.timestamp}
    prereqs.each do |p|
      result << "--#{p.name} (#{p.timestamp})\n"
    end
    latest_prereq = @prerequisites.collect{|n| Task[n].timestamp}.max
    result <<  "latest-prerequisite time: #{latest_prereq}\n"
    result << "................................\n\n"
    return result
  end
end

以下是查看所有這些內容應該是什麼的程式碼。如果您不是 rubyist,您可能會覺得奇怪,因為我實際上已將一個方法加入到 rake 中的任務類別。這種事情,與面向向量的介紹相同,在 ruby 中是合法的。就像許多 ruby 事物一樣,您可以想像這個功能會造成混亂,但只要您小心,它真的很好。

現在我可以呼叫它來查看更多正在發生的事情

src = 'foo.xml'
target = build('foo.html')
task :default => target
copyTask 'foo.css', '.', target
file target => src do |t|
  puts t.investigation
  transform src, target
end

我得到以下列印

------------------------------
Investigating build/foo.html
class: Task
task needed: true
timestamp: Sat Jul 30 16:23:33 EDT 2005
pre-requisites:
--foo.xml (Sat Jul 30 15:35:59 EDT 2005)
--build/./foo.css (Sat Jul 30 16:23:33 EDT 2005)
latest-prerequisite time: Sat Jul 30 16:23:33 EDT 2005
................................

起初我對時間戳記感到疑惑。輸出檔案的時間戳記是 16:42,所以任務為什麼顯示 16:23?然後我意識到任務的類別是 Task,而不是 FileTask。任務不會執行日期檢查,如果您呼叫它們,它們將永遠執行。所以我嘗試了這個。

src = 'foo.xml'
target = build('foo.html')
file target
task :default => target
copyTask 'foo.css', '.', target
file target => src do |t|
  puts t.investigation 
  transform src, target
end

變更在於,我在稍後於其他任務的內容中提到它之前,將任務宣告為檔案任務。這奏效了。

從中得到的教訓是,使用這種內部 DSL,您可以查詢物件結構,找出發生了什麼事。當發生這種奇怪的事情時,這會非常方便。我在另一個有不需要的建置的情況中使用了這種方法,打開引擎蓋並確切地查看發生了什麼事,這真的很有用。

(順便一提,如果輸出檔案還不存在,例如在乾淨建置中,我的 investigation 方法會中斷。我沒有花任何力氣來修復它,因為我只有在檔案已經存在時才需要它。)

由於我寫了這個 Jim,所以加入了一個調查方法,非常接近這個方法,來耙自己。因此,您不再需要執行我在這裡執行的操作。但一般原則仍然成立,如果耙子沒有做您想要的事情,您可以進入並修改其行為。

使用 Rake 建構非 Ruby 應用程式

儘管耙子是用 ruby 編寫的,但沒有理由不能使用它來建置用其他語言編寫的應用程式。任何建置語言都是用於建置東西的腳本語言,您可以使用用另一種語言編寫的工具來建置一個環境。(一個很好的例子是當我們使用 ant 來建置 Microsoft COM 專案時,我們只需要將它隱藏起來,不讓 Microsoft 顧問看到。)使用 rake 的唯一方法是,瞭解 ruby 有助於執行更進階的事情,但我一直認為任何專業程式設計師都需要知道至少一種腳本語言才能完成各種奇奇怪怪的工作。

執行測試

Rake 的函式庫允許您使用 TestTask 類別直接在 rake 系統中執行測試

require 'rake/testtask'
Rake::TestTask.new do |t|
  t.libs << "lib/test"
  t.test_files = FileList['lib/test/*Tester.rb']
  t.verbose = false
  t.warning = true
end

預設情況下,這將建立一個 :test 任務,它將執行給定檔案中的測試。您可以使用多個任務物件為不同的情況建立測試套件。

預設情況下,測試任務將執行所有給定檔案中的所有測試。如果您只想執行單一檔案中的測試,您可以使用

        rake test TEST=path/to/tester.rb
      

如果您想執行名為「test_something」的單一測試,您需要使用 TESTOPTS 將選項傳遞給測試執行器。

         rake test TEST=path/to/tester.rb TESTOPTS=--name=test_something
      

我經常發現建立暫時性 rake 任務來執行特定測試很有幫助。若要執行一個檔案,我可以使用

Rake::TestTask.new do |t|
  t.test_files = FileList['./testTag.rb']
  t.verbose = true
  t.warning = true
  t.name = 'one'
end

若要執行一個測試方法,我加入測試選項

Rake::TestTask.new do |t|
  t.test_files = FileList['./testTag.rb']
  t.verbose = true
  t.warning = true
  t.name = 'one'
  t.options = "--name=test_should_rebuild_if_not_up_to_date"
end

檔案路徑處理

Rake 延伸字串類別來執行一些有用的檔案處理表達式。例如,如果您想透過取得來源並變更檔案副檔名來指定目標檔案,您可以這樣做

"/projects/worldDominationSecrets.xml".ext("html")
# => '/projects/worldDominationSecrets.html'

對於更複雜的處理,有一個路徑對應方法,它使用與 printf 類似的樣板標記。例如,樣板「%x」是指路徑的檔案副檔名,而「%X」是指檔案副檔名以外的所有內容,因此我可以像這樣撰寫上述範例。

"/projects/worldDominationSecrets.xml".pathmap("%X.html")
# => '/projects/worldDominationSecrets.html'

另一個常見情況是讓「src」中的東西出現在「bin」中。為此,我們可以使用「%{pattern,replacement}X」對樣板中的元素進行替換,例如

"src/org/onestepback/proj/foo.java".pathmap("%{^src,bin}X.class")
# => "bin/org/onestepback/proj/foo.class"

您可以在 Rake 的 String.pathmap 文件 中找到路徑處理方法的完整清單。

我發現這些方法非常有用,因此我喜歡在自己的程式碼中進行檔案路徑處理時使用它們。若要讓它們可用,您需要

require 'rake/ext/string'

命名空間

當您建立較大的建置指令碼時,很容易就會產生許多具有相似名稱的任務。Rake 有命名空間的概念,可協助您整理這些任務。您可以使用下列方式建立命名空間:

    namespace :articles do
      # put tasks inside the namespace here eg
      task :foo
    end
  

然後,您可以使用 `rake articles:foo` 呼叫命名空間任務

如果您需要參照目前命名空間以外的任務,請使用任務的完整限定名稱,通常使用任務名稱的字串形式會比較容易。

    namespace :other do
       task :special => 'articles:foo'
    end
  

內建清理

建置時常需要清除您已產生的檔案。Rake 提供內建方式來執行這項工作。Rake 有兩個層級的清除:clean 和 clobber。Clean 是最溫和的方法,它會移除所有中間檔案,但不會移除最終產品,只會移除用於衍生最終產品的暫時檔案。Clobber 會使用較強的肥皂,並移除所有產生的檔案,包括最終產品。基本上,clobber 會將您還原到只保留已提交至原始碼控制的檔案。

這裡有一些術語上的混淆。我常聽到有人使用「clean」來表示移除所有產生的檔案,這等同於 rake 的 clobber。因此,請注意這種混淆。

若要使用內建的清除功能,您需要使用 `require 'rake/clean'` 來匯入 rake 的內建清除功能。這會引入兩個任務:clean 和 clobber。不過,它們目前並不知道要清除哪些檔案。若要告訴它,您可以使用一對檔案清單:CLEAN 和 CLOBBER。然後,您可以使用 `CLEAN.include('*.o')` 等表達式將項目新增至檔案清單。請記住,clean 任務會移除 clean 清單中的所有內容,而 clobber 會移除 clean 和 clobber 清單中的所有內容。

其他

預設情況下,如果您在 rake 呼叫的程式碼中發生錯誤,rake 不會列印堆疊追蹤。您可以使用 `--trace` 旗標執行來取得堆疊追蹤,但我通常還是比較希望直接看到它。您可以將 `Rake.application.options.trace = true` 放入 rakefile 中來執行此動作。

同樣地,我發現 FileUtils 的檔案處理輸出會造成干擾。你可以使用 -q 選項從命令列關閉它們,也可以在你的 rakefile 中使用 verbose(false) 呼叫來停用它們。

在執行 rake 時開啟警告訊息通常很有用,你可以透過操作 $VERBOSE 來做到這一點,Mislav Marohnić 有些關於使用 $VERBOSE不錯的筆記

若要在 rakefile 本身中查詢 rake 任務物件,請使用 Rake::Task[:aTask]。任務名稱可以使用符號或字串指定。這允許你使用 Rake::Task[:aTask].invoke 呼叫一個任務,而不需要使用相依性。你通常不需要這麼做,但偶爾會派上用場。

結論

到目前為止,我發現 rake 是一個功能強大且易於使用的建置語言。當然,我熟悉 ruby 也有幫助,但 rake 讓我相信建置系統可以作為內部 DSL,成為一種成熟的語言。腳本在許多方面都是建置東西的自然選擇,而 rake 增加了剛剛好夠用的功能,在一個精良的語言之上提供了一個真正好的建置系統。我們還有一個優勢,那就是 ruby 是一個開放原始碼語言,可以在我需要的所有平台上執行。

靈活相依性規範的結果讓我感到驚訝。它允許我執行許多可以減少重複的動作,這將使我未來更容易維護我的建置腳本。我發現了幾個常見函式,我將它們提取到一個獨立的檔案中,並在 martinfowler.com 和 refactoring.com 的建置腳本之間共用。

如果你正在自動化建置,你應該看看 rake。請記住,你可以將它用於任何環境,而不仅仅是 ruby。


進一步閱讀

你可以從 rubyforge 取得 rake。 rakefile 的說明 是最好的文件來源之一。Jim Weirich 的部落格也包含一些好的 條目 來幫助說明 rake,儘管你需要按相反順序閱讀它們(最早發布的排在最前面)。這些條目更詳細地說明了我在此處略過的內容(例如規則)。

使用腳本,你可以設定 rake 在 bash 中的命令列完成

Joe White 有關於 Rake 函式庫 功能的精采頁面。

如果你喜歡建置工具的內部 DSL 這個概念,但偏好 Python,你可能想看看 SCons

致謝

感謝 Jason Yip、Juilian Simpson、Jon Tirsen 和 Jim Weirich 對本文草稿的評論。感謝 Dave Smith、Ori Peleg 和 Dave Stanton 在本文發布後提供的一些更正。

但最大的感謝必須獻給 Jim Weirich,他最初撰寫了 rake。我的網站感謝你。

重大修訂

2014 年 12 月 29 日:更新執行測試的討論

2005 年 8 月 10 日:首次發布