Makefiles | The power to build
Working with languages like C/C++ requires running a process to compile our project. That process can look like a black box where magic happens, but it's not that complicated. Let's see how Makefiles work and how to write practical ones for your everyday hacks and projects.
You've maybe heard of
Makefile
generators likecmake
. We're not using them here, neither a heavy IDE. The article is explained using a plain text editor and the command-line.
make
is a tool used mostly to compile and build C/C++ source code. Makefiles just tell make
how to compile and build a program. They consist in a series of instructions that perform automatically rather than having to manually type them in the command-line.
— Before we go further with make
, let's check what happens when we call the compiler so we know how to structure steps inside the Makefile later.
A simple compiling process takes four steps:
- The first step a compiler does is take our
.c
files and call the preprocessor, which handle the directives that start with a#
like#include
and#define
and gets rid of the comments that may be present in our code.
At this point, all the code inside the header files that we have included using the directive #include "header.h"
is copied and pasted in the program.
- The second step takes the source file and calls the compiler to translate C code into Assembly code, ending up with a file that has a
.s
extension. - Once the compiler is done, it needs to translate the Assembly code into machine code, creating an object file, which is done via the assembler. The result file
.o
isn't an executable yet. - The last step is bringing together all the object files to produce an executable. This part is done with the linker.
Flags
Each one of the steps needed to build a program can be invoked with a specific option to the compiler.
Flags give us the ability to enable or disable functionality for our building processes.
-E
calls the preprocessor only.-S
runs the compiler and stops at the assembly file.-c
is used to run the compiler up to the creation of an object file.-o
generates the executable program from the object files.-g
allows using gdb debugging.-Wall
enables the compiler to print all warnings encountered while building the program.-I
specifies a directory that contains prerequisites.
All the previous flags can be manually typed in a command-line environment:
$ cc -o demo_program main.c
but the idea here is to store those commands in a Makefile
to automatically perform the build.
Basic structure
A Makefile (case sensitivity named) is a plain text file that can contain the following sections:
- Rules (that can be explicit or implicit)
- Macros (variable definitions)
- Comments
Rules
A Makefile rule needs three basic items:
- A target, which is the name of the generated file.
- The dependencies needed to build the target.
- An action to realize in order to get the target.
Actions need to be indented by a tab character (not spaces) in order to work.
Note that we can have more than one action per target, and each one needs to be in a new line.
target: dependencies
action
Let's pretend that we have a set of source files that we can compile into a program named calculator which depends on five independent source files.
calculator : main.c sum.h sub.h mult.h div.h
cc -o calculator main.c
- Our target can be named as the program we want to create, so in this case is calculator.
- Our dependencies are five object files. Each of those files comes from it's own source.
- Our action is to execute the desired compiler, in this case cc to generate an output executable with the name calculator .
Compiled programming languages like C require us to recompile the program each time we change the source code. While the program keeps being simple there's no problem in rebuilding the whole program even if we only changed one file. But when we start to have a more complex program, compilation times increase, and recompiling everything just to update few changes is not effective.
The same way we create the target calculator we can make a target for each of the files that build it.
calculator: main.o sum.o sub.o mult.o div.o
cc -o calculator main.o sum.o sub.o mult.o div.o
main.o: main.c main.h
cc -c main.csum.o: sum.c sum.h
cc -c sum.csub.o: sub.c sub.h
cc -c sub.cmult.o: mult.c mult.h
cc -c main.cdiv.o: div.c div.h
cc -c div.c
This method forces us to write function declarations in separated .h
header files and definitions in .c
files to avoid multiple definitions. But we take the advantage of building only the objects that have modified dependencies.
make()
checks the timestamp of the files to keep track of modifications. If an object dependency gets a timestamp that is newer than the object's timestamp, it'll recompile that object when executed.
We can also create rules for steps that don't involve compiling or building the program, such as placing the built program in the correct directory, removing it (the same as uninstalling) or cleaning the compiled objects.
clean:
rm -f *.o calculator
Now instead of manually removing those files when we need a clean build, we can call make clean
and the rule will perform the action.
To make an install rule we can follow the same procedure, just adding the binary as a dependency to the rule:
install: calculator
mkdir -p /opt/calc
cp $< /opt/calc/calculator
and the uninstall rule is a simple recipe that removes the copied file:
uninstall:
rm -f /opt/calc/calculator
The rules that don't involve compiling or building a program can get us in trouble if we ever meet the situation where an object is named like them (clean, install or uninstall in this case). To solve this, make has PHONY
targets which are just a name for a recipe to be executed when you make an explicit request.
.PHONY: clean
clean:
rm -f *.o calculator
This way we avoid conflicts with other files.
Macros
When programs start increasing the number of source files and library dependencies, the amount of objects and files to track increases. Luckily for us, make can handle this if we use macros (variables).
A macro has the following format:
name = data
where name is an identifier and data is the text that'll be substituted each time make sees ${name}.
Some predefined macros are:
CC
is used to store the name of the compiler which we want to use (cc, gcc, clang, etc).
CC = cc
CFLAGS
is used to list the flags we want the compiler to use.
CFLAGS = -c -g -Wall
LDFLAGS
is used to link libraries. Some header files like<math.h>
are part of the system and aren't locally present in our code, but as any other header file, they contain just declarations and the compiler needs to check for the actual definitions somewhere.
LDFLAGS = -lm
Similarly we can make a macro for all our source files, dependencies and objects.
SRC = main.c sum.c sub.c mult.c div.c
OBJ = $(SRC:.c=.o)
We are storing all our source files in the SRC
macro, and since the object files share names with the source files, we are transforming the content inside SRC
by changing the .c
suffix with .o
and storing it in the OBJ
macro.
Source files can be huge in number, and manually typing each source file name can end up being tedious and make the line hard to work with.
BSD Make can store all the source files in a variable by executing a command-line operation, by using !=
:
!= ls src/*.c SRC
and if you want to find files in subdirectories too, you can execute:
FILES!= find . -type f -name '*.c'
On the GNU Make version we can take the advantage of wildcards to get all source files stored in a variable:
SRC = $(wildcard \*.c)
Both of them which will take every .c
file inside the current directory.
Note that the value for SRC is encapsulated between brackets and includes the explicit wildcard word. If we just associate src
to *.c
it will store the literal set of characters and won't behave as expected.
Source files may happen to be in different directories. In that case we only need to repeat the wildcard process:
SRC = $(wildcard src/*.c) $(wildcard src/modules/*.c)
Macros don't need to be upper case, and can be used arbitrarily to simplify name repetitions like our program's name:
prog_name = calculator
Our Makefile
can be transformed in something cleaner:
CC = cc
CFLAGS = -c -g -Wall
LDFLAGS = -lm
!= ls src/*.c
SRC OBJ = ${SRC:.c=.o}
prog_name = calculator
calculator: ${OBJ}
${CC} -o ${prog_name} ${OBJ} ${LDFLAGS}
main.o: main.c main.h
${CC} -c main.c
sum.o: sum.c sum.h
${CC} -c sum.c
sub.o: sub.c sub.h
${CC} -c sub.c
mult.o: mult.c mult.h
${CC} -c mult.c
div.o: div.c div.h
${CC} -c div.c
.PHONY: clean
clean:
${prog_name}
rm -f *.o
.PHONY: install
install: ${prog_name}
mkdir -p /opt/calc$< /opt/calc/calculator
cp
.PHONY: uninstall
uninstall:
rm -f /opt/calc/calculator
make()
can figure out that we want an object file from a source file as it has an implicit rule for updating an object .o file from a correspondingly named .c file using a cc -c command.
cc -c main.c -o main.o
The source .c
file is automatically added to the dependencies, so we can reduce our rule:
main.o: main.c main.h
${CC} -c main.c
letting it appear as:
main.o: main.h
Chances are that when building a program with make we get an error like this:
cannot find file "sum.h"
telling us that some required header isn't found. We can tell make where to look for prerequisites using the VPATH
macro.
The value of VPATH
specifies a list of directories that make should search expecting to find prerequisite files and rule targets that are not in the current directory.
VPATH = /inc /modules/inc
Note that
VPATH
will look through the directories list in the order we write them from left to right.
Another option to look for prerequisites is telling the compile where to look for them using the -I
flag which indicates a directory where the requested code should be:
-I/src/inc
and should be included in the CFLAGS
macro.
We can take our example and clean it with the new shown resources:
CC = cc
INC = -Iinclude
CFLAGS = -c -g -Wall ${INC}
LDFLAGS = -lm
!= ls src/*.c
SRCS OBJ = ${SRC:.c=.o}
prog_name = calculator
calculator: ${OBJ}
${CC} -o ${prog_name} ${OBJ} ${LDFLAGS}
${OBJS}: ${SRCS}
${CC} ${CFLAGS} -c $< -o $@
.PHONY: clean
clean:
${prog_name}
rm -f *.o
.PHONY: install
install: ${prog_name}
mkdir -p /opt/calc$< /opt/calc/calculator
cp
.PHONY: uninstall
uninstall:
rm -f /opt/calc/calculator
Now we only need to save the file and execute make calling the desired command. To build the calculator program it'd be:
$ make calculator && install
Comments
Comments are pretty much self explanatory. They are lines of text that as in programming languages, do nothing but provide useful information or reminders.
We can place comments around our Makefile
by using the hashtag #
symbol. Anything after a #
will be ignored.
# An example comment
Summing up
In addition to compiling and building our own C/C++ code, working inside a BSD system involves being working close with its source code, and most of the times we have to compile and build packages from ports. That process works the same way so you can now start tweaking and inspecting source Makefiles each time you need to change or install a program. It'll grant you access to custom install instructions specific for your machine.
We can do more things with make like building install menus, compiling libraries and including Makefiles inside other Makefiles. All those topics need a dedicated article for each of them.
There's an official manual for GNU Make that you can read for advanced knowledge in the tool, although staying POSIX is always a recommended thing.