Lua Build Bou

lua-users home
wiki

Bou is a Lua-based build system using Lua as a DSL.

Bou has grown up and been renamed lake; see [1].

Bou: A Lua-based Build System

I first got interested in build tools implemented as embedded DSLS (Domain Specific Languages) by Martin Fowler's article on Rake [2]. Build languages like make or nant are classic DSLs, although the term 'little language' [3] has been around a lot longer and in fact is an important part of the Unix development philosophy of specialized tools that do one thing well.

Rake is an embedded DSL because it is hosted within a big language. That offers big benefits to both developer and user; the developer doesn't have to muck around reinventing a high-level programming language, and the user has the comfort and power of a tested programming language. It's easy to do unusual things without having to extend the language. Consider make; its power comes from having all those zillions of Unix commands floating around - it is much less powerful in non-POSIX environments. And one usually gets a bizarre mixture of make and shell script which is (shall we say) less than straightforward to maintain. nant is certainly cleaner, and one can write custom tasks in CLI languages, but one still has to 'drop out of the DSL' (as Fowler puts it) to do non-trivial things. XML is excellent for specifying hierarchical data, but not so good as a syntax for programming.

I've always considered Lua to be a good language for embedded DSL applications, which leads to Bou. Bou (pronounced like 'beau') is a build system based on pure Lua - the only external dependency is on the LuaFileSystem (lfs). It is deliberately similar to make in philosophy and uses the same language of targets, rules and dependencies. However, it brings two powerful things to the party; the ability to use arbitrary Lua code and a great deal of canned knowledge about some common compile tools.

The English language is rapidly running out as a resource for good open-source project names - 'lake' would have been perfect, but alas there's already a build system called that! 'bou' is Afrikaans for 'build'; you put your targets and rules in a 'boufile'.

Building Simple Programs

Consider an old friend:

#include <stdio.h>
int main(int argc, char**argv)
{
        printf("Hello, World - %d parms passed\n",argc);
        return 0;
}

Writing a makefile for the canonical "Hello, World!" program is overkill, but the equivalent boufile is very straightforward:

c.program 'hello'

Alternatively, you can get Bou to deduce that you have a C program, and say this:

program 'hello.c'

Executing Bou will give the following output:

> bou
gcc -c -O1 -DNDEBUG  hello.c
gcc hello.o  -o hello.exe

> bou
bou: up to date

Which is not in itself very impressive. But this simple boufile gives you features for free: it already knows about 'clean', knows how to use the Microsoft command-line compiler (at least on Windows), and knows how to make a debug build:

> 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

Observe that it was not necessary to invoke 'bou clean' before doing a debug build, because Bou is intelligent enough to know that the build has changed. This is done using a specfile:

> cat boufile.spec
link /nologo $(DEPENDS)  /OUT:$(TARGET)
cl /nologo -c /Zi /DDEBUG  $(INPUT)

By comparing the existing specfile to the commands generated, Bou can deduce that the commands have changed, and therefore require rebuilding.

For such a simple case, you can do without a boufile entirely, and let Bou deduce the tool needed to compile and run the file you specify:

> 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

Notice that hello.o was not regenerated!

Simple Programs with Dependencies

Consider a program with two files, one.c and two.c, which is called first.

c.program {'first',src='one,two'}

Running Bou, we get:

> bou
gcc -c -O1 -DNDEBUG  one.c
gcc -c -O1 -DNDEBUG  two.c
gcc one.o two.o  -o first.exe

This is not a very realistic situation - in practice, the source files will at least depend on some header files, and you will need to specify libraries. To specify more optional parameters, we use a common table idiom for passing 'named' parameters - note the curly braces:

c.program{'first',src='one,two',
   compile_deps='common.h',libs='user32,kernel32'}

We will now link properly against the required libraries, and if common.h changes, the source files will be recompiled:

> 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 gives Bou a list of libraries; it then decides how to format this list in the way appropriate for the particular tool. Other parameters include incdefs, for setting a list of include paths, and defines, for defining macros.

Explicit Dependencies

It is possible to say src='*.c', but this doesn't handle the fact that each source file has individual dependencies.

One very common format for expressing dependencies is the 'deps' format emitted by tools like GCC, suitable for inclusion into a makefile. Bou can process such a format explicitly. Consider the following 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
]]}

The rules parameter can be set to a filename, but if the string contains newlines it's assumed to be verbatim. Bou will do three things from this specification:

* generate targets based on the implicit rules it knows * extract the include paths * construct the dependency list for each target

> 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

Notice how global library and defines settings can be set using cpp.defaults.

Running Tests

Consider the common task of needing to build and run a number of test programs. These may require compilation (like C/C++) or can be directly interpreted. A boufile for a directory containing both C and Lua test programs could look like this:

target('all','c,lua')
target('c',forall_results('*.c',go))
target('lua',forall_results('*.lua',go))

The first target 'all' explicitly depends on the targets 'c' and 'lua'; the dependencies for 'c' are the result of generating program build-and-run targets for all C files in this directory, individually handled by go(). forall_results() is rather like map, except that it can take a wildcard instead of an explicit list and can collect multiple results from each invocation of the function.

Some Minimal Documentation

The rule function is passed the input extension, the output extension, and a command to turn the input into the output file; the standard variables INPUT and TARGET are set for you. Rather than setting a global rule, it returns a rule set to which you add target names. In this example, progs 'one' is short for progs:add_target 'one'. rule:add_target also has a second argument which can be used to pass explicit dependencies.

progs = rule('.c','.o','gcc -c $(INPUT)')
progs 'one'
progs 'two'

The target function takes three arguments, a name, any dependencies, and a command to be executed. The dependencies can be a list (a string will be converted automatically) or a set of dependencies, using depends. If the dependencies argument is nil, then the target is unconditional. The command can either be a Lua function, or a string, in which case it is interpreted as a command to be run using the shell.

The depends function is useful for defered calculation of dependencies. Here is a target which depends on the results of two target sets, which are only populated later:

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 As Programs

At the height of the XML craze, it would have been natural to encode build rules something like this:

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

This is also a tool-agnostic notation, but it is not a very good programming notation. By making the build language a subset of a real programming language, doing non-standard extra things doesn't require 'jumping out of the DSL'.

Bou has a way to go before it can handle all the tasks expected of a build environment, including support for installation. But hopefully it is a good starting base, and also I hope it shows that Lua is excellently suited to this kind of 'little language' application.

Getting and Installing Bou

[4] contains bou.lua and some small example projects. Unzip this somewhere, and make sure that you have lfs on your package cpath. lua bou.lua -f hello.bou should now work; if no boufile is provided it will look for boufile in the current directory. Then create a batch file

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

or script file, depending on your religion:

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

(The "$@" ensures that quoted parameters will be passed properly.)

Author

SteveDonovan


RecentChanges · preferences
edit · history
Last edited October 14, 2010 1:21 pm GMT (diff)