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
PREREQUISITES — You should already…
- have some programming experience, preferably in C.
- have basic command line skills (Windows or POSIX).
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
does allow one to specify several source files, but this is not very
efficient, if only one source file in a program has changed.
make utility can be given a
(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.
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
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
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
›). Only macro
names consisting of a single letter, do not require the surrounding
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
main.o), and its dependency would be
main.c has a newer date, the target
is ‘out of date’, and to get it up to date, the object file must be
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
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.
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.
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
Given a C99 program that consists of, for example, three source
input.c, and has convenience headers:
input.h, the expanded steps
required to create an executable called
myprogram.exe under Windows), the following commands are
required in a Bash or compatible shell, to create a debug
$> POSIX Shell Separate Compilation
$ 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
> 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
> 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
gcc $CFLAGS… command for that file, and then
perform the ‘link’ command
gcc -g -o… (which calls the
ld). For three files,
this may not be such a big deal, but for 300, it is another story.
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
may still have
mingw32-make, so use that instead.
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.
— Example 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 CPPFLAGS := LDFLAGS := # Change compiler flags based on release/debug compiles. # ifdef RELEASE CFLAGS += -O2 -DNDEBUG else CFLAGS += -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 ($(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) $(CPPFLAGS) $(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 peform the ‘link’ command. # $(PROG): $(OBJS) $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) .PHONY: all clean clean: -@$(RMCMD) all : $(PROG)
IMPORTANT — Hard 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.
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
variable. Then run
make all RELEASE=1 (release compile), or
make all (debug compile by default).
CPPFLAGS variable for the C PreProcessor
(not C++). You can add additional include directories
with this flags, e.g.: ‘
CPPLFAGS := -I/include-dir’.
Makefile will also work for C++ programs compiled
with GCC (
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
In the above
Makefile example, since we test if we are
running on Windows, by checking the existence of the
environment variable, which should be equal to
when running on any Windows OS. The
uname -s program and
switch should be available on any POSIX
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
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
uname -s will return
Linux, so you could use those values when you
want to differentiate between the latter two operating systems.
TIP — Generated 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
example is easily convertible for use in C++ projects.
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).
# 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 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 ($(THEOS),Windows) CFLAGS += -DWINDOWS -D__MSVCRT_VERSION__=0x1400 CXXFLAGS += -DWINDOWS -D__MSVCRT_VERSION__=0x1400 PROG := $(PROG).exe RMCMD := cmd /c del /Q $(PROG) obj\* 2>NUL: else CFLAGS += -DPOSIX CXXFLAGS += -DPOSIX 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. # 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) ifdef RELEASE ifeq ($(THEOS),Darwin) $(PROG) strip else $(PROG) strip -s endif endif .PHONY: all clean showenv dox clean: -@$(RMCMD) all: $(PROG) showenv: @echo $(THEOS) @echo $(RMCMD) @echo $(MKPATH) @echo $(PRJDIR) @echo $(PROG) @echo $(SRCS) @echo $(OBJS)
Makefile contains a
pseudo target, we did not provide any recipes for documentation
generation. If you have a
Doxyfile, you can add the
following, before or after the
Doxygen target and recipe
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) ifdef RELEASE $(PROG) strip -s endif
For C projects, you will also have to change the values for
Sources and object files for C projects
# Enable for C projects SRCS := $(wildcard src/*.c) OBJS := $(patsubst src/%.c,obj/%.o,$(SRCS))
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]