Lua 建構系統 Bou

lua-users home
wiki

Bou 是使用 Lua 作為 DSL 的基於 Lua 的建構系統。

Bou 已發展成熟並改名為lake;請參閱 [1]

Bou:基於 Lua 的建構系統

我第一次對作為嵌入式 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.ctwo.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 函式傳遞輸入延伸模組、輸出延伸模組及將輸入轉換為輸出的檔案命令,標準變數 INPUTTARGET 會為您設定。並非設定全球規則,它傳回一個規則組,您可以新增目標名稱。在此範例中,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'

Boufiles 作為程式

在 XML 風潮的高潮,自然會像這樣編寫建置規則:

<program name="first" compile_deps="common.h" libs="user32,kerner32">
one, two
</program>

這也是一個與工具無關的表示法,但並不是一個非常好的程式設計表示法。藉由讓建置語言成為一個真實程式語言的子集合,執行非標準的額外操作不需要「跳離 DSL」。

Bou 在處理建置環境預期執行的所有任務前還有很長一段路要走,包括支援安裝。但希望它是一個好的起始基礎,我也希望它展示 Lua 非常適合這類型的「小語言」應用程式。

取得並安裝 Bou

[4] 包含 bou.lua 和一些小型範例專案。將其解壓到某個位置,並確保套件 cpath 中有 lfslua bou.lua -f hello.bou 現在應該會運作;如果沒有提供 boufile,它將在目前目錄中尋找 boufile。然後建立一個批次檔

@lua <path-to-bou.lua> %*

或腳本檔,視您的宗教信仰而定

#!/bin/bash
lua <path-to-bou.lua>  "$@"

("$@" 確保會正確傳遞加引號的參數。)

作者

SteveDonovan


最近變更 · 偏好設定
編輯 · 歷史記錄
上次編輯時間為 2010 年 10 月 14 日下午 2:21 GMT (diff)