The programs we write in this course are small and actually do not justify the use of makefiles. In later courses however they will useful. Simply put, a makefile allows you specify the interdependencies among various files and to recompile your program by only recompiling those files that have changed (or depend on files that have changed) since the last compilation. The make utility compares the executable's last modification date with the modification dates of the .o files that it depends on. Likewise, those .o files modification dates are compared against their respective source files. If any files need to be recompiled, they are and the executable is rebuilt.
The makefile is a dependency tree where each node is dependent on its children. At the root is the executable file that you wish to run. Its children are the object files that it depends on. The children of each object file are the source files it depends on. We'll run through an overview of the compilation process and makefiles and then look at an example, makeDemo.zip, that contains several files that comprise a program along with a makefile that builds the executable.
The files that comprise this project are:The direct dependency tree is:
/ makeDemo.c makeDemo.o / \ arraylib.h / / / arraylib.c / demo - - - arraylib.o - - arraylib.h \ \ \ intlib.h \ / intlib.c \ intlib.o \ intlib.h
Notice the hierarchy: the executable depends on the object (.o) files. Each object file depends on source (.c, .h) files. Further notice that the only files we include in a source file's dependency list are those source files that are needed to compile that source file. Thus arraylib.o depends on arraylib.c, arraylib.h and intlib.h. intlib.h is included in arraylib.c and thus arraylib.o depends on it.
We execute the commands in Makefile by typing make. make will look first for a file called makefile and then Makefile for the commands to execute in building your executable (you can override this with the -f switch. If you haven't already done so, download the makeDemo.zip file and unzip it. Once you have the makeDemo directory, cd into it and type make. You should see something similar to the following:
$ cd makeDemo $ make gcc -Wall -pedantic -c -o makeDemo.o makeDemo.c gcc -Wall -pedantic -c -o arraylib.o arraylib.c gcc -Wall -pedantic -c -o intlib.o intlib.c gcc -Wall -pedantic -o demo makeDemo.o arraylib.o intlib.o -lm $ $./demo (the program will execute)
In the above excerpt we executed the Makefile by typing make in the same directory as the files. Note that all the .c files were compiled and the executable was built. We now want to demonstrate how the make utility only recompiles those files needed to build the project after one or more source files have been changed.
Let's now edit intlib.h and add a #define, then execute make again. Notice the result is that all the source files except makeDemo.c were recompiled because ultimately all the .o files except makeDemo.o depend on something that was affected by that change. Contrast this with what happens if we change intlib.c.
Lastly, let's change arraylib.h and execute the Makefile again. Note that this time every source file except intlib.c was recompiled. The Makefile did NOT recompile intlib.c because it is the only .c file that does NOT depend on (i.e., does NOT #include) arraylib.h.
This sample Makefile is as complex an example as we will likely need in this course. In fact, a simple one-liner that just recompiles all the files if any source is changed would be sufficient enough for the programs we write. However, when we encounter very large programs with many source files and hundreds of thousands of lines of code, the use of a makefile to keep track of dependencies is a necessity. A common example of such a scenario is compiling the Linux OS for installation on a microcomputer. Linux is open source and many experienced users prefer to download the source for the OS and compile it on their machine, rather than download a binary. Often a user will make changes to the source code to customize some aspect of the OS to their particular purpose. As these changes are being made, tested, and recompiled incrementally, the makefile prevents having to recompile ALL the code every time.