Make
What is Make?
Make is a tool used for triggering actions based on the tracking of files, traditionally in a C/C++ project. The make
command will look for a makefile
in the current directory, named either GNUmakefile
, makefile
, or Makefile
.
The makefile
contains the rules, prerequisites, targets and commands used in a project. By defining the targets to be generated, and that targets prerequisites, Make can determine whether changes to the source files warrant a revision to the target file. Make can be used to manage dependencies - if files haven't changed since the last execution of a make
command, repeating the command will have no effect. The make
command will build or re-build its targets. It tracks whether relevant changes have been made recently using filesystem timestamps.
Make Syntax
Rules
target: prequisite
command
target: prequisite
command
The functional parts of a makefile
are composed of block like this, called a rule. The rule defines the files, or prerequisites that are required to exist before the command contained in the rule is run. The result of a rule usually generates a target file.
Prerequisites
Prerequisites are files required by a rule to execute, whether they are existing files, or referencing other targets in the Makefile which are expected to have already ran.
Order Only Prerequisites
Prerequisites which are required for running a rule, but shouldn't require a full re-run when altered can be added as Order Only Prerequisites as follows:
target: prequisite | order-only-prerequisite
command
target: prequisite | order-only-prerequisite
command
Cases where this might be useful include directory creation. Any update to a directory would trigger an update using traditional prerequisites, due to timestamp changes when content inside the directory changes. Using this method would trigger its creation only once.
Targets
The output file to be generated by the Make rule, whether an object file or a binary file.
PHONY
A PHONY
target is one that doesn't produce an output file, and is instead a placeholder name that provides a description or invocable handle for the rule.
.PHONY: clean
clean:
rm -rf build
.PHONY: clean
clean:
rm -rf build
Commands
Commands are command line operations that will run when a rule is being executed. They can be separated sequentially by line breaks, and silenced by prepending with a @
. Without this, the command will appear in any command line output, followed by its print results.
Tabs vs. Spaces
Unlike other languages, Make is not agnostic about tabs and spaces. Each command line must begin with a tab, and not spaces.
Default Behavior
By calling make
with no target, it will execute the first rule it finds in the Makefile. The common approach to this is the create a default first rule called all
which has a prerequisite every target meant to be built.
.PHONY: all
all: test_program
.PHONY: all
all: test_program
Variables
Variables can be declared two ways either with =
or :=
operators:
VARIABLE1 = val
VARIABLE2 := val
VARIABLE1 = val
VARIABLE2 := val
=
variables are expanded recursively, such that everywhere that value is referenced, it will be updated in the file when assigned a new value. :=
assignments are only expanded once, and will evaluate as expected from C variable assignment.
greeting = $(hello)
hello = Hello
place = $(world)
world = World
greeting = $(hello)
hello = Hello
place = $(world)
world = World
echo $(greeting) $(place) -> Hello World
greeting := $(hello)
hello := Hello
place := $(world)
world := World
greeting := $(hello)
hello := Hello
place := $(world)
world := World
echo $(greeting) $place -> ' '
hello
and world
aren't defined when assigned to their variables, and will be evaluated as empty strings.
Variables are referenced in Make using the $()
syntax.
A variable can also be passed in from the command line:
make target VARIABLE=1
make target VARIABLE=1
These variables can be treated as optional by assigning them using the flexible operator ?=
which will assign a default value to the variable if one isn't provided.
Variables can be appended using the +=
operator.
Automatic Variables
$@
represents the target which caused the rule to run$^
represents the list of prerequisites, separated by spaces$<
represents the first in the list of prerequisites$(@D)
represents the directory part of the target filename$(^D)
represents the directory parts of the prerequisites as a list$(<F)
represensts the directory part of the first prerequisite
target: prerequisite1 prerequisite2 prerequisite3
cc $^ -o $@
target: prerequisite1 prerequisite2 prerequisite3
cc $^ -o $@
Second Expansion
The .SECONDEXPANSION
special target is used to enable secondary expansion of prerequisites in explicit rules. The secondary expansion allows you to use automatic variables like $@
, $<
, and $^
in the prerequisites list. Normally, in a makefile, automatic variables are not expanded in the prerequisites list of explicit rules, but with .SECONDEXPANSION
, you can achieve this.
.SECONDEXPANSION:
%.txt: $$(wildcard $$*.src)
cp $< $@
.SECONDEXPANSION:
%.txt: $$(wildcard $$*.src)
cp $< $@
Implicit Variables
Implicit variables are also made available for common commands, such as AR
for archiving programs, CC
for C compiling programs, and RM
for removing a file. These can be useful for cross-platform makefiles.
AR
AS
CC
CXX
CPP
RM
AR
AS
CC
CXX
CPP
RM
Flags for common commands are also made available:
ARFLAGS
ASFLAGS
CFLAGS
CXXFLAGS
CPPFLAGS
LDFLAGS
LDLIBS
ARFLAGS
ASFLAGS
CFLAGS
CXXFLAGS
CPPFLAGS
LDFLAGS
LDLIBS
.PHONY: clean
clean:
$(RM) target
.PHONY: clean
clean:
$(RM) target
Implicit Rules
Many rules can be interpreted by Make itself, such as when a .o
file is required as a prerequisite. Make is able to discern that should that file not exist, another file with the same prefix ending in .c
should be available and will be used to build the .o
file.
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $^ -o $@
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $^ -o $@
%
is a Make wildcard character, so in this example any prefix of the filename ending in .o
, such as filename.o
will have as its prerequisite filename.c
. This wildcard value can also be accessed using the operator $*
. This prefix filename is known as the stem.
Note also that the default variables $(CFLAGS)
and $(CPPFLAGS)
are used in this rule, so when defined in the Makefile, any options to those compiler will be substituted.
Conditional Logic
Conditional blocks in Make follow the syntax
if #condition
#
else if #condition
#
else
#
endif
if #condition
#
else if #condition
#
else
#
endif
ifdef and ifndef
ifdef
and ifndef
will check whether a variable has been defined or not. They don't, however, check for the variable being an empty string.
ifdef VARIABLE
#
else ifndef VARIABLE
#
else
#
endif
ifdef VARIABLE
#
else ifndef VARIABLE
#
else
#
endif
ifeq and ifneq
ifeq
and ifneq
check a variable's value against another value. These checks can be used for testing if a variable is an empty string.
ifeq ($(VARIABLE), 1)
#
else ifeq ($(VARIABLE), '')
#
else
#
endif
ifeq ($(VARIABLE), 1)
#
else ifeq ($(VARIABLE), '')
#
else
#
endif
Functions
Functions in Make follow the syntax $(function_name argument1, argument2, ...)
and can be defined by the user, though many are provided as Make build system functions. The full list is available in the GNU Manual.
Pattern Substitution
Replace the words matching the given pattern
in text
with the replacement
.
$(patsubst pattern,replacement,text)
String Substitution
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)
Strip Function
Remove excess whitespace characters.
$(strip string)
Filter Function
Select words in the text
that match one of the pattern
words.
$(filter pattern..., text)
Shell Function
Executes a shell command and returns the output.
$(shell command)
Conditional Functions
Evaluate the condition and execute the then
result. Available for if
, or
, and and
. An optional else
condition can be provided.
$(if condition, then-part[,else-part])
Directory Function
Extracts the directory part from a list of filenames.
$(dir names...)
Not Directory Function
Extracts the non-directory part from a list of filenames.
$(notfir names...)
Basename Function
Extracts the filename, without the suffix of each filename.
$(basename names...)
Foreach Function
Evaluate text with var bound to each word in words, and concatenate the results.
$(foreach var,words,text)
Message Functions
warning
, info
, and error
functions are available to trigger exit, debugging messages, or info messages.
$(info value)
$(warning value)
$(error value)
Custom Functions
Custom functions can be created by assigning any combination of GNU Make functions to a variable, and using the $(1)
, $(2)
, etc. automatic variables as the arguments.
function = $(foreach DIR,$(1),$(wildcard $(DIR)/*$(2)))
$(call function, param, param, ...)
function = $(foreach DIR,$(1),$(wildcard $(DIR)/*$(2)))
$(call function, param, param, ...)
Compiling
Clean
The compilation process can generate a number of temporary files, so it's always advisable to not only point output to a build
folder, but to include clean
rule in the Makefile that will wipe that folder for a clean rebuild.
Compile Commands
The CC
compiler variable and cc
command line variable is available to Mac/Linux users and points to the default C compiler. This can be made use of in Makefiles to easily call a compile command that should be okay on all systems.
$(CC) file1.c
$(CC) file1.c
This will generate a default a.out
file as its ouput. While this can be executed ./a.out
, it can be called something more descriptive or memorable using the -o
output flag.
$(CC) file1.c -o make_test
$(CC) file1.c -o make_test
Includes can also be passed to this command using the -I
flag. By default, the compiler will include the current directory it's invoked in, as well as the system header directory. The default directories can be shown with the cpp -v
command.
$(CC) file1.c -I include/ -o make_test
$(CC) file1.c -I include/ -o make_test
Object Files
Creating the binary output file includes the intermediary step of taking the object files generated by the compiler, and linking them to addresses in memory to create a program. The intermediary object files can be generated as the primary output in a multi-step Make process using the -c
flag.
cc -c file1.c
cc -c file1.c
This will result in a file1.o
output. The functions and variables invoked in file1
will be present in the object file, but the addresses will be temporary stubs with no set value.
Linking
Linkers combine object files, and libraries to create either object files, executables, or libraries. The default linker ld
for gcc
and clang
can be invoked for both C and C++, and outputs referenced the same was the compiler.
ld file1.o -o test_program
ld file1.o -o test_program
The compiler can also be used to invoke the ld
linker:
cc file1.o -o test_program
cc file1.o -o test_program
Linking Libraries
When linking against libraries, the ld
linker can be invoked with the -l
flag, followed by the name of the library on the compilers library path:
ld file1.o -lc -o test_program
ld file1.o -lc -o test_program
In this case, libc
was invoked by using the name of the library (all libraries begin with the lib
prefix, so this can be ignored).
When the library is located in another directory, the -L
flag can used to specify:
ld file1.o -L/home/lib/ -lcustom -o test_program
ld file1.o -L/home/lib/ -lcustom -o test_program
This will likely fail, because the ld
linker requires more flags when passing libraries for compiling. Instead, using the default compiler as your interface simplifies the operation.
cc file1.o -L/home/lib/ -lcustom -o test_program
cc file1.o -L/home/lib/ -lcustom -o test_program
To find which directories are being searched for libraries:
cc -print-search-dirs
cc -print-search-dirs
A linker script can be passed , using the -T
flag.
For debugging purposes, a .map
file can be generated with all functions, variables, library imports, and their memory addresses and size.
cc file1.o -Wl,-map,program.map -o test_program
# or
ld file1.o -map program.map -o test_program
cc file1.o -Wl,-map,program.map -o test_program
# or
ld file1.o -map program.map -o test_program
Static Libraries
Denoted by the .a
file extension, static libraries are files that contain functions that can be used by many applications. Within an application, linking a static library will copy the entire content of the library to the program.
Dynamic Libraries
Denoted by the .dll
, .so
, or .dylib
file extensions, dynamic libraries are referenced by the program that it is linked to, rather than copying the entire contents of the library. Dynamic libraries are referenced using a program called the loader, which reads the library contents and stores the machine code for the required functions into memory.
Creating Static Libraries
Static libraries are creating using a program called an Archiver, ar
. The benefits of compiling static libraries rather than dynamic libaries, are it can keep source code hidden, decrease coupling within the system, and reduce compilation times for files that aren't change often.
The syntax of the archiver command are as follows:
ar rcs library.a file1.o file2.o ...
ar rcs library.a file1.o file2.o ...
The rcs
refers to the flags r
, which overwrites symbols into the archive, c
which creates the archive, and s
which writes an index. These flags can be passed in most cases and represents a catch-all command which should suit most use-cases.
Creating Dynamic Libraries
Dynamic libaries must be created using the compiler, and are invoked using the -fPIC
flag, as well as the -shared
flag. The -f
flag is followed by PIC
for Position Independent Code, which allows the code to be references from other memory addresses.
cc -fPIC -c file`.c -o test_program.o
cc -shared test_program.o -o libcustom.dylib
cc -fPIC -c file`.c -o test_program.o
cc -shared test_program.o -o libcustom.dylib
The dylib
file extension for MacOS will be dll
for Windows, and so
for Linux.
Libraries which aren't found in the default paths will need to have their locations set as the LD_LIBRARY_PATH
variable.
LD_LIBRARY_PATH=./libraries
LD_LIBRARY_PATH=./libraries