GNU Make Fundamentals

Concepts and Principles of Program Building

Although newer build systems exist, GNU Make is ubiquitous, and it does not hurt pro­gram­mers to be, at least superficially, aware of the fundamental concepts and features of Make. This article does not delve into the more advanced features, but should never­the­less provide enough material for small and medium projects. The examples are all for C99 (with commands using GNU’s gcc).
PREREQUISITES — You should already…
  • have some programming experience, preferably in C.
  • have basic command line skills (Windows or POSIX).

The Need

Many development steps in the compilation of programs, can be broken down into a sequence of smaller steps. This is particularly true in C/C++ development, since these compilers can only com­pile one file at a time, even when programs consist of several source files; and only create object files, which in turn means that a linker must be used to create the final executable.

Practically, this means C/C++ developers use IDEs or compiler drivers to call the preprocessor, call the code generation program(s) of C/C++, and finally call the linker. A compiler driver, like gcc or g++, does allow one to specify several source files, but this is not very efficient, if only one source file in a program has changed.

The make utility can be given a Makefile (naming convention), in which the program has specified the structure of the program, and the commands that must be run to produce inter­me­diate files and final result. The Make utility will then only perform the necessary commands, and skip those that are not required.

Structural Elements

A Makefile has a certain syntax, and overall achitecture. The major, and most common, parts of this structure is explained here. You can read more in the GNU Make Manual.

Comments & Line Continuation

A Makefile may contain comments, and follow POSIX scripting language convention. The hash (#) character is used to start a single line comment, where all following text is ignored until the end of the line.

Long lines can be continued on the next, by ending the current line with a trailing backslash (\). This can be repeated for several lines, if necessary.

Macros or Variables

Make can read environment variables, but one can create variables in the Makefile, which it calls macros. They can be created or modified by assignment:

Spaces sur­round­ing the equal sign are not significant. The expression on the right of the as­sign­ment may be blank. This will either create the macro name on the left, or clear its cur­rent con­tents.

You can append values to an existing macro with +=. If the macro does not exist at that point, it will be equivalent to assignment:

Unlike POSIX shells, the contents of variables (macros) must be retrieved with parenthesis sur­round­ing the names, prefixed with $, e.g.: $(‹macro›). Only macro names consisting of a single letter, do not require the surrounding parentheses.

Targets, Dependencies and Rules

The make file syntax allows one to specify the target file, which is dependend on one or more files (refered to as the target’s dependencies). To ensure that the target is up to date with respect to its dependencies, i.e., the target has the newest date, any number of command lines, called recipes by GNU Make, can be executed.

One idiosyncrasy of GNU’s Make, is that all the recipe lines must start with a hard tab character — just indenting the line with spaces, is an error. A blank line terminates the list of commands.

From a C program’s perspective, an object file would be a target (e.g. main.o), and its de­pen­den­cy would be main.c. If main.c has a newer date, the target is “out of date”, and to get it up to date, the object file must be re-generated.

Special Variables

Make creates special variables like $<, $@, $*, amongst others, that expand to various filenames in rules. In GNU Make terminology, they are called automatic variables. They are useful in creat­ing more easily reusable Makefiles.

Generic Rules

Some documentation refers to rules as ‘recipes’, so keep that in mind. Generally, the compilation of every source file, will involved the same command line, apart from the names of the files. To make this simpler, one can specify a generic rule to use, in cases where an explicit rule is not present.

It does mean that one require a mechanism to replace certain parts of the command line(s) with either the target filename, or dependency filename(s), or both. This is where the special var­iables come into play.

Conditional Statements

Much like the C preprocessor, GNU Make can check if a macro exists, or not. This can be useful to conditionally create macros or rules. Unlike with recipes, hard tabs are not necessary in this ex­amp­le ex­tract:

The indentation is a matter of style, not a syntactical requirement. Several other conditionals are available, allowing make syntax to be quite flexible and powerful.

Pseudo Targets

It is possible to specify targets that are not real files. The are called pseudo targets (.PHONY in Make terminology). Make must know they are not real files, so it will not check for a time stamp, and so that it will always execute the rules. Popular pseudo targets are all, clean and some­times install.

Example

Given a C99 program that consists of, for example, three source files: main.c, utility.c, input.c, and has convenience headers: utility.h and input.h, the expanded steps required to create an executable called myprogram (myprogram.exe under Windows), the following commands are re­quir­ed in a Bash or compatible shell, to create a debug executable:

$> POSIX Shell Separate Compilation
CFLAGS=-Wall -Wextra -std=c99
gcc $CFLAGS -c -g main.c
gcc $CFLAGS -c -g utility.c
gcc $CFLAGS -c -g input.c
gcc -g -o myprogram main.o utility.o input.o

The order in which the source files are compiled, or the order the object files are passed to the linker (ld) is irrelevant. The last command only calls the linker, since none of the arguments are source files. We use gcc to call the linker, since it must be passed extra object files like the start­up code, and C99 standard library file(s). This is the easiest way to call the linker for C pro­g­rams (and C++, but g++ is used instead).

For completeness, if you are using the Windows Command Prompt, the above command lines will appear as follows (not properly highlighted, since Pandoc’s syntax highlight component does not understand Command Prompt shell syntax):

$> Command Prompt Separate Compilation
set CFLAGS=-Wall -Wextra -std=c99
gcc %CFLAGS% -c -g main.c
gcc %CFLAGS% -c -g utility.c
gcc %CFLAGS% -c -g input.c
gcc -g -o myprogram main.o utility.o input.o

If one of the source files should change, we only have to repeat the relevant gcc $CFLAGS… command for that file, and then perform the “link” command gcc -g -o…. For three files, this may not be such a big deal, but for 300, it is another story.

The Makefile below has been set up so that it will only perform the compilation of modified source files (*.c). The linker will then also be called to tie it all together into an executable. If you do not have make, you may still have mingw32-make, so use that instead.

This template Makefile assumes that you have a directory for each project, and that all .c files in that directory, is part of the same project. It should be placed at the same directory level as the source files.

MakefileExample Program Makefile
# GNU Make Makefile for My Example Program
#
# Call make with: `make all`           for a “debug compile”, or
# call make with: `make all RELEASE=1` for a “release compile”.
#
# Note: On Windows, you may have GNU's `make`, or `mingw32-make`, so you
#       should run the appropriate version with this `Makefile`.

# operating system detection, to make the rest of the file more generic.
ifeq ($(OS),Windows_NT)
   THEOS := Windows
else
   THEOS := $(shell uname -s)
endif

# per-project settings and modifications. do not add `.exe` on Windows.
PROG := myprogram
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
CC := gcc
CFLAGS := -Wall -Wextra -pedantic -std=c99
LDFLAGS := 

# change compiler flags based on release/debug compiles.
ifdef RELEASE
   CFLAGS += -O2 -DNDEBUG
else
   CFLAGS += -O0 -g2
endif

ifeq ($(THEOS),Windows)
   PROG := $(PROG).exe
   RMCMD := cmd /c del $(PROG) $(OBJS) 2>NUL:
else
   RMCMD := rm $(PROG) $(OBJS) 2>/dev/null || true
endif

# generic rule for any `*.o` file, which is dependent on a `*.c` file
# with the same name, just a different extension. `$@` will result in
# the object file's name, and `$<` in the source file name.
#
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# this rule specifies that the program executable, is dependend on
# the list of object files in `$(OBJS)`, and the commands to execute
# when it is out of date, is to peforming the “link” command.
#
$(PROG): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)

.PHONY: all clean

# also clean preprocessed and assembler files, just in case. checks
# if running on Windows (only OS that has `ALLUSERSPROFILE` as standard).
clean:
	-@$(RMCMD)

all : $(PROG)

IMPORTANTHard Tab Characters

A reminder that GNU Make is archaic and cantankerous — it does not handle files or paths with spaces, and it insists on a hard tab character for the commands to execute after a dependency rule. You cannot let your editor expand a press of the ‹Tab› key into spaces.

The above Makefile can easily be used as a template for relatively uncomplicated multi-file C prog­rams — simply copy to another project directory, and change the value for the PROG var­iable. Then run make all DEBUG=1 (debug compile), or make all (release compile). It will also work for C++ program, but you have to, by convention, use CXX for the C++ compiler executable name, and CXXFLAGS for C++-specific options.

In the above Makefile example, since we test if we are running on Windows, by checking the existence of the OS environment variable, which should be equal to Windows_NT when run­ning on any Windows OS. The uname -s program and switch should be available on any POSIX system.

operating system detection in a makefile example

From this point on, the rest of the Makefile code will test THEOS; for example, we could have written the clean: target as follows:

conditional directives based on operating system

On MacOS, uname -s will return Darwin, and on Linux: Linux, so you could use those values when you want to differentiate between the latter two operating systems.

TIPGenerated Dependencies with GCC

You can use: g++ -MM *.cpp to create a list of targets and dependencies. Or gcc -MM *.c for C pro­grams. It will exclude system headers.

Conventional C++ specific macros are: CXXFLAGS for C++ driver options, and CXX for naming the spe­ci­fic C++ compiler driver executable. The rest of the Makefile example is easily convertible for use in C++ projects.


2018-07-02: Add C++ notes & fixed gcc -MM… for C depencies. [brx]
2017-11-19: Update to new admonitions. [brx]
2017-09-22: Created. Edited. [brx;jjc]