Ancient CS 61 Content Warning!!!!!1!!!
This is not the current version of the class.
This site was automatically translated from a wiki. The translation may have introduced mistakes (and the content might have been wrong to begin with).

Reading Makefiles

Makefiles are a simple way to organize code compilation. They can be used to automate any builds that can be done on the shell. This guide is meant to be a primer for reading any Makefiles that you may encounter in cs61 (and for writing basic Makefiles of your own).

Makefile Basics

There are three basic commands found in Makefiles -- comments, variable declarations, and rules. A variable declaration looks like this:

OBJECTS = main.o foo.o

which defines OBJECTS to be the sequence "main.o foo.o".

main.o: main.c
    gcc -c main.c

Here is a basic rule that builds the object main.o from main.c.. A rule designates how buildable object is built and tells make what target is being built (make.o), what it depends on (make.c), and how to build it (gcc -c main.c). Rules follow the format

target: dependencies
    command-to-build-code

Notice the tab indent for the second line: Makefiles use tabs to signify that the tabbed line is the command to build the object. The first line is known as the dependency line, while the second line is known as the shell line.

Commands can be indicated using a #, as so:

# this is a comment

All Makefiles are named "Makefile" and can be run using on the terminal using the command 'make'. If 'main' is a target executable that we want to build, we would run

> make main

on the command line.

A simple example

Now consider the following example that compiles a simple hello world program.

main.c

#include <foo.h>
int main() {
  printHello(); // call printHello() defined in foo.h
  return (0);
}

foo.h

void foo();

foo.c

#include <stdio.h>
#include <foo.h>
void foo() {
  printf("Hello, World!");
}

Makefile

CC = gcc
CFLAGS = -g -Wall
OBJECTS = main.o foo.o

main: $(OBJECTS)
    $(CC) $(CFLAGS) $(OBJECTS) -o main

main.o: main.c foo.h
    $(CC) $(CFLAGS) -c main.c

foo.o: foo.c foo.h
    $(CC) $(CFLAGS) -c foo.c

.PHONY: clean
clean:
    rm -rf $(OBJECTS) main

Running

> make main

will recursively compile the main executable based on its dependencies. The makefile will attempt to build main, realize that its dependencies main.o and foo.o do not exist and need to be built, and build them based on the rules in the Makefile.

How one would build this hello world program manually is first create foo.o and main.o and then link them in the executable main. That is what is being specified here: the foo.o target depends on foo.c and foo.h and is built with the command gcc -g -Wall -c foo.c. main depends on foo.o and main.o and can be compiled with gcc -g -Wall main.o foo.o -o main. Notice that make replaces $(VARIABLE) with the contents of the variable VARIABLE.

One of the other common things to be aware of when reading Makefiles is the .PHONY directive. .PHONY signifies that a target name is not an actual file and prevents conflicts in the event that the real file exists. When we execute

> make clean

make will not check that a file named 'clean' exists and instead build the 'clean' target by running rm -rf $(OBJECTS) main, which removes the compiled objects and main executable. Makefiles usually have a clean target to return the code directory to its original form.

The make command is intelligent and will only make targets if their dependencies are newer than the targets (or have more recent timestamps).

A more complicated example

Here is the GNUMakefile from Assignment 1.

CC = $(shell if test -f /opt/local/bin/gcc-mp-4.7; then \
        echo gcc-mp-4.7; else echo gcc; fi)
CFLAGS = -std=gnu99 -g -W -Wall

TESTS = $(patsubst %.c,%,$(sort $(wildcard test[0-9][0-9][0-9].c)))

%.o: %.c m61.h
    $(CC) $(CFLAGS) -o $@ -c $<

all: $(TESTS) hhtest
    @echo "*** Run 'make check' or 'make check-all' to check your work."

test%: test%.o m61.o
    $(CC) $(CFLAGS) -o $@ $^

test017: test017-help.o

hhtest: hhtest.o m61.o
    $(CC) $(CFLAGS) -o $@ $^ -lm

check: $(TESTS) $(patsubst %,check-%,$(TESTS))
    @echo "*** All tests succeeded!"

check-all: $(TESTS)
    @x=true; for i in $(TESTS); do $(MAKE) check-$$i || x=false; done; \
    if $$x; then echo "*** All tests succeeded!"; fi; $$x

check-test%: test%
    @test -d out || mkdir out
    @rm -f out/test$*.fail
    @-sh -c "./$^ > out/test$*.output 2>&1" >/dev/null 2>&1; true
    @perl compare.pl out/test$*.output test$*.c test$*

clean:
    rm -f $(TESTS) hhtest *.o core *.core
    rm -rf out

MALLOC_CHECK_=0
export MALLOC_CHECK_

.PRECIOUS: %.o
.PHONY: all clean check check-% prepare-check

Let’s unpack this. At the top, we have a few variable declarations. First, we set a variable, if it’s not explicitly specified.

O ?= 2

We can override this on a per-invocation basis by specifying the variable on the command line. To disable optimization on this compile only, for example:

$ make O=0

Next, we set a variable TESTS to an array with the name of each test.

TESTS = $(patsubst %.c,%,$(sort $(wildcard test[0-9][0-9][0-9].c)))

We don’t expect you to understand each piece of this, but it’s useful to have a rudimentary familiarity with Make syntax. $(VARIABLE) expands to the value stored in VARIABLE. This is similar to shell scripting, where you set FOO=bar without using the $, but you need to refer to $FOO to get the value of FOO.

Make also has builtin functions, much like your shell ships with a standard set of commands. These functions are all lowercased, and are invoked like so:

    $(func ARG1 ARG2 ARG3…)

So, roughly, we obtain a list of files in the current directory matching format test###.c (where # refers to a digit), sort that list, and strip the .c extension. If we have files test001.c to test028.c in the directory, TESTS is set to the array [test001, test002, … test028].

Remember, the first rule in a Makefile is the default rule–the rule that gets selected if you just run make. Traditionally, this is the all rule, which simply causes all other rules to be compiled. Here, though, we instead instruct the user to explicitly run either make check or make check-all.