Introduction to C: Enhancing the Makefile

In this lesson, we'll expand our Makefile to add a clean target, and change it to accommodate compiling a program with multiple .c files.

Adding a clean Target

In lesson 2, while creating a basic Makefile, we briefly discussed the concept of a phony target. Generally, targets in a Makefile should refer to a file that will be created by its recipes. When you define aphony, you are telling make that the target does not correspond to any particular file. To do that, put a line at the top of the Makefile as follows:

.PHONY: clean
Compiling and Linking

In previous lessons, we built our executable binary (main) directly from the main.c source. In this lesson, we will change the process we use to compile our executable into two phases: compiling and linking. In the compiling phase, each .c file is translated into equivalent object code, and placed in a corresponding .o file. After every .o file is built, the linker will combine them into a single executable file.

As we write our clean target, let's take this into account. We will remove both the main executable and the .o files that will be built:

clean:
    $(RM) main
    $(RM) *.o

By using$(RM), we use make's predefined RM variable to remove the files. Using this variable automatically adds the -f argument, which will unconditionally delete the file(s) specified, and will not raise an error if the file(s) do not exist.

As always, be sure to use Tab characters when indenting each line of your make recipe.

Adding Targets to Compile and Link

Next, we'll change the main target to separately compile and link our code.

The first thing to note is, when organizing your Makefile, make sure the default target (the one you want to run if you simply type make) is at the top of the file.

Linking

We want to build our main binary when we run make, so we'll put that first, ensuring it is above the clean target.

main: obj
    $(CC) -o main *.o

Notice that we've used make's CC variable to reference the C compiler, and we've greatly simplified the command used to create the make executable: it now simply instructs the C compiler to link all the .o files (*.o) together to create the main executable.

Compiling the Object Files

Notice that our main target now depends on a target called obj, which will be defined as the set of all .o files that will be linked together.

At this point, I must admit that I didn't remember the syntax to do this in make. An internet search quickly pointed me to a Stack Overflow question, which showed a good way to do this:

obj: $(addsuffix .o, $(basename $(wildcard *.c)))

Note: The example code I provided for this lesson contains a mistake. The obj target should be added to the .PHONY definition, such by modifying it to look like this: makefile .PHONY: clean obj

Pattern Rules

In a modern version of make (such as GNU Make), pattern rules can be used to simplify the creation of generic recipes. The following pattern rule instructs make how to create a .o file based on a corresponding .c file:

%.o: %.c
    $(CC) -Wall -Werror -std=c11 -g -c $^ -o $@

The $^ variable is used in place of the right hand side of the pattern (the prerequisite .c file), and the $@ variable is used in place of the corresponding .o (object) file.

Further Reading

For large or complex projects, creating a generic Makefile can be a difficult or frustrating undertaking. Many projects use tools to automate the creation and maintenance of their build system. These tools usually try to account for per-platform differences, such as optional libraries or subtle differences in behavior. I won't cover those in these lessons, but I recommend looking at CMake or Automake if it becomes necessary.

Conclusion

By now, you should know how to create a basic Makefile that compiles an arbitrary number of .c files into object files, and subsequently invokes the linker to create the final executable. You've implemented .PHONY targets for collecting the list of object files your binary will require, and cleaning up your project.

Now you're ready for the next lesson: I'll show how to build upon this by moving some of the functionality in main.c into a separate .c file.