Makefile: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
(adding some partitioning to the document to support skimming; syled commands, special words, or anything that is important down to the letter;)
m (Tutorial:Makefile moved to Makefile: Moving tutorials to main namespace)
(No difference)

Revision as of 03:04, 18 May 2007

Makefile Tutorial

Setting up a simple Makefile is easy. You can even go a long way without a Makefile, simply by using make's internal rulesets, however setting up a global project Makefile that works as intended is quite a bit harder.

Overview

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":

Non-Source Files

   # 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.

Project Directories

    PROJDIRS := functions includes internals

Those are my subdirectories. It could be your sub projects, or whatever. I could use similar "auto detection" 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!

Recursion

    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.

Specialized Automated Testing

    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, encapsulated 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.

Object Files And Test Driver Executables

    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.)

Dependencies

    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.

Phony

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

The next one can take you by surprise. When you write a rule for make clean, and there happens to be a file named clean in your working directory, 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

CFLAGS

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. ;)

Rules

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 awkward looking loop in the clean rule is to avoid confusing error messages if the files to be deleted don't exist.

Extracting TODO Statements

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

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

Dependency Magic

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.

A Final Word

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)