Understanding Makefile from a Different Perspective

Source: WeChat Official Account [Programming Gems]

Author: Mr. Watcher

ID: shouwangxiansheng

Understanding Makefile from a Different Perspective

As a C/C++ developer on Linux, it’s essential to have some exposure to Makefile. Typically, constructing large C/C++ projects relies heavily on Makefile. You might be using cmake or other similar tools, but they are fundamentally similar.

As a light user, at the request of the readers, I dare to introduce Makefile, but unlike regular Makefile tutorials, this article intends to present it from a different angle. Please feel free to point out any inaccuracies.

What Exactly is Makefile?

In Linux, for the following simple program:

// Source: WeChat Official Account [Programming Gems]
// main.c
#include <stdio.h>
#include <math.h>
int main()
{
    int a = 10;
    int b = 4;
    int c = pow(a,b);
    printf("10^4 = %d",c);
    return 0;
}

We can usually compile it using gcc to get the desired program:

$ gcc -o main main.c -lm

(If you don’t understand why you need to add -lm, please refer to “A Strange Linking Issue”).

For a simple program with a single file, a single command can complete the task (compilation + linking). However, for a complex project, there could be thousands of files that require linking to a large number of dynamic or static libraries. Just imagine, would you want to execute commands one by one? The lazy gene is ingrained in programmers.

So you might think, why not write a script? Well, that sounds much better.

With many files, you tell me which files to compile, I can traverse through them, and you tell me which libraries to link, and I can link them one by one.

However, at this point, you might wonder, since compiling and linking are similar processes, can we write some general rules for them instead of making it so complicated? Then just execute according to the rules.

And that’s what Makefile is, a rule file, and make is the interpreter of these rules. You can compare it to the relationship between shell scripts and the bash interpreter.

Thus, Makefile is not only used for compilation and linking; it is just very suitable for those purposes.

What Does Makefile Look Like?

Its most important rule syntax is as follows:

<target> : <prerequisites> 
[tab]  <commands>

At first glance, is that all there is to it? But what does it mean?

  • target: The name of the target file to be generated.

  • prerequisites: The files it depends on.

  • [tab]: Yes, it’s the tab key. Beginners often overlook this, please use tab.

  • commands: The instructions to be executed.

These are the key contents, but explaining them in detail could take a lot of time. This article will only provide a simple example. Suppose we want to copy the previous main.c file to a file named pow.c. We can get:

  • target: pow.c (the target name)

  • prerequisites: main.c, meaning pow.c needs main.c to exist.

  • commands: cp main.c pow.c

Thus, our Makefile content would look like this:

pow.c:main.c
    cp main.c pow.c
clean:
    rm pow.c

Assuming there is no main.c file in the current directory, and we execute:

$ make pow.c
make: *** No rule to make target `main.c', needed by `pow.c'.  Stop.

We find that an error occurs because the required file is missing, and there are no other rules to generate it.

Now, if we place main.c in the current directory and continue executing:

$ make 
cp main.c pow.c

See, after executing the make command, our pow.c file has finally been created.

And after executing the following command:

$ make clean
rm pow.c

You will find that pow.c has been deleted.

If there is a clean file in the current directory, what will happen?

$ make clean
make: `clean' is up to date.

As for the reason, it will be explained later.

Here, note that if your Makefile file is not named Makefile, you will need to specify the exact name. For example, if the previous file is named test.txt:

$ make -f test.txt

The above example introduces the basic process of using Makefile, generating targets, and cleaning targets. However, there are many subtleties involved, such as phony targets, automatic inference, implicit rules, and variable definitions. This article will not delve into those details.

In summary, provide rules and generate targets according to those rules.

What Does Makefile Do?

There are many tutorials online that explain how to write Makefile, and many of them are quite good. However, this article aims to present it from a different perspective.

Since we are learning about Makefile, we need to know what it should do when building C/C++ projects. Then we can learn how to write Makefile.

In fact, it is quite clear what it primarily does: it compiles and links. This has been introduced in “How the Hello Program is Compiled into an Executable File”. Friends who are not familiar with it can take a brief look. So what exactly needs to be done in Makefile?

  • Compile source code files into relocatable object files .o (refer to “The Difference Between Static and Dynamic Libraries”)

  • Set compiler options, such as whether to enable optimization, pass macros, enable warnings, etc.

  • Link static or dynamic libraries with object files.

Thus, the question becomes how to quickly use the syntax rules of Makefile to compile thousands of .c files into .o files and correctly link them to the required libraries.

So how should we write Makefile to obtain our program? To help illustrate, let’s break down the previous compilation commands into two:

$ gcc -g -Wall -c main.c -o main.o
$ gcc -o main main.o -lm

Setting the Compiler

Since we are using the gcc compiler (suite), we can write it like this:

CC=gcc

For extensibility, it is common to define the compiler as a variable, making it much more convenient to use later.

Setting Compiler Options

For instance, we want to set the -g option for debugging and the -Wall option to output more warning information.

CFLAGS=-g -Wall

Setting Link Libraries

Here we only use the libm.so library:

LIBS=-lm

Compiling

Our target file is main.o, which depends on main.c. The rule should be like this:

OBJ=main.o
$(OBJ):main.c
    $(CC) $(CFLAGS) -c main.c -o $(OBJ)

Thus, we obtain our target file.

Linking

Next, we need to link the target file with the library files.

TARGET=main
$(TARGET):main.o
    $(CC) $(CFLAGS) -o $(TARGET) $(OBJ) $(LIBS)

To use make clean, which is typically used to clear these intermediate files, we need to add a phony target clean:

.PHONY:clean
clean:
    rm $(OBJ) $(TARGET)

A phony target means it is not a real target file to be generated, and .PHONY:clean indicates that clean is a phony target. In this case, even if there is a clean file in the current directory, it will still execute the following instructions.

Otherwise, if there is a clean file in the current directory, the rm action will not execute, as it will consider the target file to be up to date.

Complete Content

CC=gcc
CFLAGS=-g -Wall
LIBS=-lm
OBJ=main.o
$(OBJ):main.c
    $(CC) $(CFLAGS) -c main.c -o $(OBJ)
TARGET=main
$(TARGET):main.o
    $(CC) $(CFLAGS) -o $(TARGET) $(OBJ) $(LIBS)
.PHONY:clean
clean:
    rm $(OBJ) $(TARGET)

As you can see, there are three targets in the Makefile: main.o, main, and clean, where clean is a phony target.

Note that since the first target is main.o, if you simply execute make, it will only generate main.o. If you execute it again, you will find it prompts you that main.o is already up to date:

$ make
gcc -g -Wall -c main.c -o main.o
$ make
make: `main.o' is up to date.

To obtain main, we execute:

$ make main
gcc -g -Wall -c main.c -o main.o
gcc -g -Wall -o main main.o -lm
$ ls 
main  main.c  main.o  makefile

Of course, you can also adjust the order of the targets. Here, the target file main depends on main.o. It will first look for main.o, find that this file is also missing, and check if there are rules to generate main.o. Well, there are indeed rules. main.o depends on main.c, and there are rules for that too. Ultimately, according to the rules, main.o will be generated first, followed by main.

If you want to clear these target files, you can execute make clean:

$ make clean
rm main.o main
$ ls 
main.c  makefile

Summary

This article mainly covers two parts.

What is Makefile?

It is a rule file written in a specific syntax, which is interpreted and executed using make, just like how a shell script is run by bash. Typically, Makefile is used to build C/C++ projects.

What Does Makefile Do When Building C/C++ Projects?

Makefile primarily performs the following tasks (using C programs as an example):

  • Use variables to store various settings, such as compilation options, compiler, macros, included header files, etc.

  • Compile .c files into .o files.

  • Link .o files with libraries.

  • Clear generated files.

  • Install programs.

The most critical tasks involve compilation and linking, which means finding a way to convert .c files into .o (relocatable object files); .o + .so (dynamic libraries) + .a (static libraries) into executable files.

For the example mentioned, it may seem somewhat clumsy to accomplish with a single command, yet writing so many lines in a Makefile points out the basic thought process when writing Makefile.

For a complex project, there are many more aspects of Makefile to introduce, such as how to set variables, how to cross-compile, how to compile multiple directories, how to perform automatic inference, how to branch, and more. These will be discussed later.

Understanding Makefile from a Different Perspective

Related Exciting Recommendations

Understanding Makefile from a Different Perspective

How to Create Your Own Static Library?

One article to help you understand static and dynamic libraries.

How is the hello program compiled?

A strange linking issue.

Have you mastered the production of dynamic libraries and the two usage methods?

Follow the WeChat Official Account [Programming Gems] to get more original technical articles on Linux/C/C++/Data Structures and Algorithms/Computer Fundamentals/Tools, etc.Get classic e-books and video resources for free in the background.

Understanding Makefile from a Different Perspective

Leave a Comment