It is common to use the make program to compile code. This is especially the case for larger projects with multiple binaries or complicated compilation procedures.
An advantage of using make is having a single, simple command to use to compile a program. After creating a Makefile, you are able to run any target to produce a program, clean up the working environment, create a tar distribution or anything else that you think would be useful.
Makefiles
When run, make will search for a Makefile in the current directory. The contents of the Makefile contain the instructions that make will use to perform some action, such as build your project.
Makefiles follow a set structure, and it is important that you follow this to ensure it will work as expected. Of particular note is the indentation of commands within a rule - these must be TAB characters. If they are not, then make will substitute your rule with a default, which is undesirable.
Rules
Rules are composed of targets and dependencies and have an associated command. To build a rule, make first checks that all dependencies are met. Each dependency specifies a file which is necessary for the rule to be built. If the file does not exist, make searches the Makefile for a target that matches the dependency.
The format for a rule is:
target: dependencies
command
The target
is the name of the file to be output, or a dummy name (in the case of all
, clean
, etc. This will have zero or more dependencies, which is a list of files (which may or may not have their own rules in the Makefile). Finally there are zero or more commands listed for the rule, indented with a single TAB character underneath the target line.
A target will only be run if the target has dependencies that are newer than it (assuming the target is not specified under .PHONY
. When using a target, make compares the timestamp of the target against the timestamps of the dependencies. If any of the dependencies have a newer timestamp than the target, make will run the commands for that target again. If the target is newer, no commands for that rule will be run.
An example from the Makefile below is:
server: server.c shared.o
$(CC) $(CFLAGS) shared.o server.c -o server
This rule will compile a binary called server
. This depends on two files, one of which has a rule defined for it elsewhere in the Makefile. When make runs this rule, it will work out that shared.o
has its own rule, and will then go about checking its dependencies to find out if its rule needs to be run before running the server rule.
By default, when you run make, it will look at the first rule in the Makefile and any rules that it depends on. If you want to use a different rule as your default, or to ensure make always uses the correct rule as the default, include a .DEFAULT
rule in your Makefile.
Variables
Make supports variables that have a similar syntax to Bash variables. Some common variables are
- CC: specifies the compiler, such as gcc, g++ or clang,
- CFLAGS: specifies compilation flags, and
- LDFLAGS: specifies linker flags.
Variables are set like so: CC = gcc
. The value of a variable can be used in rules by prepending a dollar sign ($) and wrapping the variable name in parentheses (although single-letter variables do not require parentheses), like so: $(CC)
Example Makefile
The following is an example Makefile. It will compile two programs, server
and client
. There is also a shared object that is compiled and linked in to both of these binaries, shared.o
. There is also a special debug
rule, which updates the flags for compilation before running all of the rules for the binaries listed in the TARGETS
variable.
CC = gcc
CFLAGS = -Wall -pedantic -std=gnu99
DEBUG = -g
TARGETS = server client
# Mark the default target to run (otherwise make will select the first target in the file)
.DEFAULT: all
# Mark targets as not generating output files (ensure the targets will always run)
.PHONY: all debug clean
all: $(TARGETS)
# A debug target to update flags before cleaning and compiling all targets
debug: CFLAGS += $(DEBUG)
debug: clean $(TARGETS)
# Create a shared object for inclusion in our programs
shared.o: shared.c shared.h
$(CC) $(CFLAGS) -c shared.c -o shared.o
server: server.c shared.o
$(CC) $(CFLAGS) shared.o server.c -o server
client: client.c shared.o
$(CC) $(CFLAGS) shared.o client.c -o client
# Clean up our directory - remove objects and binaries
clean:
rm -f $(TARGETS) *.o