5.4. Make#

Make is a critical tool to let developers automate compiling complicated programs. After motivating why you want to do master it, we provide an example makefile and demonstrate a bit of the power of this tool.

5.4.1. Motivation#

For very simple programs, you can use emacs to write them in a single file and can compile them into executables to be run using, for example, the GNU C compiler gcc:

gcc filename.c -o filename

That’s fine for little programs, although even there, there are lots of flags that you will want to use to compile a program that are not the default with gcc. For example, we would recommend that you always use the following flags:

  • -Wall: turns on many compiler warning flags; about 90% of the simple bugs that students make are caught by gcc at compile time which emits warnings to ask you if you really want to shoot yourself in the foot. Please believe us, if there is a warning, it is much faster to fix it instead of finding the bug at run time later.

  • -Werror: turns warnings into compilation errors. This is super valuable so that you don’t miss the warning; again don’t come to a TF for help unless your program compiles with -Wall and -Werror.

  • -std=gnu99: this isn’t super critical, but gcc supports a variety of standard variants of the c programming language, so you might as well use a standard that will ensure that your program is compilable by other compielrs.

  • -O0: sets optimization level to 0. The compiler supports a wide set of optimizations for size, for performance… and for the final program, you might want to specify something different. However, optimizations re-arrange the code, copying functions into other functions. If you want to debug the program you wrote, use -O0, if you want to debug a program that is logically the same, but doesn’t really look like the software you wrote, use something else.

  • -g: adds debugging symbols to executable. Without this, you are pretty much out of luck if you want to use a debugger, and if you are not using a debugger, then you might as well give up on the projects associated with this course.

  • -I.: specifies directory where header files can be found (in this example, the working directory .). You should always seperate key information that describe the interfaces of your program and constants that you might want to change into header files. This flag tells the compiler where those header files are.

Okay, so now, to compiler your simple program, you will type:

gcc -Wall -Werror -std=gnu99 -O0 -g -I. filename.c -o filename

That’s a bit of a pain. How do you know if you remembered to do all that? How do you know how you compiled the program if you come back a day later, or prove to the TF of the course that you used the right flags. More importantly, what happens when you have more than a toy program that can be written in a single file? You won’t write any programs for this course that could be written in a single file. How do you remember which files you modified that need to be re-compiled? If you have a header file which 10 .c files depend on, how to you make sure that all 10 .c files are re-compiled?

That’s where make comes in. You create a makefile that describes the relationships between the files in your program and provides commands for updating each file. Usually for c programs, the executable file is updated from object files (.o files), which are in turn made by compiling source files (.c files). Once you have written your makefile, you can just run the shell command make and it will perform all necessary recompilations. make knows which files need to be updated based on the last-modification times of the files. You can also provide command line arguments to make to specify which files should be recompiled and how.

We expect you to have at least a basic understanding of how Make works. The version of make provided in the container image for this course is GNU Make. It has many rich features and default rules; for example, it understands that if you want to generate a .o file from a .c file, it should use the compiler. You can find extensive details here to write makefile rules.

5.4.2. A simple example#

Here is a simple example that you should understand that is used for a parser that will for many of you be the first assignment of this course.

Listing 5.2 A Makefile to build a parser#
 1override CFLAGS := -std=gnu99 -O0 -Wall -Werror -g  -fsanitize=undefined $(CFLAGS) -I.
 2override LDFLAGS := -fsanitize=undefined -fsanitize=leak $(LDLAGS)  
 3CC = gcc
 4
 5# I generally make the first rule run all the tests
 6all: check
 7
 8# rule for making the parser.o  that is needed by all the test programs
 9myshell_parser.o: myshell_parser.c myshell_parser.h
10
11
12# each of the test files depend on their own .c and myshell_parser.h
13test_simple_input.o: test_simple_input.c myshell_parser.h
14test_simple_pipe.o: test_simple_pipe.c myshell_parser.h
15
16# each of the test programs executables are generated by combining the generated .o with the parser.o
17test_simple_input : test_simple_input.o myshell_parser.o
18test_simple_pipe : test_simple_pipe.o myshell_parser.o
19
20# Add any additional tests here
21test_files=./test_simple_input ./test_simple_pipe
22
23.PHONY: clean check checkprogs all
24
25# Build all of the test program
26checkprogs: $(test_files)
27
28check: checkprogs
29	/bin/bash run_tests.sh $(test_files)
30
31clean:
32	rm -f *~ *.o $(test_files) $(test_o_files)

This makefile is used to generate and run a set of unit tests against a parser who’s implementation is in myshell_parser.c where functions implemented by that parser are defined in myshell_parser.h.

Let’s break down each line and rule. In the first line, we specify the flags we want to use to compile C files by assigning a value to CFLAGS (more on implicit variables like CFLAGS here). The override directive just makes sure you use the assignments in the makefile even if the variable has previously been set with a command argument. The argument -fsanitize=undefined to the compiler (CFLAGS) and linker (LDFLAGS) tells gcc to add additional sanitizer run time checks for undefined behavior.

The third line tells Make to use gcc as the compiler. The first rule all in this case is run by default. Here we are telling Make to run the rule check whenever make is run without any arguments. If, on the other hand, you type make clean it will run the rule on line 30 that will remove all the generated files.

Line 9 defines the rule to create the myshell_parser.o file that will be linked into all the test programs. It tells make that it should regenerate myshell_parser.o if either the corresponding .c file or .h file changes. Make has a set of implicit rules, where if no recipe is specified, Make understands that it needs to run a compiler to create a .o file from a .c file. There are many of these implicit rules as described here.

Lines 13 and 14 similarly indicates that the test programs depend not only on the corresponding .c file, but also on myshell_parser.h. That way, if you edit myshell_parser.h all the test programs that depend on a prototype you define in that header file will be recompiled. Lines 16 and 17 tell make that the test file depend on both the corresponding .c file and myshell_parser.o, and Make knows that it needs to link both .o files together to create an executable.

Line 20 specifies the list of test programs you are generated, which is used in line 24 that defines the rule checkprogs to generate those test programs, and Line 26 that defines the rule to run the run_tests script we showed before (Listing 5.1) to run all the tests. Line 22, which is not strictly necessary, tells Make that it is really not creating files called clean, check, checkprogs and all.

Note, this makefile could be greatly simplified by using a set of built-in variables Make supports (see here). For example:

  • $+ inserts all dependencies of the rule

  • $@ inserts the rule’s target

You normally don’t use make clean, but its useful when you want to, for example, commit your changes to the repository to make sure that new files you have recently created are not hiding among a whole bunch of generated files. Its also useful here to illustrate what is happening from a clean directory. So, if you type make clean and then make checkprogs {numref}`make_parser is:

If you type make checkprogs again, since nothing changed, make will not re-compile anything. While this may not seem like a big deal for your small programs, if you are compiling a complex program like an OS kernel, it can save you hours to only compile the programs you really need to. On our case, the second time we type make checkprogs we get:

If you modify test_simple_input.c make will only re-compile that file and link the corresponding executable. In this case, we can simulate modifying the file by using the unix command touch to change the modification time:

If on the other hand we modify myshell_parser.c Make understands it needs to re-compile that file and all the executables that link to it.

And if myshell_parser.h is modified, everything that relies on it needs to be re-generated.

5.4.3. Summary#

If you are writing complicated programs, composed of many files, you need to master make. The example we showed above showed how this one tool enables you to ensure that you always specify the right flags, and ensure that if a file has changed, everything that relies on it will automatically be regenerated. Finally, we have shown how you can automatically run a set of tests everytime you change anything; something we will talk more about here. For more info on make, see the GNU make manual.