5.3. Makefiles#

Hopefully, you are familiar with the process of compiling c files into executables to be run. Usually, this looks something like:

gcc filename.c -o filename

This would compile the c file “filename.c” into an executable called “filename”, which we could then run by typing ./filename.

This seems easy enough, but having to type out commands like this can quickly become quite cumbersome when we have many different commands we want to run on different combinations of files or a lot of flags we want to use. In these more complex situations, we can use make. Make automates running commands on files when they have changed and is commonly used in “building” programs.

To use make, you must write a file called 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.

5.3.1. Makefile Basics#

A simple makefile consists of rules with the following syntax:

target ...: prerequisites ...
    recipe
    ...
    ...

The target is typically the name of a file that is generated by a program (e.g. an executable or object file). It can also be the name of an action to carry out, like “clean”.

The prerequisite is a file that is used as input to create the target. It will often be multiple files.

The recipe is an action that make carries out. A recipe may have more than one command. There must be a tab character at the beginning of each recipe line.

Here is a guide for writing makefile rules.

5.3.2. An Example#

Here is the incomplete makefile we provide for HW0:

override CFLAGS := -Wall -Werror -std=gnu99 -O0 -g  $(CFLAGS) -I.

# I generally make the first rule run all the tests
check: checkprogs
	/bin/sh run_tests.sh $(test_files)

# rule for making the parser.o  that is needed by all the test programs
myshell_parser.o: myshell_parser.c myshell_parser.h

# each of the test files depend on their own .c and myshell_parser.h
#  add another time for each test, e.g., test_simple_pipe.o line below
test_simple_input.o: test_simple_input.c myshell_parser.h
test_simple_pipe.o: test_simple_pipe.c myshell_parser.h

# each of the test programs executables are generated by combining the generated .o with the parser.o
test_simple_input : test_simple_input.o myshell_parser.o
test_simple_pipe : test_simple_pipe.o myshell_parser.o

# Add any additional tests here, e.g., the commented out test_simple_pipe
test_files=./test_simple_input # ./test_simple_pipe

.PHONY: clean check checkprogs all

# Build all of the test programs
checkprogs: $(test_files)

clean:
	rm -f *~ *.o $(test_files) $(test_o_files)

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.

  • -Wall: turns on many compiler warning flags

  • -Werror: turns warnings into compilation errors

  • -std=gnu99: set c version

  • -O0: sets optimization level to 0 (faster compilation, better for debugging)

  • -g: adds debugging symbols to executable

  • $(CFLAGS): include default flags

  • -I.: specifies directory where header files can be found (in this example, the working directory .

Following the CFLAGS definition, we see a rule for building the test programs.

Next, we have a rule for building the parser’s object file, followed by rules to build two test programs.

Below that, there is an additional line that you can edit to add additional test scripts.

The rest of the rules in the makefile have phony targets. They are not names for files, rather just names for a recipe to be executed when an explicit request is made. Using a phony target helps avoid conflicts with files of the same name and improves performance.

The checkprogs rule builds all of the test programs.

The last rule is the clean rule, which will just remove all object files and test files from the directory.

For more info on make, see this chapter of Jonathan Appavoo’s book. You can also reference the GNU make manual.