How to Write a Makefile with Ease

on Reading Time: 4 minutes

1
New Feature: Inline Commenting! Please leave your thoughts.x
Makefiles provide a way to organize build steps involved in C / C++ project compilation. This article explains how you can set up your own makefile for your C / C++ project.

Why Use a Makefile?

Usual compilation with g++ will involve a command as follows.

g++ -o program main.cc

The command will compile each C++ source file and create object files. If you have additional include files, -I flag can be used to specify the include directory containing .h / .hpp header files. -L flag can be used to specify additional library locations and you will have to use -l flag to specify which library needs to be linked (eg:- -lpthread).

However, when you have a complicated compilation process, retyping the compile command becomes cumbersome and it also recompiles all your object binaries (even the source files you did not modify since the last compilation). In contrast, a Makefile only compiles the source files that have been modified since the last compile time saving compute time.

Make sure to align your Makefile using tabs.


Makefile Basics

In this tutorial, I will provide a step by step guide to build a fully equipped Makefile.

makefile basic examples
Figure 1: Makefile Basic Examples

Here’s example 1 as a copiable block 😉. Example 1 demonstrates how the compile and link command can be in one step using g++.

0
Do you think having copiable code blocks is a good idea?x
CC=g++
CFLAGS=-std=c++11

main:
	$(CC) -o program main.cc $(CFLAGS)

But more often than not, you will find that you need to have multiple rules to compile a program binary and a test binary. Example 2 explains how you can use multiple rules.

CC=g++
CFLAGS=-std=c++11

main:
	$(CC) -o program main.cc $(CFLAGS)

test:
	$(CC) -o program test.cc $(CFLAGS)

Makefile with Step Compilation

In this example, we will see how we can separate out compilation steps for each object file. In figure 2, when we run the command make, the main rule is executed (Step 1 in Figure 2). Make meets with the need to build functions.o. Thus, scans through the rules to figure out whether there is a matching rule. When met with functions.o rule, the second step begins (Step 2 in Figure 2).

In Step 2, Make replaces the symbols appropriately as shown. $< is replaced with the right hand side (this is to your right) variable name, and $@ is replaced by the left hand side.

After the variable replacement, Make creates the object file by compiling using g++. Make, moves to the first rule (“main”) to perform the final compile and link step. Again $^ is replaced with the first element on the right hand side of the colon. After replacing, Make performs the link step and generates the binary.

makefile with separated compilation steps
Figure 2: Makefile with Separated Compilation Steps
CC=g++
CFLAGS=-std=c++11

all: main

functions.o: functions.c
    $(CC) -c -o $@ $< -I functions.h $(CFLAGS)

main: functions.o
    $(CC) -o program $^ main.cc $(CFLAGS)

Advanced Usage

In Figure 3, we can see a Makefile which includes compiling binary files in a separate object directory because when you have many source files, compiling all object files to the main build directory is not the best idea.

Make executes the $(TARGET) rule in step 1 and as explained earlier, it moves to figure out what $(OBJ) means, in example 4. In step 2, a pattern substitution is made using patsubst, replacing to create the concatenated file name bin/functions.o. This specifies the output object file name as well as the directory association.

In step 3, Make performs actual compilation having resolved all directory names and moves to $(TARGET) rule to final linking.

makefile with advanced usage
Figure 3: Makefile with Advanced Usage
CC=g++
CFLAGS=-std=c++11
TARGET=program

INCLUDE_DIR=include
ODIR=bin

all: $(TARGET)

OBJ_DEPS = functions.o
OBJ = $(patsubst %,$(ODIR)/%, $(OBJ_DEPS))

$(ODIR)/%.o: %c
    $(CC) -c -o $@ $< -I $(INCLUDE_DIR) $(CFLAGS)

$(TARGET): $(OBJ)
    $(CC) -c -o $(TARGET) $^ main.cc $(CFLAGS)

.PHONY
    clean

clean:
    rm -rf $(ODIR)/*.o *~ $(TARGET)

That’s it!

Let me know if you have further questions. This article is part of a Computer Architecture collection. If you would like to read more about Computer Architecture, please use the tag filter and architecture category.


Here’s a summary created for SEO optimizations.

Time needed: 15 minutes.

How to create a Makefile

  1. Add the Main Build Step as Follows

  2. Separate out Build Steps

    Separate out the compile steps to have more control as shown in Figure 2.

  3. Run the Make Command

    All compile steps are now integrated.

Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Mike
Mike
1 month ago

This is a very helpful post! Thank you!