A Tour of Waf

Table of Contents

1. Introduction

The Waf book opens by aptly describing Waf as an extensible build system. That book is well written and along with the API documentation and the FAQ waf documentation is comprehensive. The goal of this Waf tour is to collect additional understanding of the Waf system from the point of view of a long time, if still amateur user, in the hopes it will augment existing Waf documentation.

1.1. Concepts

A Waf build is the execution of a set of interdependent tasks. A task consumes a number of sources and produces a number of targets. Sources and targets may be files, which Waf abstracts into objects called nodes. Sources may also be values of variables in a Waf environment. Tasks may be defined directly in a wscript file that provides Waf directives expressed in the Python language. Tasks may also be created during the build with task generators. Code to produce tasks may be bundled into reusable Waf tools. The detailed nature of the build may be controlled through a Waf configuration and that configuration is communicated and persisted with one or more Waf environments. The graph of tasks, sources and targets may be factored into one or more subgraphs or Waf groups. Each group will be executed in sequence. Within a group, tasks will execute in parallel to the extent possible, limited by dependencies and a maximum allowed number of jobs. Finally, configuration and build and other commands are managed through Waf contexts.

1.2. Examples

This tour will have a number of examples. They are run as part of preparing this document into HTML or PDF. Generally they include three major waf commands in a step like:

waf distclean configure build

The distclean is not typically required nor desired. It is included here to assure each example runs from a known initial state.

2. Environment

A Waf environment is similar to but distinct from both the shell environment that runs the waf command and the shell environments that may run commands in the service of a task. Where not otherwise qualified, an environment or env will refer to a Waf environment.

2.1. Calling environment

In general, the value of a variable in the calling shell environment has no impact on values in a Waf environment. However, some Waf code will allow the shell environment to influence values in the Waf environment.

from waflib.Logs import info
def configure(cfg):
    cfg.env.FOO = "bar"
    cfg.find_program("cc")
    info(str(cfg.env))
def build(bld):
    info(str(bld.env))

In configure() we set the Waf environment variable FOO. We also instruct Waf to find the program cc. By default, the resulting location will be placed in an environment variable of the same name but capitalized, namely here CC.

waf distclean configure
'distclean' finished successfully (0.002s)
Setting top to                           : /home/bv/org/topics/waf/tour/calling-environment 
Setting out to                           : /home/bv/org/topics/waf/tour/calling-environment/build 
Checking for program 'cc'                : /usr/bin/cc 
'BINDIR' '/usr/local/bin'
'CC' ['/usr/bin/cc']
'FOO' 'bar'
'LIBDIR' '/usr/local/lib'
'PREFIX' '/usr/local'
'configure' finished successfully (0.003s)

In addition to the variables we set directly or through find_program(), Waf has set PREFIX, BINDIR and LIBDIR. We may return configure with some shell environment variables set.

PREFIX=/tmp CC=gcc FOO=baz waf configure
Setting top to                           : /home/bv/org/topics/waf/tour/calling-environment 
Setting out to                           : /home/bv/org/topics/waf/tour/calling-environment/build 
Checking for program 'cc'                : gcc 
from configure:
	FOO = bar
	CC = ['gcc']
'configure' finished successfully (0.003s)

Note FOO remains unchanged while CC takes the value we gave in the shell environment. Not only is PREFIX changed but so are the two directories. Waf will persist an environments persist between calls to configure and build.

PREFIX=/nope waf build
Waf: Entering directory `/home/bv/org/topics/waf/tour/calling-environment/build'
from build:
	FOO = bar
	CC = ['gcc']
Waf: Leaving directory `/home/bv/org/topics/waf/tour/calling-environment/build'
'build' finished successfully (0.011s)

Rerunning configure will reset all variables.

waf configure
Setting top to                           : /home/bv/org/topics/waf/tour/calling-environment 
Setting out to                           : /home/bv/org/topics/waf/tour/calling-environment/build 
Checking for program 'cc'                : /usr/bin/cc 
'BINDIR' '/usr/local/bin'
'CC' ['/usr/bin/cc']
'FOO' 'bar'
'LIBDIR' '/usr/local/lib'
'PREFIX' '/usr/local'
'configure' finished successfully (0.004s)

3. Nodes

Waf abstracts the file system into instances of a class Node. Nodes may be collected into a tree with a node representing a directory continuing children nodes or representing a file. Nodes may be created to represent an as yet non-existent file or directory.

3.1. Node features

from waflib.Logs import info
def configure(cfg):
    info(f'cfg.path = {cfg.path}')
    cfg.find_program('cat')
def build(bld):
    info(f'bld.path = {bld.path}')
    info(f'bld.bldnode = {bld.bldnode}')
    wscript = bld.path.find_resource("wscript")
    info(f'wscript = {wscript}')
    info(f'parent = {wscript.parent}')
    this = bld.path.make_node("this.txt")
    this.write("# end of file.\n")
    that = bld.path.find_or_declare("that.txt")
    bld(rule="${CAT} ${SRC} > ${TGT}", shell=True,
        source=[wscript, this], target=that)

Here we print a few path values, make use of .parent. We also call node methods find_resource() (a node that must exist in the source) and find_or_declare() (a node that may exist already in the source or the build area or which may be created during the build) and make_node() to create a node from thin air. We also use a node as an file-like object to write. Finally, we tie it all into a rule that combines two source files, one that we created, to a target.

We will discus rules next.

waf configure build
Setting top to                           : /home/bv/org/topics/waf/tour/node-features 
Setting out to                           : /home/bv/org/topics/waf/tour/node-features/build 
cfg.path = /home/bv/org/topics/waf/tour/node-features
Checking for program 'cat'               : /usr/bin/cat 
'configure' finished successfully (0.004s)
Waf: Entering directory `/home/bv/org/topics/waf/tour/node-features/build'
bld.path = /home/bv/org/topics/waf/tour/node-features
bld.bldnode = /home/bv/org/topics/waf/tour/node-features/build
wscript = /home/bv/org/topics/waf/tour/node-features/wscript
parent = /home/bv/org/topics/waf/tour/node-features
Waf: Leaving directory `/home/bv/org/topics/waf/tour/node-features/build'
'build' finished successfully (0.011s)

The .path points to the current source directory. In a simple project, as in the example, this is the top-level directory. When a project consists of a number of sub-projects each in a sub directory and each with a wscript file, the .path will changes as the build descends. By setting the top-level variable top in a wscript file an alternative source directory may be specified. In addition the default location of build/ for receiving targets may be changed by setting the variable out in the wscript file.

tail -1 build/that.txt
# end of file.

4. Rules

The kernel of Waf operation is a rule. It specifies a file transformation parameterized by the input or source files and/or the output or target files.

4.1. This to that

Produce one file from another file with a command and everything is hard-wired.

def configure(cfg):
    pass
def build(bld):
    bld(rule="cp ../this.txt that.txt")

Normally, one will never define fully literal rule commands nor hide input and output files from Waf as we do in this example.

Waf runs tasks from the build/ directory and normally all targets produced by a task should place files into this same directory. Waf will assure proper file placement but here we are cutting Waf out of that duty. Next section will repeat this example in a more proper manner.

date > this.txt
waf distclean configure build
'distclean' finished successfully (0.002s)
Setting top to                           : /home/bv/org/topics/waf/tour/this-to-that 
Setting out to                           : /home/bv/org/topics/waf/tour/this-to-that/build 
'configure' finished successfully (0.002s)
Waf: Entering directory `/home/bv/org/topics/waf/tour/this-to-that/build'
[1/1] Running cp ../this.txt that.txt
Waf: Leaving directory `/home/bv/org/topics/waf/tour/this-to-that/build'
'build' finished successfully (0.019s)
Sat Apr  8 05:19:18 PM EDT 2023
Sat Apr  8 05:19:18 PM EDT 2023
cat this.txt build/that.txt

Even though we have denied Waf any knowledge of what files the rule produced, it knows that it successfully ran the task once and will not rerun it:

waf build
Waf: Entering directory `/home/bv/org/topics/waf/tour/this-to-that/build'
Waf: Leaving directory `/home/bv/org/topics/waf/tour/this-to-that/build'
'build' finished successfully (0.018s)

4.2. Source to target

Here we do not deny Waf the knowledge of the input and output to a rule to repeat the above example in a more proper way.

def configure(cfg):
    pass
def build(bld):
    bld(rule="cp ${SRC} ${TGT}", source="this.txt", target="that.txt")

The SRC and TGT variables referenced in the rule string map to an list of input and output nodes, respectively. A node is a Waf file or directory object. By default, ${SRC} expands to a space separated list of the paths of the nodes in the input array. Individual nodes in the array may be addressed through indexing, as in ${SRC[0]}. Node methods may be called for example, ${SRC[0].abspath()}.

Although we know that targets go into build/ we need not mention that directory.

And, when specifying source or target, Waf is generous in that it can accept a file name as a string or array of file name strings, a node or array of node.

date > this.txt
waf distclean configure build

We suppress the output this time as it is same as above.

4.3. Rule function

So far, a rule has been given as a parameterized or templated command line string. A rule can also be a Python function.

from waflib.Utils import subst_vars
def copy(task):
    src = task.inputs[0]
    tgt = task.outputs[0]
    tgt.write(src.read())
def configure(cfg):
    pass
def build(bld):
    bld(rule=copy, source="this.txt", target="that.txt")

The copy() rule implements a primitive version of the cp command. We will again omit the output of running this example.

5. Tasks

6. Task generators

7. Contexts

8. Options

9. Configuration

The configuration() function has already been used in a number of examples. Here we further explore what this function may do.

10. Tools

10.1. Using tools

10.2. Writing tools

10.3. Built in and extra tools

10.4. Distributing tools

11. Advanced Waf

Much can be done simply with Waf as described so far. A dividing line between simple and advance may be crossed when it becomes necessary to extend Waf itself. Extension is done by “bolting on” and sometimes altering methods and values to Waf internal classes and objects. This is a perfectly acceptable thing to do and Waf provides some facilities and documentation on how to do it. However, it is one of the more complex ways to work with Waf, at least for this tourist.

Author: BV

Created: 2023-04-09 Sun 17:44

Validate