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,intlength,inttarget){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 + 1right=*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.
intmain(intargc,char**argv){intarray[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.