Wmake tutorial

From Open Watcom

Revision as of 11:17, 23 September 2006; view current revision
←Older revision | Newer revision→
Jump to: navigation, search

Contents

Why Makefiles?

A surprisingly large percentage of programmers couldn't write a makefile if their life depended on it. That is unfortunate -- for them, because makefiles can be tremendously useful. I believe there are two major reasons why so many programmers consider them mysterious, if not outright black magic.

The first reason is that IDEs remove the need for makefiles when the development tasks are simple; then laziness sets in and programmers are reluctant to learn a new way of doing things even when IDEs may no longer be adequate.

The second reason is that makefiles require a completely different programming style. Programmers skilled in C, C++, Java, Pascal, Fortran, Basic, etc. know the imperative style of programming. A program written in this style says "first do this, then do that, then do this". Makefiles use declarative programming, where the programmer only says "to arrive at this, do that". A makefile contains a number of such statements, but their order is largely irrelevant. Makefiles have no set structure like programs in the above mentioned languages. This takes a while to get used to.

Despite these obstacles, makefiles are well worth learning. They are very widely used and the ability to understand and modify a makefile is often handy. Makefiles are also tremendously flexible and can be used for a wide variety of purposes.

Makefile Basics

To understand how makefiles work, knowledge of the basic concepts is of course necessary.

Targets

The most important element of makefiles is a target. Every makefile must have at least one (but usually contains many) -- everything else is optional. A target in a makefile is a, well, target that the make utility will attempt to arrive at, the fruit of make's efforts. A target is typically a file (executable, object file, or any other file) but doesn't have to be. It is often helpful to use 'virtual' (symbolic) targets to structure makefiles.The most basic target syntax is as follows:

<target> :

That is, the name of the target followed by a colon. Important note: Target name must start in the first (leftmost) column. Any text that does not start in the first column is understood to be a command (commands will be explained shortly). Make sure there is no whitespace before the target and everyone will be happy.

Commands

Obviously, targets by themselves aren't very useful. Usually there is a command list associated with each target. The syntax is as follows:

<target> :
    <command>
    [<command> ...]

Some make utilities require that the command be preceded by a hard tab. More user-friendly make tools (including wmake) only require one or more spaces. The commands can be any commands that could be entered on the command line and will be executed through the shell (therefore, features such as redirection or pipes can be used). A command is often an invocation of a compiler, linker, assembler, librarian, etc. but can be anything. Keep in mind that makefiles are not restricted to programming and many other tasks can be automated with makefiles. A trivial makefile could look like this:

target :
        echo My first makefile

If you save the above in a plain text file called 'makefile' and run wmake in that directory, you will see the following output (after the copyright header):

        echo My first makefile
My first makefile
Error(F38): (target) does not exist and cannot be made from existing files
Error(E02): Make execution terminated

This output is worth dissecting. The first line displays the command that wmake is about to execute. This is useful when the command produces no output and you want to know what's going on. The second line is the actual output of the echo command. The third line is wmake specific; wmake by default expects that every target is a file and checks for its existence after executing the command list. To avoid the error, you can either use the -c switch which prevents the file checks, or much better, use the .SYMBOLIC directive which tells wmake that a target is not a file and merely symbolic. Like this:

target : .symbolic
        echo My second makefile

Now let's see what happens if we create two targets in a makefile:

first : .symbolic
        echo The first target

second : .symbolic
        echo The second target

What happens if you run wmake on this makefile? Only the first target is updated. The second target is seemingly completely ignored. It may be worth remembering that there is a way to specify which target should be updated on the make utility command line. The syntax is very simple:

wmake <target>

If you run

wmake second

you will see that command list for the second target was executed (but not for the first!). In other words, the target 'second' was updated.

Prerequisites

So far makefiles may not seem to be useful. If all they could do was run lists of commands, no one would need them. But things start to get interesting when prerequisites (aka dependents) are introduced. We already know that a target can be associated with a list of commands. But it can also have a list of prerequisites specified, using the following syntax:

<target> : <prerequisite> [<prerequisite>] ...

Prerequisites need to be updated first, before the target on the left hand side can be updated. To do that, a make utility turns the prerequisites into targets and tries to update them. Let's tweak the second makefile a little bit:

first : second .symbolic
        echo The first target

second : .symbolic
        echo The second target

Now the target 'second' became a prerequisite of 'first', or in other words, 'first' is dependent on 'second'. Try running wmake on this makefile and use the -s switch; it stands for "silent" and suppresses echoing of commands that are about to be executed. The result will be:

The second target
The first target

You can see that although the 'first' target is listed at the top of the makefile, 'second' was updated first. The lists of prerequisites can be arbitrarily long and will be processed in left-to-right order.

The target/prerequisites/commands construct is called a rule. A makefile is essentially a sequence of rules.

Special Macros

To ease makefile creation and maintenance, make utilities support text replacement through a simple macro language. We'll discuss macros in general later, but now let's take a look at special macros. Special macros are used in rules and their value varies with the target and prerequisites. All macros start with the dollar sign ($). The most basic ones are:

$@
Full name of the target
$*
Target name without extension (suffix)
$?
List of dependents younger (newer) than the current target
$<
List of all dependents

NB: The $< macro has somewhat different meaning in many other make utilities.

Let's see what these macros do.

test.c : second .symbolic
        echo Updating $* because of $?

second : .symbolic
        echo The second target

The output of the above will be:

The second target
Updating test because of second

The special macros obviously work -- but so far they don't seem all that useful. Their usefulness might become more apparent in the next step.

Implicit Rules

Things get a lot more interesting when implicit rules, also known as inference rules, are thrown into the mix. A general form of implicit rule is as follows:

.<ext1>.<ext2> :
    <command>
    [<command>...]

This rule reads: to arrive at a file with extension (suffix) 'ext2', look for a file with the same name but extension 'ext1', and then run 'command'.

A makefile with an implicit rule might look like this:

.c.obj :
    wcc386 -zq -oaxt $<

hello.obj :

Note that the implicit rule is not a target, but hello.obj is. There is no command list, in other words there is no explicit rule associated with hello.obj. Therefore wmake will examine implicit rules and attempt to arrive at hello.obj through them. To see anything here, you might want to create a valid C source file called hello.c. If you do, and run wmake on the makefile, you should see:

wcc386 -zq -oaxt hello.c

You can see that the $< macro got expanded into a useful value -- implicit rules wouldn't be possible without special macros.

Timestamp Checks

There is another important observation to be made with the above makefile. If you run wmake again, nothing happens. That is, wmake takes a look at the timestamp of hello.obj and compares it against the hello.c timestamp. Unless your computer's clock is seriously off, hello.obj will be newer and wmake will decide that it is up to date already and there is no need to run the commands to update it. This is perhaps the most crucial feature of a make tool and the real time saver.

Macros

Let's take a small detour and look at macros. We already know how macros are dereferenced (remember the dollar?). The basic syntax for defining a macro is as follows:

<name> = <value>

For instance

foo = bar

will define a macro 'foo' with the value 'bar'. Note that whitespace around the equals sign is optional and will be discarded. To dereference the macro, write $(foo). An almost trivial example of macro usage is the following makefile:

foo = $(bar)
bar = hello
tgt = goal
cmd = echo

$(tgt) : .symbolic
    $(cmd) Target $@, $$(foo) = $(foo)

Run through wmake -s, the output will be:

Target goal, $(foo) = hello

Once again, the makefile is worth a closer look. First off, macros can refer to other macros. Second, macros are not evaluated immediately. That's why 'foo' can refer to 'bar' before 'bar' was defined. Since 'foo' will only be evaluated when the command list is executed, that is okay. Third, macros can be used in rules to specify targets, prerequisites (not shown above), and commands. Fourth, the $$ macro expands to a single dollar sign. Therefore, $$(foo) has nothing to do with the macro 'foo', it's simply a dollar sign followed by the text "foo" in parentheses.

Comments

Like any other code, makefiles benefit from judicious use of comments. Comments in makefiles are simple, as there is only one type of comment. It starts with the octothorpe (#) and continues until the end of the line. That is, starting with the '#', wmake will ignore the entire rest of a source line. Note that the octothorpe will be recognized even within quotes etc. If you want to use a literal '#', write $# -- you should recognize that as a special kind of macro. Example:

all : .symbolic  # this is a comment
    echo $# no comment

The above makefile will, predictably, print

# no comment

Putting it All Together

Now it's time to build a real and useful makefile. To demonstrate the capabilities of a make tool, we'll need three files. The first is called hello.c:

#include "header.h"

int main( void )
{
    func();
    return( 0 );
}

It contains the main() function and calls a function in another module. That module is called other.c and looks like this:

#include <stdio.h>
#include "header.h"

void func( void )
{
    printf( "makefile must have worked\n" );
}

The two modules share a common header file called (you guessed it) header.h, which is not very long at all:

extern void func( void );

And here comes the makefile... applause please...

CC = wcc386
CFLAGS = -zq
LINKER = wlink
LFLAGS = option quiet

OBJS = hello.obj other.obj

.c.obj : .autodepend
    $(CC) $(CFLAGS) $<

test.exe : $(OBJS)
    $(LINKER) $(LFLAGS) name $@ file { $< }

The macros at the beginning specify the tools and flags to be used during builds. The inference (implicit) rule tells wmake how to produce object files from C source files, and says that automatic dependency checking should be performed by wmake. the OBJS macro contains the list of object files. The sole target is called test.exe (note that this name is independent of the object file names), depends on the object files, and will be updated via the linker. Note the use of special macros to specify target name and list of dependents. The curly braces are specific to wlink and denote a whitespace-separated list (by default wlink expects comma-separated lists).

If you run wmake on this makefile and have created the source files listed above, you should see the following output from wmake:

        wcc386 -zq hello.c
        wcc386 -zq other.c
        wlink option quiet name test.exe file { hello.obj other.obj }

The object files were created first, because they are prerequisites of the test.exe target. The test.exe executable was linked as the last step. If you run wmake again, nothing will happen because all targets are already uptodate.

Now what happens if you modify the source files? You can use wtouch or your favourite touch utility to update the timestamp of the source files. If you touch other.c, wmake will rebuild other.obj and relink the executable, but will leave hello.obj alone. If you touch hello.h, both object files will be rebuilt because both source files include hello.h; that's automatic dependencies at work - hello.h is mentioned nowhere in the makefile, but wmake knows the object files depend on it thanks to the information that was stored in the object files by the compiler.

Cleaning Up

What if you want to rebuild everything from scratch, even if targets are up to date as far as wmake is concerned? There are two ways. The first is the -a switch of wmake which forces an update of all targets. A much cleaner (and more flexible) way is to define a 'clean' target. Note that the name is purely conventional and if you want to call it 'Armageddon' or 'daffodil', you can -- all you risk is ridicule. A rule to clean up a project might look like this:

clean : .symbolic
    rm -f *.obj *.exe

The rm used here is modeled after the classic UNIX tool and the -f switch ensures that no errors will be reported if the files don't exist. To clean up a project, you need to (conceptually speaking) update the 'clean' target. If you still remember the preceding paragraphs, that is done via running wmake clean. If you add a 'clean' target, make sure it's not the first target in a makefile; a makefile that by default deletes all targets tends to be only marginally useful.

Conditional Builds

What if you want to build the source files in a slightly different way? It is common that developers need debug and release builds. Using the preprocessor is a good solution here. Add the following to the makefile just before the inference rule:

!ifdef debug
CFLAGS += -d2
LFLAGS += debug all
!else
CFLAGS += -oaxt
!endif

Depending on whether a macro called 'debug' exists or not, parts of the makefile will be either processed or ignored. The += syntax directs wmake to append text to an existing macro (or define it if it didn't exist).

To easily select which type of build should be performed, you can define (or not) the 'debug' macro on the command line. You can run wmake debug=1 to do a debug build -- note the lack of spaces in the macro definition.

Also note that because the !ifdef directive only checks for the existence of a macro, its contents are irrelevant. Therefore, wmake debug=no would be a mildly confusing way to specify that yes, a debug build should be performed.

Complete Makefile

For reference, here is the complete makefile again in all its glory:

CC = wcc386
CFLAGS = -zq
LINKER = wlink
LFLAGS = option quiet

!ifdef debug
CFLAGS += -d2
LFLAGS += debug all
!else
CFLAGS += -oaxt
!endif

OBJS = hello.obj other.obj

.c.obj : .autodepend
    $(CC) $(CFLAGS) $<

test.exe : $(OBJS)
    $(LINKER) $(LFLAGS) name $@ file { $< }

clean : .symbolic
    rm -f *.obj *.exe

Feel free to adapt it for your own projects. Keep in mind that compilers and linkers are not the only tools that a makefile can run -- in general, any tool that can be executed from the command line can be run from a makefile. A makefile can easily build a product using several different compilers, packaging the executables into an installer, and copying or uploading the finished installer to a web server -- all without requiring the typing of a single command (beyond the initial 'make', though that might be automated as well) or a mouse click.

Learning More

How to learn more about makefiles? Besides reading available documentation, the best approaches are the same as with programming in general: examine other people's makefiles, and write your own. It is possible to learn makefile programming entirely by osmosis, although it's probably not the fastest way.

As mentioned above, the hardest part about makefiles is probably the radically different programming style. But once you learn what's a target, what's a dependent, what's a command list, and what's a rule, you should be able to understand moderately complex makefiles as well as write your own.

For an advanced application of makefiles, look for instance at the Open Watcom build system, the Linux kernel build system, or other medium or large projects. With a bit of experience, it is possible to create makefile-based build systems that are usable with a number of different compilers and across multiple operating systems, with a wide range of capabilities.

Personal tools