Skip to main content
Compilation Toolchain
Compilation Toolchain
IOT
Rx 2h

Practical #1: Makefile

Objectives

By the end of this session, you will be able to:

  • Compile a multi-file C project manually and understand each step
  • Write a complete Makefile with explicit rules and variables
  • Refactor rules using automatic variables and pattern rules
  • Set up automatic dependency tracking for header files

Starter Code

Create a directory tp_makefile/ with the following project structure — a small data-processing program that reads numbers and computes statistics:

tp_makefile/
├── main.c
├── stats.c
├── stats.h
├── io.c
└── io.h

io.h

#ifndef IO_H
#define IO_H

#define MAX_VALUES 100

// Read integers from stdin until EOF or MAX_VALUES reached.
// Returns the number of values read.
int read_values(int values[], int max);

// Print an array of integers, one per line.
void print_values(const int values[], int count);

#endif

io.c

#include <stdio.h>
#include "io.h"

int read_values(int values[], int max) {
    int count = 0;
    while (count < max && scanf("%d", &values[count]) == 1) {
        count++;
    }
    return count;
}

void print_values(const int values[], int count) {
    for (int i = 0; i < count; i++) {
        printf("  [%d] %d\n", i, values[i]);
    }
}

stats.h

#ifndef STATS_H
#define STATS_H

// Return the sum of count integers.
int stats_sum(const int values[], int count);

// Return the minimum value.
int stats_min(const int values[], int count);

// Return the maximum value.
int stats_max(const int values[], int count);

// Return the arithmetic mean (integer division).
int stats_mean(const int values[], int count);

#endif

stats.c

#include "stats.h"
#include <limits.h>

int stats_sum(const int values[], int count) {
    int s = 0;
    for (int i = 0; i < count; i++) {
        s += values[i];
    }
    return s;
}

int stats_min(const int values[], int count) {
    int m = INT_MAX;
    for (int i = 0; i < count; i++) {
        if (values[i] < m) m = values[i];
    }
    return m;
}

int stats_max(const int values[], int count) {
    int m = INT_MIN;
    for (int i = 0; i < count; i++) {
        if (values[i] > m) m = values[i];
    }
    return m;
}

int stats_mean(const int values[], int count) {
    if (count == 0) return 0;
    return stats_sum(values, count) / count;
}

main.c

#include <stdio.h>
#include "io.h"
#include "stats.h"

int main(void) {
    int values[MAX_VALUES];

    printf("Enter integers (Ctrl+D to finish):\n");
    int count = read_values(values, MAX_VALUES);

    if (count == 0) {
        printf("No values entered.\n");
        return 1;
    }

    printf("\n%d values read:\n", count);
    print_values(values, count);

    printf("\nStatistics:\n");
    printf("  Sum  = %d\n", stats_sum(values, count));
    printf("  Min  = %d\n", stats_min(values, count));
    printf("  Max  = %d\n", stats_max(values, count));
    printf("  Mean = %d\n", stats_mean(values, count));

    return 0;
}

Create all these files, then move on to Exercise 1.


Exercise 1 — Manual Compilation

Difficulty: Easy

The goal is to compile the project by hand to understand every step before automating with make.

Step 1: Compile each source file to an object file

gcc -Wall -Wextra -c main.c -o main.o
gcc -Wall -Wextra -c stats.c -o stats.o
gcc -Wall -Wextra -c io.c -o io.o
Info

What does -c do?

The -c flag tells gcc to compile and assemble, but not link. It produces an object file (.o) from each source file.

gcc main.o stats.o io.o -o datastats

Step 3: Test

echo "10 20 30 40 50" | ./datastats

Questions

  1. How many .o files were created? What does each one contain?
  2. Modify only stats.c (e.g. add a comment). Which commands do you need to re-run to get an updated executable? Which ones can you skip?
  3. What happens if you modify stats.h? Which files need recompilation?
Warning

Key takeaway

When a single file changes, you only need to recompile that file and re-link. This is exactly the problem make solves: it tracks dependencies and only rebuilds what is necessary.


Exercise 2 — Basic Makefile

Difficulty: Rx

Create a Makefile that automates the manual steps from Exercise 1.

Step 1: Variables

Define the following variables at the top of your Makefile:

CC      = gcc
CFLAGS  = -Wall -Wextra -g
TARGET  = datastats

Step 2: Rules with explicit dependencies

Write rules for each target. Remember: recipes must be indented with a tab character (not spaces).

# Final executable depends on all object files
$(TARGET): main.o stats.o io.o
	$(CC) $^ -o $@

Now write similar rules for each .o file with its dependencies:

  • main.o depends on main.c, io.h, stats.h
  • stats.o depends on stats.c, stats.h
  • io.o depends on io.c, io.h

Step 3: Add a clean target

clean:
	rm -f *.o $(TARGET)

.PHONY: clean
Info

.PHONY

Marking clean as .PHONY tells make that clean is not a real file — it should always run the recipe when requested, even if a file named clean happens to exist.

Verification

make clean
make
echo "5 15 25" | ./datastats

Modify stats.c, run make again, and verify that only stats.o and the final executable are rebuilt (not io.o or main.o).


Exercise 3 — Automatic Variables & Pattern Rules

Difficulty: Rx

Your Makefile from Exercise 2 has a lot of repetition. Let’s refactor it.

Step 1: List object files with a variable

SRCS = main.c stats.c io.c
OBJS = $(SRCS:.c=.o)

The substitution reference $(SRCS:.c=.o) replaces .c with .o for every word in SRCS.

Step 2: Replace individual .o rules with a pattern rule

Instead of writing one rule per .o file, use a pattern rule:

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
VariableMeaning
$@The target (e.g. stats.o)
$<The first prerequisite (e.g. stats.c)
$^All prerequisites

Step 3: Update the linking rule

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $@

Verification

Your complete Makefile should now be around 15 lines (excluding comments). Run:

make clean && make
Warning

Missing header dependencies

With the pattern rule %.o: %.c, make no longer knows that main.o depends on io.h and stats.h. If you modify a header, the affected .o files will not be rebuilt. Exercise 4 solves this problem.


Exercise 4 — Automatic Dependency Tracking

Difficulty: Hard

GCC can generate dependency information automatically using -MMD. This creates .d files alongside .o files that list header dependencies.

Step 1: Add -MMD to CFLAGS

CFLAGS = -Wall -Wextra -g -MMD

Step 2: Include the generated dependency files

Add these lines at the end of your Makefile:

DEPS = $(OBJS:.o=.d)
-include $(DEPS)
Info

Why -include and not include?

The -include directive (with a dash) silently ignores missing files. On the first build, no .d files exist yet — using include (without dash) would cause an error.

Step 3: Update clean to remove .d files

clean:
	rm -f $(OBJS) $(DEPS) $(TARGET)

Verification

  1. Run make clean && make
  2. Inspect one of the .d files: cat main.d — you should see main.o: main.c io.h stats.h
  3. Add a blank line to stats.h, then run make. Both main.o and stats.o should be recompiled (because both include stats.h), but not io.o.

Your Makefile is now fully generic: adding a new .c file only requires updating the SRCS variable.


Bonus — Build Modes and Utility Targets

Difficulty: Hard

Enhance your Makefile with the following features:

Debug and Release modes

Add two targets that set different compiler flags:

debug: CFLAGS += -O0 -DDEBUG -fsanitize=address
debug: $(TARGET)

release: CFLAGS += -O2 -DNDEBUG
release: $(TARGET)
Info

Target-specific variables

The syntax target: VARIABLE += value sets a variable only when building that specific target and its prerequisites. This is a Make feature called target-specific variable values.

run target

run: $(TARGET)
	@echo "3 7 1 9 4" | ./$(TARGET)

valgrind target

valgrind: debug
	@echo "3 7 1 9 4" | valgrind --leak-check=full ./$(TARGET)

Don’t forget to add all new targets to .PHONY.


Summary

ConceptWhat you learned
Manual compilationgcc -c to compile, gcc *.o to link
Basic MakefileTargets, prerequisites, recipes
VariablesCC, CFLAGS, TARGET, OBJS
Automatic variables$@, $<, $^
Pattern rules%.o: %.c — one rule for all .o files
Dependency tracking-MMD + -include $(DEPS)
.PHONYDeclare non-file targets
Info

Useful make flags

  • make -n — dry run: print commands without executing them
  • make -j4 — parallel build with 4 jobs
  • make -B — unconditionally rebuild all targets