Tutorial
Table of contents:
- Setup
- Building Without a Build System
- The Build Description
- Building It
- Going Meta: Building the Build Description
If you haven’t done so already, follow the installation instructions to install Button.
Setup
Suppose, in our button-tutorial
directory, we have two C files and one header
file that we want to build:
button-tutorial
|-- bar.c
|-- foo.c
`-- foo.h
foo.c
:
#include "foo.h"
const char* foo()
{
return "Hello world!";
}
bar.c
:
#include "foo.h"
#include <stdio.h>
int main()
{
puts(foo());
return 0;
}
foo.h
:
#ifndef FOO_H
#define FOO_H
const char* foo();
#endif // FOO_H
Building Without a Build System
Without a build system, we might compile and link these like so:
$ gcc -c foo.c -o foo.o
$ gcc -c bar.c -o bar.o
$ gcc foo.o bar.o -o foobar
Alternatively, it could have been done in one step:
$ gcc foo.c bar.c -o foobar
However, that approach doesn’t scale well with many source files. If only one source file has changed since the last compilation, the compiler would be duplicating a lot of work by building everything from scratch. Build systems generally take the first approach so that only the necessary files are recompiled.
The Build Description
In order to automate the three steps above, we need to create a build description. A build description lists every step necessary to build a project.
Button reads in the build description in JSON format. The above three steps would look like this:
[
{
"inputs": ["foo.c", "foo.h"],
"task": [["gcc", "-c", "foo.c", "-o", "foo.o"]],
"outputs": ["foo.o"]
},
{
"inputs": ["bar.c", "foo.h"],
"task": [["gcc", "-c", "bar.c", "-o", "bar.o"]],
"outputs": ["bar.o"]
},
{
"inputs": ["foo.o", "bar.o"],
"task": [["gcc", "foo.o", "bar.o", "-o", "foobar"]],
"outputs": ["foobar"]
}
]
Obviously, you don’t want to write this by hand for real-world projects. However, for learning purposes, it is important to do it at least once. In later sections, we will generate the build description as part of the build.
Name this JSON file button.json
. Our source tree should then look like this:
button-tutorial
|-- bar.c
|-- button.json
|-- foo.c
`-- foo.h
Visualizing It
It can be very useful to see what the build description looks like. There is a command to produce input for GraphViz:
$ button graph --full | dot -Tpng > build.png
Like looking at a map, you can gain an immediate understanding of what the build
is doing by looking at its graph. See the documentation on button
graph
for more information.
Building It
To build it, simply run button build
:
$ button build
> gcc -c foo.c -o foo.o
> gcc -c bar.c -o bar.o
> gcc foo.o bar.o -o foobar
$ ./foobar
Hello world!
By default, Button looks for a button.json
file in the current directory (or
any parent directory). See the documentation on button build
for more information.
Lets kick the tires and see what happens to the build under certain scenarios.
The Null Build
Immediately running the build again, without changing anything, nothing will happen:
$ button build
Button sees that none of the source files have changed and so it has nothing to do.
Touching a File
What happens if we touch
a file (i.e., change its modification time stamp)?
$ touch foo.c
$ button build
Nothing happened! Indeed, nothing needs to happen. The file itself didn’t change – only the metadata associated with the file. While other build systems use a file’s time stamp to determine changes, Button determines changes based on the checksum of a file’s contents. This ensures 100% correctness when detecting changes to files. There are many cases where a file’s contents can change but its timestamp remain the same.
Modifying a File
Lets change the return string in foo.c
to "Farewell, cruel world!"
and run the
build again:
#include "foo.h"
const char* foo()
{
return "Farewell, cruel world!";
}
$ button build
> gcc -c foo.c -o foo.o
> gcc foo.o bar.o -o foobar
$ ./foobar
Farewell, cruel world!
Of course, foo.c
changed and so it got recompiled. Since bar.c
hasn’t
changed, there is no need to recompile it. It also relinked because foo.o
changed after recompiling foo.c
.
Deleting an Output
What happens if we delete the executable foobar
?
$ rm foobar
$ button build
- Warning: Output file `foobar` was changed externally and will be regenerated.
> gcc foo.o bar.o -o foobar
Button sees that foobar
doesn’t exist anymore and rebuilds it. If foobar
was
modified by us in some other way, it would have been rebuilt as well.
Adding a Comment
Lets add a comment to foo.c
and see what happens:
#include "foo.h"
/**
* Returns a pleasant greeting. Guaranteed to be random.
*/
const char* foo()
{
return "Farewell, cruel world!";
}
$ button build
> gcc -c foo.c -o foo.o
Only foo.c
was rebuilt. It wasn’t relinked. Shouldn’t it have relinked?
When compiling object files, gcc
is deterministic. That is, given the same
input, it always produces the same output. Adding a comment to the source file
has no effect on the generated code and so gcc
generates the same exact object
file as it would without the comment. Of course, this is specific to gcc
. Not
all compilers are deterministic like this, but they should be (I’m looking at
you Microsoft!).
Here, the checksum of foo.o
did not change from the previous build and so
Button avoids doing work that doesn’t need to be done.
Modifying the Build Description
How does Button handle changes to the build description?
Lets remove the compilation of bar.c
from the build description and run the
build again. button.json
should then look like this:
[
{
"inputs": ["foo.c", "foo.h"],
"task": [["gcc", "-c", "foo.c", "-o", "foo.o"]],
"outputs": ["foo.o"]
},
{
"inputs": ["foo.o", "bar.o"],
"task": [["gcc", "foo.o", "bar.o", "-o", "foobar"]],
"outputs": ["foobar"]
}
]
$ button build
> gcc foo.o bar.o -o foobar
gcc: error: bar.o: No such file or directory
➥ Error: Task failed
:: Build failed! See the output above for details.
Linking failed because bar.o
doesn’t exist on disk! Button deleted bar.o
.
Button stores the build description internally in a database and does a
comparison to what is in button.json
. If it sees that a rule was removed, it
will delete its outputs from disk. This helps ensure that incremental builds,
even in the face of structural changes to the build description, are correct. If
bar.o
hadn’t been deleted, the link task would have happily succeeded. We
could have spent a long time tracking down why the program is misbehaving at
runtime. Instead it failed at build-time as it should.
Going Meta: Building the Build Description
As mentioned earlier, writing the JSON build description by hand simply doesn’t scale for anything beyond trivial examples. Lets build the build description as part of the build.
Go ahead and delete everything except the source files, including button.json
.
We’re going to start fresh:
$ button clean --purge
$ rm button.json
Our source should now look the same as it did at the start of the tutorial:
button-tutorial
|-- bar.c
|-- foo.c
`-- foo.h
Lets initialize it with some extra files:
$ button init .
That created a few new files for us, so we don’t have to do it manually. Our source tree should now look like this:
basic-example
|-- bar.c
|-- BUILD.lua
|-- button.json
|-- foo.c
|-- foo.h
`-- .gitignore
The important ones are button.json
and BUILD.lua
. Lets take a peek inside
button.json
:
[
{
"inputs": ["BUILD.lua"],
"task": [["button-lua", "BUILD.lua", "-o", ".BUILD.lua.json"]],
"outputs": [".BUILD.lua.json"]
},
{
"inputs": [".BUILD.lua.json"],
"task": [["button", "build", "--color=always", "-f", ".BUILD.lua.json"]],
"outputs": [".BUILD.lua.json.state"]
}
]
The first rule runs button-lua
on the file BUILD.lua
to generate the build
description .BUILD.lua.json
. The second rule then runs Button with the
generated build description file.
Lets run a build:
$ button build
> button-lua BUILD.lua -o .BUILD.lua.json
> button build --color=always -f .BUILD.lua.json
As expected, it ran the two rules in button.json
, but the second rule didn’t
really do anything. The generated .BUILD.lua.json
has no rules in it because
BUILD.lua
isn’t creating them yet.
BUILD.lua
currently only has a comment in it:
--[[
This is the top-level build description. This is where you either create
build rules or delegate to other Lua scripts to create build rules.
See the documentation for more information on how to get started.
]]
To generate our rules using Lua, we can add this to the end of BUILD.lua
:
rule {
inputs = {"foo.c", "foo.h"},
task = {{"gcc", "-c", "foo.c", "-o", "foo.o"}},
outputs = {"foo.o"},
}
rule {
inputs = {"bar.c", "foo.h"},
task = {{"gcc", "-c", "bar.c", "-o", "bar.o"}},
outputs = {"bar.o"},
}
rule {
inputs = {"foo.o", "bar.o"},
task = {{"gcc", "foo.o", "bar.o", "-o", "foobar"}},
outputs = {"foobar"},
}
rule
is a function that takes a table as an argument. Since we’re working with
a full-fledged programming language, we can add more abstractions so it isn’t so
verbose. Indeed, there are modules to do exactly that:
local cc = require "rules.cc"
cc.binary {
name = "foobar",
srcs = glob "*.c",
}
"rules.cc"
is a module that lets you generate rules for C and C++ builds.
Here, calling cc.binary
generates the compilation and linker rules for us.
This is the JSON file it generates:
[
{
"inputs": ["bar.c"],
"task": [["gcc", "-c", "bar.c", "-o", "bar.c.o"]],
"outputs": ["bar.c.o"],
"display": "cc bar.c"
},
{
"inputs": ["foo.c"],
"task": [["gcc", "-c", "foo.c", "-o", "foo.c.o"]],
"outputs": ["foo.c.o"],
"display": "cc foo.c"
},
{
"inputs": ["bar.c.o", "foo.c.o"],
"task": [["gcc", "-o", "./foobar", "bar.c.o", "foo.c.o"]],
"outputs": ["./foobar"],
"display": "ld foobar"
}
]
You may notice that there is a new "display"
field. This just gives a
human-readable name to the task. The display name is printed in the output
instead of the command line of the task. It is not uncommon for command lines to
get very long and thus very unreadable. Linker commands that are linking in many
object files are usually the biggest offenders.
Your BUILD.lua
should now look like this:
--[[
This is the top-level build description. This is where you either create
build rules or delegate to other Lua scripts to create build rules.
See the documentation for more information on how to get started.
]]
local cc = require "rules.cc"
cc.binary {
name = "foobar",
srcs = glob "*.c",
}
Now we can run a build:
$ button build
> button build --color=always -f .BUILD.lua.json
> cc bar.c
> cc foo.c
> ld foobar
Notice that the display names were shown instead of the full command lines.
BUILD.lua
can be freely modified and Button will detect changes to it and
rebuild as necessary. If we modify BUILD.lua
without changing how the build
description is generated, nothing will get rebuilt.