Any such tool for taming the complexity of software helps developers become more productive.

Build systems are an example of such a tool; however, they are frequently neglected due to the false impression that they are only necessary for large software systems. This type of neglect leads to technical debt.

Problem of Repetition

In order to understand the necessity for build systems, we must understand the fundamental problem they tackle, repetitive tasks. Repetition is the scourge of software development from compilation tasks to repetitive strain injuries. Build systems alleviate developers from repetition through automation.

Some may claim that automation is not always necessary such as when hacking together binary search in C. We will show that even small projects demand repetitive tasks that could have been easily automated.

For those of you that underestimate the difficulty of binary search implementations, see how Google researchers made mistakes in their implementation of binary search.

A Simple Program

Suppose that we are given the task to automate binary search. Here is a preliminary code sample that handles this task. However, there is an error. Do you see it?

int *binary_search(int *array, int length, int target) {
    int *left = array;
    int *right = array + length - 1;
    int *mid = left + (right - left) / 2;
    while (left <= right && *mid != target) {
        left = *mid < target ? mid : left; // should be: mid + 1
        right = *mid < target ? right : mid - 1;
        mid = left + (right - left) / 2;
    }
    return *mid == target ? mid : NULL;
}

This problem could have easily gone unnoticed. Best practice tells us that we should write tests for our code in order to avoid such problems. So we will write one.

int main(int argc, char **argv) {
     int array[4] = {1, 2, 3, 4};
     assert(binary_search(array, 4, 4) == array + 3);
}

If you attempt to execute this code, you will find yourself in an infinite loop. Efforts to fix this bug involve recompiling and re-running the test until it becomes correct. In our command line, this will require the following commands:

$ gcc -o binary_search binary_search.c 
$ ./binary_search

We will call these commands, build commands.

Build Lifecycles, Repetitive

The lesson with the binary search example is that code will break; we fix broken code by recompiling and running tests. In fact, almost all developers fall under the consistent development cycle of coding, compiling, and testing. Even for such simple tasks, this becomes extremely repetitive. In fact, this three-fold repetition is a generalized form of a software development lifecycle.

Recall the build commands in the binary search example. Although they require very little typing, it is an unnecessarily repetitive task that can be automated. This is the purpose of GNU Make, a program for helping us automate building and running repetitive tasks outside of our code editor.

Introduction to GNU Make

GNU Make will build our programs and run our tests for us by executing a single command on our terminal make. We save several keystrokes per build in exchange for configuring GNU Make. We configure GNU Make through a Makefile which is a file, named as is, that is placed at the root of our project. For example, suppose our binary search project is structured as follows:

.
|-- binary_search
`-- binary_search.c

The configuration file, Makefile is placed at the root of our project directory:

.
|-- binary_search
|-- binary_search.c
`-- Makefile

When a Makefile exists in our project root, we may build the project by executing make. However, if your Makefile is empty, you will receive the following error:

make: *** No targets.  Stop.

We must first populate our Makefile so that make knows how to compile our code and run our tests. We will briefly discuss the syntax for Makefile.

Targets and Components

GNU Make is intended for building files, so we need to specify what files we want to build. The built files are known as targets. Targets frequently depend on other files; such dependencies are known as components. Together with a command to build the target from the components, they compose a rule of the form:

target: component1 component2 componentN
    command

Recall in our binary search example that we would like to build a binary_search executable from the binary_search.c source using gcc. We may encode this rule in our Makefile as follows:

binary_search: binary_search.c
    gcc -o binary_search binary_search.c

Compiling Programs

Now, executing make will run the default rule which is always the first rule defined in Makefile.

$ make
gcc -o binary_search binary_search.c

Now, we can build our program in a single command! At first, this may seem like a lot of effort just to get our build to one line; however, a single command is easy to bind to a key in your favorite text editor such as vim.

From command line to build command, the order of execution is as follows: the user specifies a target for make to build, the rule associated with the target is found, the components are recursively searched for as independent targets for their associated rule, and the command associated with the rule is executed.

In addition to the ease of building, make is able to decide which components to build based on which source files have been modified through their last modification time. This modular rebuilding is made possible through the dependency of targets to components in rules.

Running Tests

Recall that running tests is also a common aspect of our development cycle. We can also automate this process in make by adding another rule:

test: binary_search
    ./binary_search

We add the test target which depends on the binary_search executable in order to run its tests. Recall that executing make in isolation will run the default rule (the first rule defined in the Makefile). In order to run test we may pass the target as an argument:

$ make test
# Infinite loop due to bug
# Press Ctrl-C to kill the program

Build rules are as simple as that.

Conclusion

At this point, you should be getting creative about automation. In particular, it should not be hard to conceive of a rule that will both compile and test your code at the same time. This is where the true power of automation will shine. Starting early will help you prevent technical debt.

Save yourself a few keystrokes for each development cycle; automate your builds.