GNU make basics

Larger C programs consist of several source and header files. GNU make is used to compile changed files into binary objects and link those into the executable program.

Let’s consider the following example. Here’s our minimalistic data access layer:

data.h

#define NUMBER 69
int number(void);

data.c

#include "data.h"
int number(void) {
    return NUMBER;
}

This is our business logic, about as minimalistic:

func.h

int sum(int a, int b);

func.c

int sum(int a, int b) {
    return(a + b);
}

And our main program:

main.c

#include <stdio.h>
#include "data.h"
#include "func.h"
 
int main(void) {
    printf("Result: %d\n", sum(number(), 8));
}

We can compile all this, using -I to specify the path to our header files:

gcc -I . -o prog *.c

We end up with an executable:

$ ls
data.c	data.h	func.c	func.h	main.c	prog
$ ./prog
Result: 77
$

Alternatively, we could compile every single source file to a binary object and link these objects into the final executable:

gcc -I . -c data.c
gcc -I . -c func.c
gcc -I . -c main.c
gcc -o prog *.o

This process can be automated by ‘make’. A ‘makefile’ (or ‘Makefile’) defines all the rules needed to compile our artifact. Only changed source files will be re-compiled.

makefile

CC	= gcc
CFLAGS	= -I .
INCL	= data.h func.h
OBJ	= data.o func.o main.o
 
prog: $(OBJ)
	$(CC) $(CFLAGS) -o prog $(OBJ)
 
%.o: %.c $(INCL)
	$(CC) $(CFLAGS) -c -o $@ $<;
 
clean:
	rm *.o prog

First common constants are defined for the compiler, the command line options, the header files and the binary objects.

Rules are in the form: ‘target: dependencies’. So for target ‘prog’ the objects defined in OBJ are needed. The next line describes how to make them. These lines need to be indented with tab characters, not spaces!

All object files (%.o) depend on their C source (%.c) and header files (INCL). Two automatic variables are used here. $@ refers to the current target matching the wildcard (e.g. data.o). $< refers to the first prerequisite (e.g. data.c).

A common target is ‘clean’, which removes all compilation products. Another common target would be ‘install’, to copy all needed artifacts to their proper places in the filesystem.

The first rule is the default rule, so all we have to do here is type ‘make’:

$ make
gcc -I . -c -o data.o data.c
gcc -I . -c -o func.o func.c
gcc -I . -c -o main.o main.c
gcc -I . -o prog data.o func.o main.o
$