by
Wenton L. Davis
I hadn't originally planned on this, but I suddenly came across some interesting stuff on Makefiles, so, well, here we go...
Simple Makefile
Assuming you have a project that is compiled from multiple source .c files, you probably compile it thus:
~$ gcc file1.c file2.c file3.c -o executable
and for most simple uses, that's just fine. hoewver, once a project gets much bigger, those command lines get complex, and a pain to type. Instead, consider using a makefile. The makefile is a file that defines compilation of larger projects. That earlier example could be put into a very simple makefile, named Makefile:
executable: file1.c file2.c file3.c <TAB>gcc file1.c file2.c file3.c -o executable
and compiled:
~$ make
Basically, the Makefile specifies that the executable is dependent on the files file1.c, file2.c, and file 3.c, and that if any of those files are newer than the existing (or missing) executable file, to compile the code using the command(s) beginning in the next line. It is important that the next line begins with a TAB character, or the makefile won't work. (That <TAB> is not literally typed in the file, but showing tabs in text is tricky!)
OK, lets make this a bit more complex. Lets assume that each file also has a .h file, and that the project must also link in the math library, libm.a it becomes simpler to work with each source file by itself, generating object code files (.o), and then link them together, later:
LIBS="-lm" executable: file1.o file2.o file3.o gcc file1.0 file2.o file3.o $(LIBS) -o executable file1.o: file1.c file1.h gcc file1.c -c file2.o: file2.c file2.h gcc file2.c -c file3.0: file3.c file3.h gcc file3.c -c
Obviously this becomes cumbersome, but before we simplify it, we need to learn about the "automatic variables" available to us:
So we can rewrite the above makefile:
LIBS="-lm" executable: file1.o file2.o file3.o gcc $< $(LIBS) -o $@ file1.o: file1.c file1.h gcc -c $< file2.o: file2.c file2.h gcc -c $< file1.o: file2.c file2.h gcc -c $<
Next, lets add a couple of "dummy" targets. these allow us to type something like make clean to remove old object files and executables if we want to make a project completely clean, or if we have a project with multiple executables, we can type make all to make each executable.
LIBS="-lm" all: exectuable executable: file1.o file2.o file3.o gcc $< $(LIBS) -o $@ file1.o: file1.c file1.h gcc -c $< file2.o: file2.c file2.h gcc -c $< file1.o: file2.c file2.h gcc -c $< clean: rm *.o *.exe executable
Normally, the *.exe would only apply to working in the Windoze environment, or the executable with no dot extension would only apply to the Linux environment. a typical Makefile would not include both in the rm statement.
OK, we can finally start doing some neat things. I think we'll begin by using pattern rules to simplify all those compilations from .c to .o, and add just a few useful definitions:
LIBS="-lm"
CC=/usr/bin/gcc
LD=/usr/bin/gcc
CSRC = file1.c file2.c file3.c
COBJ = ${CSRC:.c=.o}
CFLAGS = -g
.SUFFIXES : .o .c
#compile .c files to .o
.c.o :
$(CC) $(CFLAGS) -c $<
executable: ${COBJ}
$(LD) ${COBJ} $(LIBS) -o $@
clean:
rm *.o *.exe executable
What in the hell....? OK, step by step: First, we add definitions that say we want to use gcc as the compiler and linker. the only real reason (but important) is that it is just too easy to leave out libraries when we try to use ld directly; lets let gcc figure out all that stuff.
Second, we create a list of all of our C code source files by listing them explicitely. Then, we create a list of object files that will be created, which is the same as the source code files, but also replace the .c extension with .o.
Then, we might want to include symbolic references for debugging, so for now, we will include the -g option. When we move to production, we will change that line to simply
CFLAGS =
which is completely unnatural for programmers since it looks like an incomplete statement. Don't worry, make will still accept this line to mean that CFLAGS is defined; it is just empty.
Then we have a really odd-looking line that simply defines .o and .c as suffixes (dot extensions) that are going to be important.
next comes the really bizzare "suffix rule" that simply tells make that the way to convert files ending in .c into files that end with .o is by following the following command, which is really just the same as all of the gcc -c file?.c commands, earlier.  make will not actually know to compile them yet.... this just tells make HOW to build them when it needs to.
Finally, make finds the line, requesting to build the executable, dependent on the lisf of COBJ files. If it decides that it needs to compile one or all of the .c files in order to produce updated .o files, it was just given the suffix rule for how to accomplish that, so it uses that rule for whicher files it needs, then once all of the .o files are done, link them, along with the libraries, into the executable file.
And viola, there it is.
OK, now lets go one more step. In many cases, I add assembly language to many programs, so lets look at adding them. let's assume that in addition to the three .c files, we want to add asm1.s and asm2.s to the program, and because we are really nuts, we are going to add a fortran file, nuts.f! We would modify the makefile:
LIBS="-lm"
AS=/usr/bin/as
FTN=/usr/bin/fortran
CC=/usr/bin/gcc
LD=/usr/bin/gcc
CSRC = file1.c file2.c file3.c
COBJ = ${CSRC:.c=.o}
ASRC = asm1.s asm2.s
AOBJ = ${ASRC:.s=.o}
FSRC = nuts.f
FOBJ = ${FSRC:.f=.o}
OBJECTS = COBJ AOBJ FOBJ
CFLAGS = -g
ASMFLAGS =
FTN FLAGS =
.SUFFIXES : .o .c
#assemble .s files to .o
.s.o:
$(AS) $(ASMFLAGS) -o $@ $< -alds=$*.list
#compile .c files to .o
.c.o :
$(CC) $(CFLAGS) -c $<
#compile .f files to .o
.f.o:
$(FTN) $(FTNFLAGS) -c $<
executable: ${OBJECTS}
$(LD) ${OBJECTS} $(LIBS) -o $@
clean:
rm *.o *.exe executable
So obviously, makefiles can be very powerful, and in some cases, they begin to look like a whole new level of programming. It might not seem like it at first, but trust me, they can become extremely useful!
Huh... I found this... to get a lit of every pattern rule and suffix rule and defaults that make is already aware of, type:
~$ make --print-data-base
Warning, this will be long, so pipe it through less.