用Rakefile管理工程

遊戲項目可能是所有軟件項目中需要在編譯時處理資源最多的項目, 一般的項目都有下面幾種常見需求:

  1. 將文本格式的Json, XML等配置換成二進制
  2. 將Json, XML等配置加密
  3. 將tga, png的圖壓縮成壓 縮比更高的pvr, webp等格式
  4. 用texturepacker等工具打包小圖
  5. 將UI編輯器, 動畫編輯器的編輯時格式(往往是文本格式)編譯成二進制的發佈格式.

特別是圖片相關的的資源生成, 時間消耗較多, 需要儘量減少重複生成. 此時像makefile這種東西就很有價值了.

目錄:

Makefile的利弊

Makefile最大的好處自然是依賴關係的作用, 在正確設置後, 能做到當原始文件(源文件, 原始的資源等)沒有更改時, 不生成目標文件, 更改時才生成, 並且可以自定義生成的規則.

缺點也很明顯, Makefile太難寫了, 傳統的Makefile格式獨特, 甚至tab敏感, 而功能相對單一(功能強大基本靠shell). 所以很多人都弄了一套別的東西, 比如傳統的Unix/Linux開發環境的Automake和Autoconf, 可以跨平臺生成工程的CMake, Qt的qmake, Java的ant等, 而Ruby則提供了Rake.

Rakefile使用

簡單的說Rakefile就是使用Ruby語法的makefile, 對應make的工具就是rake. 在Ruby on Rails裏面, 不管是數據庫的初始化, 內容初始化, 刪除, 還是測試, 都是用rake來完成的.

優點

官方說明有如下優點:

  1. Ruby語法
  2. 可以設定task的依賴
  3. 支持patterns的規則
  4. 靈活的FileList類, 行爲像array, 但是可以方便的操作文件名和路徑
  5. 有一個預先包裝好的庫, 可以方便的實現類似build tarball和發佈到ssh網站等功能.
  6. 支持並行task.

其實想像一下, 在makefile文件中能使用完整的ruby功能, 不僅僅是ruby的語法, 還支持ruby現有的所有庫, gems, 光聽聽就讓人高興.

碰到複雜工程時, 不管邏輯需要多複雜, 你都有一個完整, 強大的語言可以使用, 不再需要藉助其他的東西就能夠完全hold住.

假如有缺點的話, 那就是ruby畢竟還是需要學習的....並且, 總體的內容比一般的makefile要複雜一些.

使用說明

Rakefile分幾個基本的build規則, 用"=>"來表示依賴關係.

比如常見的helloworld工程, 我們可以輸入完整的命令:

g++ helloworld.cc -o hello.o

也可以在源代碼目錄中新建Rakefile文件來管理, Rakefile文件如下:

file "helloworld" => "helloworld.cc" do |t|
    sh "g++ #{t.prerequisites.join(' ')} -o #{t.name}"
end

然後運行rake helloworld, 來編譯, 好處就是當helloworld.cc文件沒有改變時, 實際根本不會編譯.

上面的例子中我們是用了一個file task, 當我們要想要直接運行rake, 省略helloworld的話, 可以利用rake的default task.

task :default => "helloworld"

file "helloworld" => "helloworld.cc" do |t|
    sh "g++ #{t.prerequisites.join(' ')} -o #{t.name}"
end

這個default的task就是一個simple task, 會在直接運行rake的時候運行, 並且, 可以看到, task之間也是可以用"=>"表示依賴的.

當文件比較多時, 一個一個的寫file task可能會比較累, 於是rake加入了rule特性, 比如, 我們可以用下列的rule來編譯所有的".cc"文件.

比如, 我自建一個my_print函數, 現在就有my_print.cc, helloworld.cc兩個源文件了, 可以通過下面這種方式來生成代碼:

task :default => "helloworld"

file "helloworld" => ["helloworld.o", "my_print.o"]  do |t|
    sh "g++ #{t.prerequisites.join(' ')} -o #{t.name}"
end

rule ".o" => [".cc", ".h"] do |t|
    sh "g++ -c #{t.source} -o #{t.name}"
end

當然, 雖然rake很強大, 但是還是沒有強大到能夠分析理解C++代碼的地步, 所以, 這種規則和以前的makefile文件一樣, 設定後, 僅僅是同名文件的頭文件, 源文件能夠產生依賴關係(更改後能夠觸發重編譯), 但是此例中, helloworld.cc也include了my_print.h, 也是對my_print.h的實際依賴, 但是rake就理解不了了.

而事實上, 我們幾乎不可能都手動的將所有的這種include關係輸入到rakefile中, 那簡直就是自虐. 我們通常的做法是, 碰到有改頭文件的時候, 直接clean項目, 然後再重新編譯.

task :clean do
    sh "rm *.o"
end

同樣的, 我們也能實現makefile中常有的install任務, 這裏就不再累述了.

實例

這裏用一個遊戲項目的實例來說明:

首先, 我們一般通過base_dir = File.dirname(__FILE__)的方式來獲得當前目錄, 以方便解決目錄相關的問題, 手動的從相對目錄轉爲絕對目錄.

然後, 爲了從png格式壓縮爲webp格式, 建立以下規則:

quality = 90 
rule '.webp' => '.png' do |t|
    puts "webp convert begin:" + t.source.to_s

    if !File.exist?(converted_dir)
        sh "mkdir #{converted_dir}"
    end

    sh "/usr/bin/env cwebp -q #{quality} -quiet #{t.source} -o #{t.name}"
    sh "cp #{t.name} " + converted_dir + "/"

    puts "webp convert end:" + t.source.to_s
end

其中converted_dir就是我們實際資源需要移動到的目錄. 這裏之所以用cp, 而不是用mv來移動, 是爲了在源目錄保留有轉換後的副本, 當圖片沒有更改的時候, 就不需要重新壓縮圖片. 這裏, 有個疑問, 最佳的方式是直接將converted_dir的資源和源文件形成依賴, 就可以省掉一次拷貝的過程, 但是, 不知道怎樣使用跨目錄的rule.

再比如說, 使用TexturePacker對小圖片進行打包, 這個依賴關係本來是一個大圖片對需要打包的所有小圖片, 特別適合rakefile/makefile, 不過TexturePacker自己就實現了這種機制, 我們也就沒有必要重複實現了, 即使其實比較容易.

desc "pack texture with texture packer."
task :pack_texture do
    puts "pack texture begin."
    tps_files = FileList["#{tps_dir}" + "/*.tps"]
    puts "tps files:" + tps_files.to_s
    tps_files.each { |file|
        sh "/usr/local/bin/TexturePacker --quiet #{file}"
    }
end

這裏的desc是Rakefile專用的註釋, 可以在運行rake -T時, 看到較爲友好的命令說明:

$rake -T
rake clean         # clean the all generated resource
rake clean_packed  # clean the packed resource.
rake default       # generate all the resouce neeed.
rake pack_texture  # pack texture with texture packer.
rake png2webp      # convert all the png to webp format.

這裏又有另外一個較爲不好的地方, 我們首先用TexturePacker把小圖都打包成大圖了(見前面pack_texture task的例子), 我們可以完全用FileList動態生成需要打包的tps文件, 而只有打包後纔能有我們想要轉換爲webp的png圖文件, 但是, 當我想要動態的用FileList獲取到生成的所有的png作爲file task的任務時, 發現rakefile並不支持. 簡單的說, 當file task依賴的文件是另一個task的結果時, 我們無法處理這種依賴關係, 如下例:

generated_texs = nil
task :pack_texture do
    // generate the textures
    // the code

    generated_texs = FileList[...]
end

task :png2webp => [:pack_texture] + generated_texs do

end

這個例子中, 雖然我們可以肯定的說png2webp task運行時genereated_texs會獲得正確的值, 無論我們是通過default task運行, 還是直接運行png2webp這個task(因爲png2webp本身依賴pack_texutre task), 但是實際上, 無論你用那種方式運行png2webp, genereated_texs總是爲nil, 就算你實際上在pack_texture task中改變了generated_texs的值. 這個挺讓人鬱悶的.

總結

總的來說, Rakefile算是那種一勞永逸的工程管理解決方案, 因爲ruby語言本身的強大和相關庫的豐富, 基本上不會再需要用其他方式來管理你的工程了. 也許, 還要更好的話, 那就是自動的理解代碼, 瞭解諸如include, import等依賴關係的工具了.

參考

Rakefile Readme
Rakefile Format

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章