GNU Make Fundamentals

Concepts and Principles of Program Building

Although newer build systems exist, GNU Make is ubiquitous, and it does not hurt programmers 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 nevertheless 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 compile 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 IDE s 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 intermediate 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:

CFLAGS = -Wall -Wextra -std=c99

For immediate expansion, you can use ‘:=’, instead of ‘=’.

CFLAGS := -Wall -Wextra -std=c99

Spaces surrounding the equal sign are not significant. The expression on the right of the assignment may be blank. This will either create the macro name on the left, or clear its current contents.

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

CFLAGS += -DNDEBUG -O2

Unlike POSIX shells, the contents of variables (macros) must be retrieved with parenthesis surrounding 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 dependency 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.

Automatic Variables

Make creates ‘special’ variables like $<, $@, $*, amongst others, that automatically expand to various filenames in rules. In GNU Make terminology, they are called automatic variables. They are useful in creating 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 variables 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 example extract:

Conditional statement example
ifdef DEBUG
   CFLAGS += -O0 -g
else
   CFLAGS += -O2 -DNDEBUG
endif

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 sometimes 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 required 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 startup code, and C99 standard library file(s). This is the easiest way to call the linker for C programs (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:

$> 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

And for PowerShell, you might use these commands:

$> PowerShell Separate Compilation
> $Env:CFLAGS=-Wall -Wextra -std=c99
> gcc $Env:CFLAGS -c -g main.c
> gcc $Env:CFLAGS -c -g utility.c
> gcc $Env: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… (which calls the GNU linker: ld). 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`.

CC=gcc
CFLAGS=-Wall -Wextra -Wpedantic -std=c++17

BUILDTYPE=-g2 -O0
ifdef RELEASE
	BUILDTYPE=-O2 -DNDEBUG
endif

PROG=myprogram
SRCS=$(wildcard *.c)
OBJS=$(SRCS:%.c=%.o)

ifeq ($(OS),Windows_NT)
   PROG := $(PROG).exe
   SHELL := powershell.exe
   .SHELLFLAGS := -NoProfile -NoLogo -Command
   RM := Remove-Item $(PROG),$(OBJS)
else
   RM := rm $(PROG) $(OBJS)
endif

$(PROG): $(OBJS)
	@echo LINKING $^ to $@
	$(CC) $(BUILDTYPE) -o $@ $^

# generic target/dependency specification
%.o:%.cpp
	@echo COMPILING $^ to $@
	$(CC) $(CFLAGS) $(BUILDTYPE) -c $^

.PHONY: all clean
all: $(PROG)
clean:
	-$(RM)

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 programs — simply copy to another project directory, and change the value for the PROG variable. Then run make all RELEASE=1 (release compile), or make all (debug compile by default).

The CPPFLAGS variable for the C PreProcessor (not C++). You can add additional include directories with this flags, e.g.: ‘CPPLFAGS := -I/include-dir’.

This Makefile will also work for C++ programs compiled with GCC (g++), but you have to, by convention, use CXX for the C++ compiler executable macro, and CXXFLAGS for C++-specific options in your command lines. Also remember to change reference to *.c to *.cpp.

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 running on any Windows OS. The uname -s program and switch should be available on any POSIX system.

Operating system detection in a makefile example
ifeq ($(OS),Windows_NT)
    THEOS := Windows
else
    THEOS := $(shell uname -s)
endif

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
clean:
ifeq ($(THEOS),Windows)
	-@cmd /c del $(OBJS) $(PROG) 2>NUL:
else
	-@rm  $(OBJS) $(PROG) 2>/dev/null || true
endif

The Windows version will only work in Command Prompt. It gets more complicated within PowerShell, where we have to change Make's SHELL variable to powershell.exe:

Dealing with PowerShell on Windows
ifeq ($(OS),Windows_NT)
   PROG := $(PROG).exe
   SHELL := powershell.exe
   .SHELLFLAGS := -NoProfile -NoLogo -Command
   RM := Remove-Item $(PROG),$(OBJS)
else
   RM := rm $(PROG) $(OBJS)
endif

Now we can add -$(RM) as a command-line under the clean: target, and should work on most operating systems.

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 programs. It will exclude system headers.

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

Generic Makefile

The following Makefile for GNU make, tries to be as generic as possible. It automatically picks up all source files in .\src\, and the name of the executable will be taken from the project directory name. Object files are stored in .\obj\. It has been tested on Linux, macOS, Windows, and WSL (Windows Subsystem for Linux).

MakefileGeneric Makefile
# Generic GNU Make Makefile for Simple C/C++ Projects
#
# 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`.

# This is not really necessary, considering our code below. You may need
# this if you modify the code.
#
vpath %.c ./src
vpath %.cpp ./src

# Save project directory as absolute and relative names. This will not
# work if the path has spaces, or you have included `Makefile`s, but is
# at least not dependent on the current working directory.
#
MKPATH := $(abspath $(lastword $(MAKEFILE_LIST)))
PRJDIR := $(notdir $(patsubst %/,%,$(dir $(MKPATH))))

# 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.
# If you do not want the executable to have the same name as the project
# directory, modify `PROG` to suit your requirements.
#
PROG := $(PRJDIR)

# Enable for C projects
# SRCS := $(wildcard src/*.c)
# OBJS := $(patsubst src/%.c,obj/%.o,$(SRCS))

# Enable for C++ projects
SRCS := $(wildcard src/*.cpp)
OBJS := $(patsubst src/%.cpp,obj/%.o,$(SRCS))

CC := gcc
CXX := g++
CFLAGS := -Wall -Wextra -pedantic -std=c99
CPPFLAGS :=
CXXFLAGS := -Wall -Wextra -Wpedantic -std=c++14
LDFLAGS :=
#
#=======================================================================


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

# Set operating system specifics, like adding `.exe` to the executable
# name, and the commands for deleting files (Windows has no `rm`).
#
ifeq ($(OS),Windows_NT)
   PROG := $(PROG).exe
   SHELL := powershell.exe
   .SHELLFLAGS := -NoProfile -NoLogo -Command
   CFLAGS += -DWINDOWS
   CXXFLAGS += -DWINDOWS
   space := $(subst ,, )
   comma := ,
   FILES := $(PROG),
   FILES += $(subst $(space),$(comma),$(OBJS))
   RMCMD := Remove-Item $(FILES) -ea SilentlyContinue; $$true > $$null
else
   CFLAGS += -DPOSIX
   CXXFLAGS += -DPOSIX
   RMCMD := rm $(PROG) $(OBJS) 2>/dev/null || true
endif

ifeq ($(THEOS),Windows)
else
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.
#
obj/%.o: src/%.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

obj/%.o: src/%.cpp
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -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 (recipe), perform ‘linking’.
#
$(PROG): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS)

.PHONY: all clean showenv dox

clean:
	-@$(RMCMD)

all: $(PROG)

showenv:
	@echo $(THEOS)
	@echo $(RMCMD)
	@echo $(MKPATH)
	@echo $(PRJDIR)
	@echo $(PROG)
	@echo $(SRCS)
	@echo $(OBJS)

You could add a doxygen: or dox: target to the Makefile to create Doxygen documentation, if you have a Doxyfile and use Doxygen:

Doxygen target and recipe
dox:
	doxygen

If you want to use it for C programs, you have to change the $(PROG): target's recipe to:

Modified recipe for C projects
$(PROG): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)

For C projects, you will also have to change the values for SRCS and OBJS:

Sources and object files for C projects
# Enable for C projects
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,obj/%.o,$(SRCS))

2024-10-04: Change Makefile to use PowerShell. [brx]
2022-08-14: Add strip condition for macOS. [brx]
2021-05-03: Change ‘special variables’ to ‘automatic variables’. [brx]
2020-02-26: Added CPPFLAGS (C Preprocessor) flag. [brx]
2020-02-04: Added a generic Makefile. [brx]
2019-02-18: Corrected DEBUG and RELEASE options for ‘Makefile’. [brx]
2018-12-07: Corrected some comments in the Makefile. [brx]
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]