Makefiles (or, the GNU automake system), is a programming language all its own, and you can do some pretty spectacular things with them (it). This is a basic introduction to what you'll need to write useful makefiles.
The goal of any Makefile is to build all of its targets. Ultimately, a Makefile is a series of rules to accomplish this task. Makefiles break down then into the following:
VARIABLE DECLARATIONS TARGET: DEPENDENCIES RULES TO BUILD TARGET
Let's break this down...
Makefiles have variables; these are very useful for hopefully obvious reasons. Some common variables include:
# Hashes are comments in Makefiles # Some variables are automatically defined, for example CC is the # default C compiler and CXX the default c++ complier, but we can # override them if we like. Generally speaking, however, overiding # the CC variable is the *incorrect* thing to do CC = clang # CFLAGS are the flags to use when *compiling* CFLAGS=-m32 -c # LFLAGS are the flags to use when *linking* LFLAGS=-ldl # Variables can inherit from other variables # (note the $ for accessing the value of a variable): CFLAGS_DBG=$(CFLAGS) -g -Wall -Werror LFLAGS_DBG=$(LFLAGS) -g # EXE, OUT, TARGET, or GOAL are all some common examples of your end goal EXE=disk_sched
To access a variable in a makefile, simply use $(VAR_NAME) note the parentheses. Variables in Makefiles are just like #defines in C/C++, simple string substitution.
tab space space | | | target: dependency1 dependency2 dependency3 rule1 rule2 rule3 ^^^^^^^^ | tab
These three pieces work together:
make [ -f makefile ] [ options ] ... [ targets ] ...So, when you run
$ makemake will choose the default makefile "Makefile", with no options, and it will try to build the default target, which is the first target listed in the file, in this case "all".
If the target "all" looks like this:
EXE=disk_sched all: $(EXE)Then make will try to satisfy all the dependencies of the rule "all", in this case by building the target $(EXE), which is disk_sched.
Consider the very simple Makefile, whose goal is to build "a":
# You should 'man touch' if you aren't familiar with the command EXE=a CC=touch all: $(EXE) $(EXE): b $(CC) aLet's walk through what happens when we run
$ make make: *** No rule to make target `b', needed by `a'. Stop.So, make failed because it couldn't satisfy all the dependencies of "a". Let's help it out a little bit:
$ touch b $ make
$ make touch aNow, what happens if we run make again?
$ make make: Nothing to be done for `all'.What happened? It turns out Step 4, "Check for the file b" is a bit more complicated. We said that "a" depends on "b". So, when make starts to build "a", it notices that a copy of "a" already exists (from our last 'build'); it also notices that "b" hasn't changed since the last time we built "a". So, if "a" only depends on "b", and "b" hasn't changed since the last time we built "a", then "a" is 'up to date'. This is the most useful feature of Makefiles, but it's important to understand how it works; that is, "a" will only be rebuilt if "b" has changed.
How does make know if "b" has changed since we last built "a"? It uses file timestamps. Every time you write a file, it's last modified time is updated. Since the last time we built "a" after "b" already existed, "a"'s timestamp was more recent than "b"'s. If "b"'s timestamp were more recent, then "a" would be rebuilt.
Working with the same Makefile:
EXE=a CC=touch all: $(EXE) $(EXE): b $(CC) aAnd staring clean...
$ rm a b $ make make: *** No rule to make target `b', needed by `a'. Stop. $ touch b $ make touch a $ make make: Nothing to be done for `all'. $ touch b $ make touch aNotice in the last example, since "b" had been updated, "a" had to be rebuilt.
Make sure you understand everything in this example before moving on.
Our goal now is the build my_project, which is made up of main.c, other.c, and library.o - where library.o is some prebuilt library. Don't worry if this looks complicated, we'll break it down in a moment.
## Define Variables ## #CC=gcc, remember, we don't need this one, make defines it for us CFLAGS=-m32 -c # -m32, library.o is 32-bit, so we want to force a 32-bit build # -c, for the compile step, just build objects OBJS=main.o other.o LIBS=thread.o EXE=my_project ## Define our target ## all: $(EXE) ## Define our link step ## # As a convention, always include the LDFLAGS variable in your link step, even # if your project doesn't define it because other tools make try to use it. $(EXE): $(OBJS) $(LIBS) $(CC) $(LDFLAGS) $(OBJS) $(LIBS) -o $(EXE) ## Define our compilation steps / dependencies ## main.o: main.c $(CC) $(CFLAGS) main.c other.o: other.c $(CC) $(CFLAGS) other.c ## We'll discuss this one in a minute ## clean: rm -f $(OBJS) $(EXE)
One of the biggest reasons makefiles look complicated is all of the variables can seem to hide what's actually happening. Let's look at this same Makefile with all of the variables substituted:
all: my_project my_project: main.o other.o thread.o gcc main.o other.o thread.o -o my_project main.o: main.c gcc -m32 -c main.c other.o: other.c gcc -m32 -c other.c clean: rm -f main.o other.o my_project
A little better... now let's walk through what's happening:
The target clean is a conventional thing to include in Makefiles that will remove all of the files built by make. It is a convenient thing to include and is also an interesting example. Let us observe what happens when we run
$ make clean
$ touch clean $ make <output snipped, try it!>To fix this problem, read here about PHONY targets.
Using the same scenario as the previous example, let's add some testing:
CFLAGS=-m32 -c # -m32, library.o is 32-bit, so we want to force a 32-bit build # -c, for the compile step, just build objects LFLAGS= OBJS=main.o other.o LIBS=thread.o EXE=my_project all: $(EXE) $(EXE): $(OBJS) $(LIBS) $(CC) $(LFLAGS) $(OBJS) $(LIBS) -o $(EXE) main.o: main.c $(CC) $(CFLAGS) main.c other.o: other.c $(CC) $(CFLAGS) other.c clean: rm -f $(OBJS) $(EXE) rm -f tests/*.out # The '@' symbol at the start of a line suppresses output test1: $(EXE) tests/1.good @rm -f tests/1.out @./$(EXE) > tests/1.out @diff tests/1.good tests/1.out && echo "Test 1 PASSED" || echo "Test 1 FAILED" test2: $(EXE) tests/2.good @rm -f tests/2.out @./$(EXE) > tests/2.out @diff tests/2.good tests/2.out && echo "Test 2 PASSED" || echo "Test 2 FAILED" tests: test1 test2 .PHONY: clean test1 test2 tests
What happens when you run 'make tests'?