Lua 构建系统 Bou

lua-users home
wiki

Bou 是一个基于 Lua 的构建系统,使用 Lua 作为 DSL。

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”本来就很完美,但可惜的是,已经有一个名为“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.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 可以显式地处理这种格式。考虑以下 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 函数接受输入扩展名、输出扩展名和将输入转换为输出文件的命令;标准变量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 函数,也可以是字符串,在这种情况下,它被解释为使用 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'

Boufiles 作为程序

在 XML 狂潮的顶峰,用类似这样的方式对构建规则进行编码是自然而然的

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

这也是一种与工具无关的表示法,但它不是一个很好的编程表示法。通过使构建语言成为真实编程语言的子集,执行非标准的额外操作不需要“跳出 DSL”。

Bou 还有很长的路要走,才能处理构建环境所期望的所有任务,包括对安装的支持。但希望它是一个良好的起点,并且我希望它表明 Lua 非常适合这种“小语言”应用程序。

获取和安装 Bou

[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>  "$@"

("$@" 确保带引号的参数能够正确传递。)

作者

SteveDonovan


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2010 年 10 月 14 日下午 2:21 GMT (差异)