Makefile: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
m (→‎Basic Example: Removed an incorrect ld flag)
(Started reworking the article after reworking the PDCLib Makefile - some things not quite correct in here, will continue tonight.)
Line 2: Line 2:


= What is a Makefile? =
= 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).


A Makefile is a file which controls the 'make' command. Make is available on virtually any platform capable of C development, and is used to control the build process of a project. Once a Makefile has been written for a project, make 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.


Make reads dependency information from your Makefile, and ensure that files are built in the correct order. When source files or their dependencies have been updated, make can rebuild any affected output files - and only those, as compared to "build batches" that always rebuild the whole project.
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.

While the building of a project is done with nothing more than typing 'make', a Makefile offers many other features (called "targets"), which can be invoked by 'make <target>'. A frequently-used target is 'make clean', which removes any temporary files, or 'make dist', which prepares a project for release.

But 'make' requires a well-written Makefile to do these things efficiently and reliably.


= Makefile Tutorial =
= 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.


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.
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 [http://sf.net/projects/pdclib 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 '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 mainly use the [http://pdclib.rootdirectory.de/trac.fcgi/browser/trunk/Makefile PDCLib] Makefile as a template, and show some of the "tricks" used therein that may not be that obvious to the make beginner. It aims at the GNU variant of 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.
The Makefile creates only one project-wide linker library, but it should be easy to expand it for multiple binaries/libraries.
Line 17: Line 21:
== Basics ==
== Basics ==


It is best practice to name your makefile <code>Makefile</code> (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.
It is best practice to name your makefile <code>Makefile</code> (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 directory listings, at least on Unix machines.


Generally speaking, a makefile consists of definitions, and rules.
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''.
A ''definition'' declares a variable, and assigns a value to it. Its overall syntax is <code>VARIABLE := Value</code>.


'''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.
'''Note:''' Frequently, you will see declarations with "=" instead of ":=". That is usually in error; 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:
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:
Line 34: Line 38:
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.
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.
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, building a dependency tree, and then resolves the dependencies by hopping from rule to rule until the target you gave it on the command line has successfully been resolved.


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.
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:
<pre>
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
</pre>

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

<pre>
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
</pre>

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 ==
== File Lists ==
Line 77: Line 47:


=== Non-Source Files ===
=== Non-Source Files ===

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

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.
Further down I have a target ''"dist"'', which packages all required 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 but should still end up in the tarball, I need this explicit list to have them included.


=== Project Directories ===
=== Project Directories ===

Revision as of 07:44, 1 August 2010

Difficulty level

Beginner

What is a Makefile?

A Makefile is a file which controls the 'make' command. Make is available on virtually any platform capable of C development, and is used to control the build process of a project. Once a Makefile has been written for a project, make can easily and efficiently build your project and create the required output file(s).

Make reads dependency information from your Makefile, and ensure that files are built in the correct order. When source files or their dependencies have been updated, make can rebuild any affected output files - and only those, as compared to "build batches" that always rebuild the whole project.

While the building of a project is done with nothing more than typing 'make', a Makefile offers many other features (called "targets"), which can be invoked by 'make <target>'. A frequently-used target is 'make clean', which removes any temporary files, or 'make dist', which prepares a project for release.

But 'make' requires a well-written Makefile to do these things efficiently and reliably.

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. 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 mainly use the PDCLib Makefile as a template, and show some of the "tricks" used therein that may not be that obvious to the make beginner. It aims at the GNU variant of 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 directory 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 in error; 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, building a dependency tree, and then resolves the dependencies by hopping from rule to rule until the target you gave it on the command line has successfully been resolved.

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.

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 required 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 but should still end up in the tarball, I need this explicit list to have them included.

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