Lua 建構系統 Bou |
|
Bou 已發展成熟並改名為lake
;請參閱 [1]。
我第一次對作為嵌入式 DSL(特定領域語言)實作的建構工具感興趣,是透過 Martin Fowler 關於 Rake 的文章 [2]。像 make 或 nant 這樣的建構語言是經典的 DSL,儘管術語「微型語言」[3]已經存在很長一段時間,實際上是 Unix 開發哲學的一個重要部分,其中包含能完善完成一項工作的專業工具。
Rake 是嵌入式 DSL,因為它出現在一個大型語言中。這對開發人員和使用者都有很大的好處;開發人員不必重新發明高級程式語言,而使用者則擁有經過測試的程式語言帶來的便利性和功能。在不擴充語言的情況下,可以輕易地完成不尋常的工作。考慮make
;其功能來自於有數以千計的 Unix 指令在四處運行 - 在非 POSIX 環境中,其功能遠低於此。而且通常會得到 make 和 shell 指令的奇特組合,(姑且稱作)不直觀且難於維護。nant
肯定較為簡潔,而且可以在 CLI 編寫自訂任務,但我們仍然必須「退出 DSL」(正如 Fowler 所言)才能進行一般事務。XML 非常適合於指定階層資料,但作為程式設計的語法並不太理想。
我始終認為 Lua 是適合嵌入式 DSL 應用程式的良好語言,這導致了 Bou。Bou(發音為「beau」)是一個基於純 Lua 的建構系統 - 唯一的外部相依性在於 LuaFileSystem (lfs)。它在哲學上故意類似於make
,並使用相同的目標、規則和相依性語言。然而,它為這場盛會帶來了兩項強大的功能;使用任意 Lua 程式碼的能力,以及大量關於一些常見編譯工具的罐頭知識。
英文作為優良開源專案名稱的資源迅速耗盡 -「lake」本來會很完美,但遺憾的是早已有一個建構系統以此為名!「bou」在南非語中是「建構」的意思;你將目標和規則放在「boufile」中。
考慮一個老朋友
#include <stdio.h> int main(int argc, char**argv) { printf("Hello, World - %d parms passed\n",argc); return 0; }
為慣例的「Hello,World!」程式撰寫一個 makefile 有點小題大作,但等效的 boufile 非常簡單明瞭
c.program 'hello'
或者,你可以讓 Bou 推斷你有一個 C 程式,並這麼說
program 'hello.c'
執行 Bou 會產生下列輸出
> bou gcc -c -O1 -DNDEBUG hello.c gcc hello.o -o hello.exe > bou bou: up to date
本身並不怎麼令人印象深刻。但這個簡單的 boufile 為你免費提供功能:它已經知道「clean」,知道如何使用 Microsoft 命令列編譯器(至少在 Windows 中),並且知道如何建立一個 debug 建構
> bou clean removing 1 hello.exe 2 hello.o > bou CC=cl cl /nologo -c /O1 /DNDEBUG hello.c hello.c link /nologo hello.obj /OUT:hello.exe > bou CC=cl DEBUG=1 cl /nologo -c /Zi /DDEBUG hello.c hello.c link /nologo hello.obj /OUT:hello.exe
請注意,在執行偵錯建置之前,並不需要呼叫「bou clean」,因為 Bou 足夠聰明,知道建置已變更。這是使用規格檔案執行
> cat boufile.spec link /nologo $(DEPENDS) /OUT:$(TARGET) cl /nologo -c /Zi /DDEBUG $(INPUT)
透過將現有的規格檔案與產生的指令進行比較,Bou 可以推論出指令已變更,因此需要重新建置。
對於如此簡單的情況,您可以完全省略 bou 檔案,並讓 Bou 推論編譯和執行所指定檔案所需的工具
> del hello.exe > bou hello.c 10 20 30 gcc hello.o -o hello.exe hello.exe 10 20 30 Hello, World - 4 parms passed
請注意,沒有重新產生 hello.o
!
思考一下一個具有兩個檔案的程式,為 one.c
和 two.c
,且稱為 first
。
c.program {'first',src='one,two'}
執行 Bou,會得到
> bou gcc -c -O1 -DNDEBUG one.c gcc -c -O1 -DNDEBUG two.c gcc one.o two.o -o first.exe
此情況不太切實際 — 在實際應用中,來源檔案至少會依賴於一些標頭檔,而且您需要指定程式庫。為了指定更多選用參數,我們針對傳遞「命名」參數使用常見的表格慣用法 — 請注意大括弧
c.program{'first',src='one,two', compile_deps='common.h',libs='user32,kernel32'}
我們現在將正確地連結至必要的程式庫,而如果 common.h
變更,則來源檔將重新編譯
> bou gcc -c -O1 -DNDEBUG one.c gcc -c -O1 -DNDEBUG two.c gcc one.o two.o -luser32 -lkernel32 -o first.exe > bou CC=cl cl /nologo -c /O1 /DNDEBUG one.c one.c cl /nologo -c /O1 /DNDEBUG two.c two.c link /nologo one.obj two.obj user32.lib kernel32.lib /OUT:first.exe
libs
會給予 Bou 程式庫清單;然後,它會決定如何以適合特定工具的方式格式化此清單。其他參數包括 incdefs
(用於設定包含路徑清單)和 defines
(用於定義巨集)。
可以說 src='*.c'
,但這不會處理每個來源檔案都有個別相依項式的這個事實。
表達相依項式的一個非常常見格式是 GCC 等工具發出的「deps」格式,適合納入 makefile 中。Bou 可以明確地處理此類格式。思考以下 bou 檔案
-- bonzo.bou cpp.defaults = {defines = 'SIMPLE',libs = 'user32'} cpp.program {'bonzo',rules=[[ cppfile.o: cppfile.cpp cpp/inc.h c/common.h cfile.o: cfile.c c/inc.h c/common.h clib.o: c/clib.c c/inc.h ]]}
rules
參數可以設定為檔名,但如果字串包含換行,則假設為原樣。Bou 將根據這個規格執行三件事
* 根據已知的隱含規則產生目標 * 萃取包含路徑 * 建構每個目標的相依項式清單
> bou -f bonzo.bou g++ -c -O1 -DNDEBUG -DSIMPLE -Icpp -Ic cppfile.cpp gcc -c -O1 -DNDEBUG -Ic cfile.c gcc -c -O1 -DNDEBUG -Ic c/clib.c g++ cppfile.o cfile.o clib.o -luser32 -o bonzo.exe
請注意,如何使用 cpp.defaults
設定全域程式庫和定義設定。
思考需要建置與執行多個測試程式的常見任務。這些可能需要編譯(例如 C/C++),或可以被直接解釋。一個包含 C 與 Lua 測試程式的目錄的 bou 檔案看起來如下
target('all','c,lua') target('c',forall_results('*.c',go)) target('lua',forall_results('*.lua',go))
第一個目標「all」明確地依賴於目標「c」與「lua」;「c」的相依項式是產生此目錄中所有 C 檔案的程式建置與執行目標的結果,由 go()
分別處理。forall_results()
非常像 map
,不同的是,它可以採用萬用字元替代明確清單,並且可以從函式的每個呼叫中收集多個結果。
rule
函式傳遞輸入延伸模組、輸出延伸模組及將輸入轉換為輸出的檔案命令,標準變數 INPUT
及 TARGET
會為您設定。並非設定全球規則,它傳回一個規則組,您可以新增目標名稱。在此範例中,progs 'one'
為 progs:add_target 'one'
的簡寫。rule:add_target
也有第二個引數,可用來傳遞明確的相依關係。
progs = rule('.c','.o','gcc -c $(INPUT)') progs 'one' progs 'two'
target
函式需要三個引數,名稱、任何相依關係及即將執行的命令。相依關係可以是清單(字串會自動轉換)或一組相依關係,使用 depends
。如果相依關係引數為 nil,則目標無條件。命令可以是 Lua 函式或字串,如果是字串,會解釋成使用殼層運行的命令。
depends
函式對於遞延計算相依關係很有用。以下是相依於兩個目標組結果的目標,而目標組只有在稍後才會填滿
progs = rule('.c','.o','gcc -c $(INPUT)') files = rule('.gif','.jpg','convert $(INPUT) $(TARGET)') target('all',depends(progs,files),function() print 'yes' end) progs 'one' progs 'two' files 'pool'
在 XML 風潮的高潮,自然會像這樣編寫建置規則:
<program name="first" compile_deps="common.h" libs="user32,kerner32"> one, two </program>
這也是一個與工具無關的表示法,但並不是一個非常好的程式設計表示法。藉由讓建置語言成為一個真實程式語言的子集合,執行非標準的額外操作不需要「跳離 DSL」。
Bou 在處理建置環境預期執行的所有任務前還有很長一段路要走,包括支援安裝。但希望它是一個好的起始基礎,我也希望它展示 Lua 非常適合這類型的「小語言」應用程式。
[4] 包含 bou.lua
和一些小型範例專案。將其解壓到某個位置,並確保套件 cpath 中有 lfs
。lua bou.lua -f hello.bou
現在應該會運作;如果沒有提供 boufile,它將在目前目錄中尋找 boufile
。然後建立一個批次檔
@lua <path-to-bou.lua> %*
或腳本檔,視您的宗教信仰而定
#!/bin/bash lua <path-to-bou.lua> "$@"
("$@"
確保會正確傳遞加引號的參數。)