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
Makefile
s.
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
$ 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
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.
Makefile
— 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`.
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)
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.
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.
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 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).
Makefile
—
Generic 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]