Why make?

Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles, which specify how to derive the target program. It avoids the need to manually type everything out every time you want to compile your code.

Syntax

Make has the following syntax:

target: dependencies
    command

For example, if you have a file called hello.c and you want to compile it into an executable called hello, you can write a Makefile like this:

hello: hello.c
    gcc hello.c -o hello

This Makefile has one target, hello, which depends on hello.c. The command to build the target is gcc -o hello hello.c. We could type make hello in the terminal to build the target.

More than one file?

If hello.c depends on foo.c and bar.c, you can write a Makefile like this:

hello: hello.c foo.o bar.o
    gcc hello.c foo.o bar.o -o hello

foo.o: foo.c
    gcc -c foo.c -o foo.o

bar.o: bar.c
    gcc -c bar.c -o bar.o

When we compile using gcc -c, it compiles the source file into an object file instead of an executable. The .o file is a intermediate file that contains machine code but is not yet linked into an executable.

When we run make hello, make will check if everything is up to date. If hello.c is newer than hello, make will run the command to build hello. If foo.c is newer than foo.o (or if foo.o doesn’t exist), make will run the command to build foo.o, but you need to provide the command to build foo.o in the Makefile.

Clean up

When we want to publish our code, we don’t want to include the object files or the executable files. We can add a target called clean to remove the object files:

hello: hello.c foo.o bar.o
    gcc hello.c foo.o bar.o -o hello

foo.o: foo.c
    gcc -c foo.c -o foo.o

bar.o: bar.c
    gcc -c bar.c -o bar.o

clean:
    rm *.o main

Now we can run make clean to remove the object files and executable files.

Two main functions?

If we have two main functions in your code, hello.c and world.c, we can’t compile them into one executable. We can compile them into two separate executables like this:

all: hello world

hello: hello.c foo.o bar.o
    gcc hello.c foo.o -o hello

world: world.c foo.o bar.o
    gcc world.c bar.o -o world

foo.o: foo.c
    gcc -c foo.c -o foo.o

bar.o: bar.c
    gcc -c bar.c -o bar.o

clean:
    rm *.o hello world

Now we can run make all to build both hello and world.

Constants

If we decide to change the compiler from gcc to clang, we would have to change every instance of gcc in the Makefile. We can use constants to avoid this:

CC = gcc

all: hello world

hello: hello.c foo.o bar.o
    $(CC) hello.c foo.o -o hello

world: world.c foo.o bar.o
    $(CC) world.c bar.o -o world

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

bar.o: bar.c
    $(CC) -c bar.c -o bar.o

clean:
    rm *.o hello world

This also works for flags:

CC = gcc
DBGFLAGS = -g -D_DEBUG_ON_
OPTFLAGS = -O2

hello_opt: hello.c foo_opt.o
    $(CC) $(OPTFLAGS) hello.c foo_opt.o -o hello_opt

foo_opt.o: foo.c
    $(CC) $(OPTFLAGS) -c foo.c -o foo_opt.o

hello_dbg: hello.c foo_dbg.o
    $(CC) $(DBGFLAGS) hello.c foo_dbg.o -o hello_dbg

foo_dbg.o: foo.c
    $(CC) $(DBGFLAGS) -c foo.c -o foo_dbg.o