Makefile

From OSDev.wiki
Revision as of 13:28, 16 January 2007 by Solar (talk | contribs) (Ported from old wiki, some improvements.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Setting up a simple Makefile is easy. You can even go a long way without a Makefile, simply by using make's internal rulesets.

Setting up a global project Makefile that works as intended is quite a bit harder.

I think I've assembled quite a good Makefile for [PDCLib], and would like to share some of the "tricks" that are not that obvious to the 'make' beginner. The Makefile creates only one project-wide object file, but it should be easy to expand it for multiple binaries / libraries. Some of the stuff described here is project-specific, but I left it in because it might be useful in other contexts, too.

Thus, the PDCLib Makefile in "tutorial mode":

  # This is a list of all non-source files that are part of the distribution.
  AUXFILES := Makefile Readme.txt

Further down I have a target "dist", which packages all source and header files into a tarball. Easy, as I create lists of the sources and headers anyway. For auxiliary files, I need this explicit list to have them included in the tarball.

   PROJDIRS := functions includes internals

Those are my subdirectories. It could be your subprojects, or whatever. I could use similar "autodetection" for them as I do for my source files further down, but as I like to have temporary subdirectories in my project (for testing, keeping reference docs etc.), that wouldn't work.

Note that this is not a recursive approach; there is no Makefile in those subdirectories. Dependencies are hard enough to get right if you use one Makefile. Google for a very good paper on the subject titled "recursive make considered harmful"; not only is a single Makefile easier to maintain (once you learned a couple of tricks), it's also more efficient!

   SRCFILES := $(shell find $(PROJDIRS) -mindepth 1 -maxdepth 3 -name "*.c")
   HDRFILES := $(shell find $(PROJDIRS) -mindepth 1 -maxdepth 3 -name "*.h")

While it should be obvious to see what these two lines do, it took some experimentation to get it right given the typical GNU-style documentation of 'make'. (Correct, but not very helpful.) I now have a list of all source and header files in my project directories.

   INTFILES := atomax digits seed strtox_main strtox_prelim

I think it's time I explain my approach to testing. I write strictly one library function per source file. Then, I add an int main() to that source file, encapsuled by an #ifdef TEST, containing test code for that function and returning the number of errors encountered. Thus, when I compile that source with gcc -c, I get an object file with the library function code; when I compile with gcc -DTEST, I get a test driver for that function.

In the special case of the PDCLib, I added another #ifdef, this time around the (standard) library function - #ifdef REGTEST (for "regression test"). So, when I compile the source file a third time, with gcc -DTEST -DREGTEST, I get the same test driver, but this time testing the library of the OS installed instead of the PDCLib - so I can see whether a given error is due to my library function or due to my test driver.

The INTFILES list is used to mask out certain files from the regtesting, as they are source files that are internal to other functions (atomax, digits, strtox_main and strtox_prelim are internals to the integer conversion functions, seed is internal to the pseudo-random number generator), and there is no equivalent to them in the system library to run a regression test on.

   OBJFILES := $(patsubst %.c,%.o,$(SRCFILES))
   TSTFILES := $(patsubst %.c,%.t,$(SRCFILES))

OBJFILES should be clear - a list of the source files, with *.c exchanged for *.o. I use the *.t extension for my test driver executables. The regtest executables end in *.r, but I need to filter out the INTFILES defined above (which all reside in ./functions/_PDCLIB/):

   REGFILES := $(filter-out $(patsubst %,functions/_PDCLIB/%.r,$(INTFILES)),$(patsubst %.c,%.r,$(SRCFILES)))

Then there is the issue of header files. Do you remember to adjust your Makefile every time you edit an #include somewhere in your code? If yes, did you know that GCC can do this automatically for you? Yes, it can. Trust me. Although the approach looks a little backward. For each source file, GCC will create a "dependency file", which contains a Makefile dependency rule listing that source file's includes. (And more, but see below.)

   DEPFILES := $(patsubst %.c,%.d,$(SRCFILES))

The last list is the one with all sources, headers, and auxiliary files that should end up in a distribution tarball.

   ALLFILES := $(SRCFILES) $(HDRFILES) $(AUXFILES)

The next one can take you unawares. When you write a rule for "make clean", and there happens to be a file named "clean" in your workdir, you might be surprised to find that 'make' does nothing, because the "target" of the rule "clean" already exists. To avoid this, declare such rules as "phony", i.e. disable the checking for a file of that name:

   .PHONY: clean dist test testdrivers regtest

If you thought -Wall does tell you everything, you'll be in for a rude surprise now. If you don't even use -Wall, shame on you. ;)

   CFLAGS := -Wall -Wextra -pedantic -Wshadow -Wpointer-arith -Wcast-align \
             -Wwrite-strings -Wmissing-prototypes -Wmissing-declarations \
             -Wredundant-decls -Wnested-externs -Winline -Wno-long-long \
             -Wconversion -Wstrict-prototypes

I suggest you add them one by one instead of all at once. ;)

Now come the rules, in their typical backward manner (top-level rule first):

   all: pdclib.a
   pdclib.a: $(OBJFILES)
           @ar r pdclib.a $?

The '@' at the beginning of the line tells 'make' to be quiet, i.e. not to echo the executed commands on the console. The Unix credo is "no news is good news". You don't get a list of processed files with cp or tar, either, so it's completely beyond me why developers chose to stare at endless lists of compiler invocations. One very practical advantage of shutting 'make' up is that you actually get to see those compiler warnings, instead of having them drowned out by 'make' noise.

   test: testdrivers
           -@rc=0; count=0; for file in $(TSTFILES); do ./$$file; rc=`expr $$rc + $$?`; \
           count=`expr $$count + 1`; done; echo; echo "Tests executed: $$count  Tests failed: $$rc"
   testdrivers: $(TSTFILES)
   regtest: regtestdrivers
           -@rc=0; count=0; for file in $(REGFILES); do ./$$file; rc=`expr $$rc + $$?`; \
           count=`expr $$count + 1`; done; echo; echo "Regression tests executed: $$count  Tests failed: $$rc"
   regtestdrivers: $(REGFILES)

Call it crude, but it works beautifully for me. The leading '-' means that 'make' will not abort when encountering an error, but continue with the loop.

If you get a SEGFAULT or something like that, add a temporary echo $$file to the loop to get the name of the test driver this happens in. For "normal" test failures, add a diagnostic to the test driver.

   -include $(DEPFILES)

This adds all the dependency rules auto-generated by GCC (see below).

   clean:
           -@for file in $(OBJFILES) $(DEPFILES) $(TSTFILES) $(REGFILES) pdclib.a pdclib.tgz; do if [ -f $$file ]; then rm $$file; fi; done
   dist:
           @tar czf pdclib.tgz $(ALLFILES)

Nothing much to say here, really. The awkard-looking loop in the 'clean' rule is to avoid confusing error messages if the files to be deleted don't exist.

   todolist:
           -@for file in $(ALLFILES); do grep -H TODO $$file; done; true

Greps all those "TODO:" comments from my files. Nice to be remembered what's still missing before you do a release.

Now comes the dependency magic I talked about earlier:

   %.o: %.c Makefile
           @$(CC) $(CFLAGS) -DNDEBUG -MMD -MP -MT "$*.d $*.t" -g -std=c99 -I./includes -I./internals -c $< -o $@

Isn't it a beauty? ;-) The bunch of M-flags create a *.d file for the object file created, which defines a dependency of the *.d file itself, the *.t test driver, and the *.o object file, on the source file and any headers it includes. That means that whenever any of those files is touched, the object file, the test driver, and the dependency file itself get recreated. (After all, if the edit was the adding of another #include, the dependency file itself is outdated, too.)

Compiling the object file looks like a side effect of this treat. ;-) Beware, however, that pre-3.3.x versions of GCC are not capable of this stunt.

Note that the dependency list of the rule includes the Makefile itself. If you changed e.g. the CFLAGS, you want them to be applied, don't you?

   %.t: %.c Makefile pdclib.a
           @$(CC) $(CFLAGS) -DTEST -std=c99 -I./includes -I./internals $< pdclib.a -o $@
   %.r: %.c Makefile
           @$(CC) $(CFLAGS) -DTEST -DREGTEST -std=c99 -I./internals $< -o $@

These last two are somewhat of an anticlimax in their "triviality". My test drivers need to link against the PDCLib itself (the regression test drivers don't), but that's already all.

Hope this helps you in creating your own Makefile, and then forgetting about it (as you should, because 'make' should lessen your workload, not add more).

--Solar 07:28, 16 January 2007 (CST)

Category:Tutorial