Makefile

From OSDev.wiki
Revision as of 15:32, 20 August 2008 by Solar (talk | contribs) (→‎File Lists: As in "man make", chapter "Description".)
Jump to navigation Jump to search

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.

The 'make' manual will tell you about the hundreds of things you can do with a Makefile, but it doesn't give you an example for a good Makefile. The following examples cover mainly the PDCLib Makefile, and show some of the "tricks" that may not be 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.

File Lists

It is best practice to name your makefile Makefile (without an extension), because when make is executed without any parameters, it will by default look for that file in the current directory.

First, I assemble various "file lists" which I need later in the Makefile.

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. I create lists of the sources and headers anyway, but for auxiliary files which are not used anywhere else in the Makefile, 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 (which is correct, but not very helpful). I now have a list of all source and header files in my project directories.

The -mindepth option keeps any top-level files out of the result set, like tmp.c or test.c or whatever you might have created for testing something on the fly. The -maxdepth option stops the 'find' to recurse into the CVS/ directories I used to have. You might want to adapt this to your requirements.

Automated Testing, pt. 1

I think it's time I explain my approach to testing. I write strictly one library function per source file. Then, I add a test driver to that source file:

#ifdef TEST
int main()
{
    /* Test function code here */
    return NUMBER_OF_TEST_ERRORS;
}
#endif

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 executable for that function. Returning the number of errors allows me to do a grand total of errors encountered (see below).

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. TSTFILES does the same for the extension *.t, which I chose for my test driver executables.

Dependencies, pt. 1

Many people edit their Makefile every time they add/change an #include somewhere in their code. 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 I made end in *.d), which contains a Makefile dependency rule listing that source file's includes. (And more, but see below.)

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

Distribution Files

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)

Phony

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. ;) These flags are for C, there are more to be used for C++.

Rules

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

Top-Level Targets

    all: pdclib.a

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

    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)

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 have their Makefiles churn out endless lists of useless garbage. One very practical advantage of shutting up make is that you actually get to see those compiler warnings, instead of having them drowned out by noise.

The awkward looking loop in the clean rule is to avoid confusing error messages if the files to be deleted don't exist.

Automated Testing, pt. 2

    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)

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.

Dependencies, pt. 2

    -include $(DEPFILES)

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

Extracting TODO Statements

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

This will grep all those TODO and FIXME comments from the files and display them in the terminal. It's nice to be remembered of what's still missing before you do a release. To add another keyword, just add another -e keyword. Don't forget to add todolist to your .PHONY list.

Dependencies, pt. 3

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? ;-) Note that this needs GCC 3.3.x or newer.

The bunch of "M"-flags create a *.d file alongside the object file, which holds (in Makefile syntax) rules making all the generated files (*.o, *.t, *.d) depend on the source file and any headers it includes. That means the object file, the test driver, and the dependency file itself get recreated automatically whenever relevant sources are touched. (The dependency file requires recreating too as the source edit could have added another #include.)

Compiling the object file actually looks like a side effect. ;-)

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?

The Rest

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

This is somewhat of an anticlimax in its "triviality". My test drivers need to link against the PDCLib itself, but that's already all. This is not really perfect; if any PDCLib function was touched (and, thus, pdclib.a updated), it recreates all test drivers, even when not necessary. Ah well, no Makefile ever is perfect, but I'd rather have too much compiled than missing a dependency.

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 04:05, 21 January 2008 (CST)

See Also

External Links