Understanding Makefile Dependencies

2 min

make is a simple and useful tool that controls the building of your source code. It can be used in any programming language and is particularly effective at reducing repetitive tasks. For example, if you have multiple shell tasks listed one by one, you can use make to organize the workflow efficiently.

When you download an open-source project or join a new project as a developer, the first step is often to build the project. Nearly all open-source projects include a Makefile to organize the build process. However, understanding and modifying a Makefile can be challenging, especially for beginners. The official manual is long and complex, making it difficult for new users to grasp the concepts quickly. While it is possible to find Makefile tutorials online and copy-paste code, this approach does not help in truly understanding how Makefiles work.

This article will help you understand the core concept of make in just 10 minutes. By the end, you will be able to create your own Makefile to run basic tasks and understand how make works, enabling you to manage projects more efficiently and reduce repetitive tasks.

Core Concept of Makefile Dependencies

Dependency is the core concept of make. Understanding dependencies is the most important principle in make. In a Makefile, every task depends on other prerequisites, which can be files, tasks, or some other form of rules. If you want to run a task, you must define the target and prerequisites. We call this a dependency. A simple Makefile consists of “dependencies” with the following structure:

foo: bar

The above Makefile does nothing because the bar target is undefined. A target should depend on a defined prerequisite or have a defined way of working. The following target is defined to run the echo command:

foo:
    echo "hello world"  #noticed: you must use 'tab' to start this line.

In this example, foo is the target, and there are no prerequisites. When make runs, it executes the command associated with the foo target, which in this case is echo hello world.

Real-World Example

Let’s dive into a real-world example to see how make and Makefile dependencies work in practice. Suppose we have a simple C project with the following files:

.
├── http.c
├── http.h
├── main.c
├── socket.c
├── socket.h

In our project, the dependency relationships are as follows:

  • http.c includes http.h and socket.h.
  • socket.c includes socket.h.
  • main.c includes socket.h and http.h.

To build an executable file with gcc, we must compile the source code into object files and then link them to create an executable file. Here are the rules for our Makefile:

http.o: http.c http.h socket.h
    gcc -c http.c
socket.o: socket.h
    gcc -c socket.c
main.o: main.c http.h socket.h
    gcc -c main.c

In the above rules:

  • http.o is the target, and it depends on http.c, http.h, and socket.h. The command to compile it is gcc -c http.c.
  • socket.o is the target, and it depends on socket.c and socket.h. The command to compile it is gcc -c socket.c.
  • main.o is the target, and it depends on main.c, http.h, and socket.h. The command to compile it is gcc -c main.c.

To link all object files into an executable file, we define the following rule:

exe: http.o socket.o main.o
    gcc -o exe http.o socket.o main.o

Now, you can type make exe to build your project. This command will execute the necessary steps to compile the object files and then link them into the exe executable.

Hope it's clear to you how Makefile Dependencies work. Experimenting with creating and modifying Makefiles will deepen your understanding and improve your ability to handle complex build processes.

If you have any questions or comments, please feel free to leave a comment below.


Note: This post was originally published on liyafu.com (One of our makers' personal blog)