Makefile: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
m (→‎What is a Makefile?: Spelling corrections)
m (→‎Basic Example: Removed an incorrect ld flag)
Line 44: Line 44:
gcc -c main.c -o main.o -Wall
gcc -c main.c -o main.o -Wall
gcc -c ports.c -o ports.o -Wall
gcc -c ports.c -o ports.o -Wall
ld -o kernel.o main.o ports.o -i -e _main -Ttext 0x7E00
ld -o kernel.o main.o ports.o -e _main -Ttext 0x7E00
objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin
objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin
</pre>
</pre>

Revision as of 19:30, 19 July 2010

Difficulty level

Beginner

What is a Makefile?

A Makefile is a file which controls the 'make' command. The 'make' command is available with most C toolchains (such as GCC) and is used to aid in build process of a project. Once the makefile has been written for a project, the 'make' command can easily and efficiently build your project and create the required output file(s).

Make can read dependancy information from your makefile and ensure that files are built in the correct order, and can even detect when source files or other dependencies have been updated, and can perform only the build steps that are required.

Your makefile can also contain other commands (or "targets") to remove built files (usually referred to as 'clean') or to create an archive of your project, ready for release.

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. It aims at GNU 'make', and uses some expressions specific to that particular 'make' implementation (for example the patsubst function). Adaption to any special needs you might have should not be too hard once you got the idea, however.

The Makefile creates only one project-wide linker library, but it should be easy to expand it for multiple binaries/libraries.

Basics

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. It also makes the file show up prominently on top of listings, at least on Unix machines.

Generally speaking, a makefile consists of definitions, and rules.

A definition declares a variable, and assigns a value to it. Its overall syntax is VARIABLE := Value.

Note: Frequently, you will see declarations with "=" instead of ":=". That is usually a typo; such a variable declaration will result in the assignment being evaluated every time the variable is used. Unless you know exactly what you are doing, use ":=" for variable declarations.

A rule defines a target, 0..n dependencies, and 0..n commands. The general idea is that make checks if the target (file) is there; if it isn't, or any of the dependencies is newer than the target, the commands are executed. The general syntax is:

target: dependency
        command

Note that the command must be tab-indented. If your editor environment is set to replace tabs with spaces, you have to undo that setting while editing a makefile.

What makes makefiles so hard to read, for the beginner, is that we are not looking at an imperative program here that is executed top-down; 'make' reads the whole makefile, and then hops from rule to rule to satisfy whatever target you gave it on the command line.

I won't go into further details. This is not a man page, but a tutorial, so I will show you how a makefile is build, and the ideas behind each line.

Basic Example

Lets say you currently build your OS (or other project) using a batch file that looks like this:

gcc -c main.c -o main.o -Wall
gcc -c ports.c -o ports.o -Wall
ld -o kernel.o main.o ports.o -e _main -Ttext 0x7E00
objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin

You can start out into the world of makefiles by creating a file named "Makefile" that contains the following:

all:
	gcc -c main.c -o main.o -Wall
	gcc -c ports.c -o ports.o -Wall
	ld -o kernel.o main.o ports.o -e _main -Ttext 0x7E00
	objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin

Here we are creating a single rule to build a target called "all" that has no dependencies, and providing the list of commands to build this target. With this file in your source directory, executing the command "make" will then find this file, and compile your OS (or other project). (Note specifically that all the commands must start with a single tab character, not spaces.)

Special macros

There are several special predefined macros, of which the following are the most useful:

$@    # the target of the rule
$<    # the first dependency
$^    # all dependencies
$?    # all *changed* dependencies
$+    # all dependencies, preserving duplicates and ordering

Once you get more familiar with 'make', check out the others in the make manual.

File Lists

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: all clean dist test testdrivers todolist

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:
            -@$(RM) $(wildcard $(OBJFILES) $(DEPFILES) $(TSTFILES) $(REGFILES) pdclib.a pdclib.tgz)

    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 $*.o" -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? Using the $< macro ("first dependency") makes sure we do not attempt to compile the Makefile as C source.

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.

Advanced Techniques

Conditional Evaluation

Sometimes it becomes useful to react on the existence or content of certain environment variables. For example, you might have to rely on the path to a certain framework being passed in FRAMEWORK_PATH. Perhaps the error message given by the compiler in case the variable is not set isn't that helpful, or it takes long until 'make' gets to the point where it actually detects the error.

Luckily, 'make' allows for conditional evaluation and manual error reporting, quite akin to the C preprocessor:

    ifndef FRAMEWORK_PATH
        $(error FRAMEWORK_PATH is not set. Please set to the path where you installed "Framework".)
    endif

    ifneq ($(FRAMEWORK_PATH),/usr/lib/framework)
        $(warning FRAMEWORK_PATH is set to $(FRAMEWORK_PATH), not /usr/lib/framework. Are you sure this is correct?)
    endif

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