Are you dissatisfied with Python? Do you prefer XML, YAML or JSON for build scripts? Or maybe you think you can create a better domain-specific language for build scripts? Before trying to invent new build system from scratch, try re-using the Waf framework.

Waf 1.6 features a new customization system for the Waf files, which means that the Waf libraries can be re-used easily without forcing the use of wscript files. Here is how to create a waf file that will print "Hello, world!" and stop:

waf configure 
--prelude=$'\tprint("Hello, world!")\n\tsys.exit(0)\n'
The resulting waf file will then exhibit the following lines of code:
if __name__ == '__main__':
print("Hello, world!")
sys.exit(0)
from waflib import Scripting
Scripting.waf_entry_point(cwd, VERSION, wafdir)
Writing code in the prelude section is a bit complicated, but the execution can be delegated to a separate waf tool. For example, with a tool named bbdlib featuring a function named "start":
./waf-light configure build  --tools=$PWD/bbdlib.py 
--prelude=$'\tfrom waflib.extras import bbdlib\n\tbbdlib.start(cwd, VERSION, wafdir)\n\tsys.exit(0)'
The file bbdlib.py will be included in the resulting waf file as waflib/extras/bbdlib.py, so the resulting code has to use an import. Also, the original parameters (current working directory, waf version and waf directory) are propagated as they can be useful in the execution:
if __name__ == '__main__':
from waflib.extras import bbdlib
bbdlib.start(cwd, VERSION, wafdir)
sys.exit(0)
from waflib import Scripting
Scripting.waf_entry_point(cwd, VERSION, wafdir)

Here is for example a script that may be used to read files named "bbit":
import os, sys, imp
from waflib import Context, Options, Configure, Utils, Logs

def start(cwd, version, wafdir):
try:
os.stat(cwd + '/bbit')
except:
print('call from a folder containing "bbit"')
sys.exit(1)

Logs.init_log()
Context.waf_dir = wafdir
Context.top_dir = Context.run_dir = cwd
Context.out_dir = os.path.join(cwd, 'build')
Context.g_module = imp.new_module('wscript')
Context.g_module.root_path = os.path.join(cwd, 'bbit')
Context.Context.recurse = lambda x, y: getattr(
Context.g_module, x.cmd, Utils.nada)(x)

Context.g_module.configure = lambda ctx: ctx.load('g++')
Context.g_module.build = lambda bld: bld.objects(
source='main.c')

opt = Options.OptionsContext().execute()

do_config = 'configure' in sys.argv
try:
os.stat(cwd + '/build')
except:
do_config = True
if do_config:
Context.create_context('configure').execute()

if 'clean' in sys.argv:
Context.create_context('clean').execute()
if 'build' in sys.argv:
Context.create_context('build').execute()
Here are a few points to keep in mind:
  • A module simulating a wscript file is created dynamically to simulate the execution of a top-level wscript file. This script attempts to re-use as much code as possible, but some configuration tests illustrate how to bypass all restrictions.
  • Although a few variables are shared by several classes (Context.top_dir), the initialization is not required. One might want to subclass the BuildContext class and start with a nearly empty script.
  • The commands configure, clean and build are implemented for the illustration. In practice it may be easier to create command subclasses for specific purposes.
The complete code source for this example can be found in the custom build section of the waf source distribution. If you start writing a Waf-based build system, feel free to notify us in the comments!