Writing Makefiles by Hand

Recently, in a fit of insanity, I offered to "write an article or few" on C programming issues. The areas I'd like to concentrate on are tips that will help take those without much experience a step further. My first article is about writing makefiles by hand.

First, I'd better clear up what's what in terminology. A makefile is a textual script describing rules to build programs from source, C being the most common. It contains information about which source and library files are needed to create a program, and can work out exactly how much to recompile when one of the files is updated.

On Unix platforms, makefiles are written in a text editor and run with a command-line tool called make, but in Acorn's C packages, the tool equivalent to make is called amu (probably standing for something like "Acorn Make Utility"), and they provide !Make to automate the building of makefiles. This article applies to Acorn's amu, but the free alternative(s) should use the same syntax, as should Easy C, unless it uses something completely proprietary instead of makefiles.

Filename conversion

You should be aware of how RISC OS C compilers, and similar tools, deal with the difference between RISC OS filenames and Unix/PC filenames. Suffixes (aka extensions) become subdirectories. You can also substitute / for . as the directory separator in the rest of the pathnames to be compatible with Unix. The Acorn Desktop Tools manual describes this in more detail.

Why write makefiles by hand?

If Acorn provide !Make to let you write makefiles with drag & drop etc, why bother learning the syntax and "getting your hands dirty"? The reason is that !Make hides some of the power of makefiles and generates "ugly" makefiles, difficult to read and with much needless duplication of rule information. When you delete a source file and remove it from !Make, it leaves behind dependency information which may cause compilation to fail.

If you create a template makefile, you can use it as a starting point for all your projects by copying it and typing in a few filenames. This is hardly any slower than drag & drop, and it will save time in the long run when you're familiar with the syntax and can easily edit more complicated makefiles as a whole instead of !Make hiding it from you.

Disabling !Make

If we're not going to use !Make any more, it would be more convenient if double-clicking a makefile ran it in amu instead of loading it into !Make. Or you can filetype them as Text and have them load into your editor when double-clicked, and use something like a TaskObey file in the same directory to run them in amu.

To change the run action of the Makefile filetype (&FE1) comment out the Alias$@RunType lines in the !Boot and !Run files of !Make. Or just delete the whole thing altogether if you're feeling bold, but you should copy the appropriate bits to load the sprite and set the File$Type variable somewhere else where they'll get booted. !SetPaths is the ideal place if you have Acorn C/C++, otherwise probably somewhere in your boot sequence.

Create a TaskObey file containing the following, and save it as !Make in !SetPaths or wherever. You don't have to call it "!Make" - it's just the name I use. It could also be Obey instead of TaskObey, but I like to have compilations running in a TaskWindow.

RMEnsure DDEUtils 0.00 RMLoad System:Modules.DDEUtils
Set Alias$@RunType_FE1 <Obey$Dir>.!Make %%*0
amu -desktop -f %0 %*1

Also, add the second line of that to !SetPath's !Boot and !Run files. Now double-click !SetPaths and everything should be set up.

The first line loads the DDEUtils module which is needed for Throwback and to deal with long command lines. The second resets the filetype run action to run this file, because DDEUtils seems to change it back to !Make when it initialises. The third line runs the makefile in a TaskWindow. I'm not sure why I wrote %0 %*1 instead of %*0; it was either absent-mindedness or for a good reason. Just in case it was the latter, I'll leave it like that!

amu command line syntax

The syntax is:

  amu [options] [target1 {target2...}] [macrodefn1 {macrodefn2...}]

Options are:

  -f filename     use filename as Makefile

  -i              ignore return codes

  -k              continue after errors

  -n              don't execute

  -o filename     write commands to filename

  -s silent

  -t stamp

If you don't use the -f option, amu will look for a file called "Makefile" in the current directory. Usually, when one of the commands executed by amu generates an error, amu halts at that point. The -i causes it to continue as if there were no error, and -k causes it to continue with rules that don't depend on the failed one. The -n makes it just print the commands it would execute without executing them, the -o makes it print the commands into a file, and the -s stops it from printing command; usually, it prints each one just before executing it. Finally, the -t makes it stamp all the targets and source files, so the project can be seen to be up to date.

If you omit targets, amu uses the first one in the makefile.

Macro definitions take the form VARIABLE=value and have the same effect as if they were set in the makefile, which I'll explain below. If you want spaces in a macro defined on the command line, enclose the definition in quotes.

When amu is running a makefile, the current working directory is set to the directory the makefile is in, and it is usual for the makefile to refer to all files relative to this.

Syntax of a makefile

If you're reading this because you have trouble understanding the makefile section in the Acorn C manuals (first appendix of the Desktop Tools manual in Acorn C/C++), you probably won't understand the following sections of this article either, until you get to the example. So you might like to skim through as far as the example, then refer back to these sections for more details when you understand the basics.

A makefile consists of a series of text lines which can be macro definitions, rule prerequisites and commands, or comments. Blank lines are ignored. Anything after a # character, anywhere on a line, is a comment. Long lines can be split with the \ character i.e. if the last character on a line is a \, make treats the following line as a continuation of the first, swallowing the \. This feature is usable in comments.

Macros (variables)

Macros, also called variables, aren't the most vital feature of makefiles, but they're very useful and usually appear before anything else, so I'll describe them first. A macro definition is a line of the form:

MACRO_NAME = macro value

To be on the safe side, apply the same rules to characters in macro names as you do to C variable names. Macro names are usually upper case, but some people advise using lower case for those that are only for internal use in the makefile.

Leading and trailing spaces in the macro value are ignored, but any in the middle are included.

Macros defined in the makefile can be overridden by defining them on the command line to Unix versions of make, but this doesn't seem to work in amu, where versions defined in the makefile less usefully take priority.

To dereference a macro (i.e. substitute its value), use $(MACRO_NAME) or ${MACRO_NAME}. You can use dereferenced macros as part of the definitions for other macros. If a macro doesn't exist, dereferencing it produces a blank without generating an error.

Rules

A rule consists of:

Targets:     Prerequisites
             Commands

Targets and prerequisites are files, although a common trick is to use a target name which never exists as a file. As long as it's a valid filename, it doesn't have to exist. A target name must have no leading spaces. A prerequisite is a file, such as a source file, which is used to build the target. It is quite common to have multiple prerequisites in one rule.

In this article, I'll only consider rules with one target. When amu builds a target, it finds all the rules that refer to it. Apart from double-colon rules, which are beyond the scope of this article, only one rule per target can have commands. It checks all the prerequisites in the rules, and if any of them are newer than the target, or the target doesn't exist, it uses the commands to build the target. Prerequisites can be targets themselves, so if rules exist for them, amu satisfies those rules before executing any commands of the rule they're prerequisites of.

It follows that you can have rules with no commands, in which case the prerequisites' rules are checked without building the target, or rules without prerequisites, in which case the commands are always executed. Rules with neither commands nor prerequisites are meaningless.

You can have as many commands in a rule as you like, executed sequentially, but each one must begin with a tab. Acorn amu allows spaces instead of tabs, but this is incompatible with Unix. SrcEdit can't handle tabs properly, but you shouldn't be using that!

Note that the Acorn Desktop Tools manual claims you can't use a macro to specify the first target, but I've often broken this rule with no errors being generated.

Dynamic dependencies

Acorn amu, cc, c++ and objasm (the compilers) have a special feature to automatically generate dependency data between object files (created from source) and headers. The compilers generate a file of dependency rules which amu tacks onto the makefile at each step. This comment:

# Dynamic dependencies:

is used by amu as a marker, so make sure you include it at the bottom of any makefiles you write.

Predefined variables

These variables have a special meaning. You can't define them, and note that no braces are used when dereferencing them:

$@     Current target

$*     Current target with suffix stripped

$<     Current prerequisites

$?     Current prerequisites which are newer than target

You'll probably only be commonly using $@ and $<.

VPATH

Another special variable which can be defined, but isn't intended to be dereferenced by the user, is VPATH. It contains a list of directories where files should be looked for if they aren't in the current directory. Remember that certain subdirectories are treated as filename extensions. Under Unix, multiple paths are separated by colons. Normally, commas are used to separate RISC OS paths, but in amu VPATH spaces are used. In addition, normal RISC OS paths include a trailing '.', but ones in VPATH don't.

Default rules

Most programs are built from a large number of files in mostly the same language and with the same suffix. Makefiles allow you to write default rules for files with a given extension to save you repeating similar rules over and over. The predefined variables are essential here.

To use default rules, you must first use the pseudo-rule .SUFFIXES to indicate which suffixes you're using. This will look something like:

.SUFFIXES: .o .c .c++ .s

Then you need a rule for each source suffix. The target for the rule is the source (prerequisite) suffix followed by the target suffix, e.g.:

.c.o:               # No specific prerequisites
     cc -o $@ $<    # Simplest possible compiler command

Example

I've supplied an example of a template makefile which you can embellish to suit most simple projects. Just insert the object files at the top, and any further libraries you want to use besides Stubs. And yes, I did write insert object files, not source files. This is because the main target is made by linking the object (.o) files. Further rules in the makefile tell it how to make the object files from source. It doesn't matter if you're not using all of c++, c, and s (assembler), because any rules that aren't needed are simply ignored.

In particular, notice the extensive use of macros to separate out different concepts, making it easier to find and alter various parameters.

OBJECTS    =
# Insert your object files here. It looks neatest if you use one per
# line with a backslash at the end of each to make one logical line.

LIBS       =    C:o.Stubs
# Insert any further libraries you need eg tboxlibs, DeskLib at this point.

INCLUDE    =    C:
# Include path for C.

TARGET     =    !RunImage
# Name of master target.

ASMFLAGS   =    $(ASMEXTRA) -Stamp -NoCache -CloseExec \
                -Quit -throwback
CCFLAGS    =    $(CCEXTRA)  -fahi -depend !Depend -throwback \
                -I$(INCLUDE)
CPPFLAGS   =    $(CPPEXTRA) -depend !Depend -throwback \
                -I$(INCLUDE)
LINKFLAGS  =    $(LINKEXTRA)
SQUEEZEFLAGS =  $(SQUEEZEEXTRA)
# Flags for the various tools. These can be overriden on amu's
# command-line, or you can use the *EXTRA variables to add to them.
# You should look up the meanings of the flags in the manuals.

ASM        =    objasm $(ASMFLAGS)
CC         =    cc -c $(CCFLAGS)
CPP        =    c++ -c $(CPPFLAGS)
LINK       =    Link -aif $(LINKFLAGS)
SQUEEZE    =    Squeeze $(SQUEEZEFLAGS)
# Skeleton commands

all: $(TARGET)
# Using this as the default rule works around the (apparently wrongly)
# documented restriction of not being able to use a macro as the first
# target. "all" is a very common target name.

$(TARGET):    $(OBJECTS) $(LIBS)
              $(LINK) -o $@ $(OBJECTS) $(LIBS)
              $(SQUEEZE) $@
# This is what we actually want to build, hence we list .o files
# instead of source files above.
.SUFFIXES:.o .s .c .c++
.s.o:
              $(ASM) -from $< -to $@
              
.c.o:
              $(CC) -o $@ $<
              
.c++.o:
              $(CPP) -o $@ $<
              
# Dynamic dependencies:

In another series of articles I wrote for Archive years ago, I used FormText. So I'm using it again with a makefile constructed from the above template, so you can see for yourself how a complete makefile typically looks. Have a look at the Makefile in Example 1 on the monthly disc. To compile it yourself, you may need to change your C$Path (C:) or the INCLUDE macro in the makefile. The latter is the better way to sort out any problem, but this may leave the dynamic dependencies wrong for your setup; so you should delete them all and let them be rebuilt. Please ignore any compilation warnings; I wrote that code before I knew how to write good quality C! Improve the code as an exercise if you like. Do try running the Makefile, then see how it adds the dynamic dependencies.

At this point, you should probably go back and read this article again and/or the Makefile syntax appendix in Acorn's Desktop Tools manual.

Using VPATH for multiple builds

Example 2 shows how you can use some of the more advanced features. I've stripped out all the files which aren't necessary to demonstrate compilation. I won't describe it in detail here, but double- clicking !MakeNorm produces a normal !RunImage, and !MakeDebug produces one to be run in the debugger. The object (.o) files for each version are kept separately so that they don't all need to be replaced when switching from one build type to another. Study the files in these pseudo-applications to see how they make use of VPATH and pass extra flags on the amu command-line.


Source: Archive Magazine - 12.10
Publication: Archive Magazine
Contributor: Tony Houghton