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”本来就很完美,但可惜的是,已经有一个名为“lake”的构建系统了!“bou” 是南非语中的“build”;你将你的目标和规则放在一个“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 上),也知道如何进行调试构建。
> 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 足够智能,知道构建已经改变。这是使用 *specfile* 完成的。
> cat boufile.spec link /nologo $(DEPENDS) /OUT:$(TARGET) cl /nologo -c /Zi /DDEBUG $(INPUT)
通过将现有的 specfile 与生成的命令进行比较,Bou 可以推断出命令已更改,因此需要重新构建。
对于如此简单的案例,你可以完全不用 boufile,让 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 可以显式地处理这种格式。考虑以下 boufile
-- 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 测试程序的目录的 boufile 可能如下所示
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 函数,也可以是字符串,在这种情况下,它被解释为使用 shell 运行的命令。
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
应该可以工作;如果没有提供 bou 文件,它将在当前目录中查找 boufile
。然后创建一个批处理文件
@lua <path-to-bou.lua> %*
或脚本文件,取决于你的信仰
#!/bin/bash lua <path-to-bou.lua> "$@"
("$@"
确保带引号的参数能够正确传递。)