The Ultimate Guide to Makefile

Click the blue text

The Ultimate Guide to Makefile

Follow us

What is Makefile?

Perhaps many Windows programmers do not know this thing, because those Windows IDE do this work for you, but a good and professional programmer still needs to understand makefile. It’s like there are so many HTML editors now, but if you want to become a professional, you still need to understand the meaning of HTML tags. Especially in software compilation under Unix, you cannot avoid writing makefile yourself. “Whether you can write a makefile indicates whether a person has the ability to complete a large project”. Because makefile relates to the compilation rules of the entire project.

Last issue “Using GCC to Create a Library for You” has detailed the process of GCC compilation and linking, so let’s talk about the art of makefile next.

Introduction to Makefile

The make command requires a Makefile file when executed, to tell the make command how to compile and link the program.

The rules of makefile are:

1. If this project has not been compiled, then all our C files need to be compiled and linked.

2. If some C files in this project have been modified, then we only compile the modified C files and link the target program.

3. If the header files of this project have been changed, then we need to compile the C files that reference these header files and link the target program.

As long as our Makefile is written well enough, all of this can be completed with just one make command, which will automatically and intelligently determine which files need to be recompiled based on the current file modification status, thus compiling the necessary files and linking the target program.

Rules of Makefile

Before discussing this makefile, let’s take a rough look at the rules of makefile.

target ... : prerequisites ...
    command
    ...
    ...

“target”

Can be an object file (target file), or an executable file.

“prerequisites”

Files and/or target that the target depends on.

“command”

The command to be executed for the target (any shell command).

An Example

First, let’s use the test code from the last issue “Compilation and Linking, You Still Don’t Know How to Use GCC to Generate Standard Library”

div.c add.c div.c mult.c sub.c head.h

├── Calculator
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c

So we need to compile the example code into the target file app using makefile.

First Version

app:sub.o mult.o div.o add.o main.o
 gcc sub.o mult.o div.o add.o main.o -o app
sub.o:sub.c
 gcc -c sub.c
mult.o:mult.c
 gcc -c mult.c
div.o:div.c
 gcc -c div.c
add.o:add.c
 gcc -c add.c
main.o:main.c
 gcc -c main.c
# Pseudo-instruction
.PHONY:clean
clean:
 rm -f sub.o mult.o div.o add.o main.o app

Execute the make command to see the effect, very nice

[root@Calculator]# make
gcc -c sub.c
gcc -c mult.c
gcc -c div.c
gcc -c add.c
gcc -c main.c
gcc sub.o mult.o div.o add.o main.o -o app

Think: Why write so complicated, first generate .o and then generate .c

Simply put: “Convenient for compilation and linking”

Small experiment: Modify the content in add.c, just press a space, then save and exit and execute the make command

[root@Calculator]# make
gcc -c add.c
gcc sub.o mult.o div.o add.o main.o -o app

The result is obvious: only add.c was compiled, skipping unnecessary compilation steps.

Why is this? Then we need to talk about how make works.

How Make Works

In the default mode, that is, when we only enter the make command, then,

  1. make will look for files named “Makefile” or “makefile” in the current directory.
  2. If found, it will find the first target file (target) in the file, in the above example, it will find the file “app” and take this file as the final target file.
  3. If the app file does not exist, or the modification time of the files that app depends on is newer than the edit file, then it will execute the defined commands to generate the edit file, saving unnecessary compilation steps.
  4. If the edit file depends on .o files that also do not exist, then make will look for dependencies for .o files in the current file, if found, it will generate the .o file based on that rule. (This is somewhat like a stack process)
  5. Of course, your C files and H files exist, so make will generate .o files, and then use .o files to generate make’s ultimate task, which is to execute the edit file.

This is the entire dependency of make, which will look for file dependencies layer by layer until the first target file is compiled.

The above is still a simple makefile, belonging to “explicit rules”, so to optimize makefile we introduce “implicit rules”

Using Variables in Makefile

In Makefile, we need to define a series of variables, which are generally strings, somewhat “similar to macros in C language”, when Makefile is executed, the variables will be expanded to the corresponding reference position, you can directly understand the variables as macros in C language.

There are four ways to define (assign) variables in Makefile:

1. Simple assignment ( := ) is the conventional assignment method in programming languages, which is only valid for the current statement (recommended to use)

2. Recursive assignment ( = ) assignment statements may affect multiple variables, all target variables related to other variables are affected

3. Conditional assignment ( ?= ) defines the variable using the value in the symbol if the variable is not defined. If the variable has already been assigned, this assignment statement is invalid.

4. Append assignment ( += ) adds a new value to the original variable separated by spaces

Using variables is very simple, the variable needs to be given an initial value when declared, and when used, you need to add a $ symbol before the variable name, but it is best to include the variable in parentheses () or braces {}.

OBJ:=main.o # Define variable
 # Reference variable
${OBJ}   
 # Use variable
$(OBJ)  # Recommended use

In addition to user-defined variables, makefile also provides predefined variables

In the commands of implicit rules, basically, some pre-set variables are used. You can change the values of these variables in your makefile, or pass these values on the make command line, or set these values in your environment variables

Command Variables

Variable Default Command Meaning
AR Default command is ar Library packaging program.
CC Default command is cc C language compiler.
CXX Default command is g++ C++ language compiler.
CPP Default command is $(CC) –E C program preprocessor (output is standard output device).
RM Default command is rm –f Delete file command.

Command Parameter Variables

Command Meaning
CFLAGS C language compiler parameters.
CXXFLAGS C++ language compiler parameters.
CPPFLAGS C preprocessor parameters
LDFLAGS Linker parameters. (e.g.: ld)

Implicit Rules

If we want to define a series of similar files, we naturally think of using wildcards.

Wildcards

Symbol Meaning
% Any one
Matches one character
* All

GNU’s make is powerful; it can automatically infer the files and the commands behind the file dependencies.

For example:

As long as make sees a .o file, it will automatically add the .c file to the dependencies, if make finds a main.o, then main.c will be the dependency file of main.o.

OBJ:=sub.o mult.o div.o add.o main.o
$(TARGET):$(OBJ)
 ${CC} $(OBJ) -o $(TARGET)

If there are .o files but no .c files, makefile will automatically infer the generation of .o files.

In addition to wildcards, makefile also provides automatic variables for automatic inference.

Automatic Variables

Symbol Meaning
$@ Represents the target file
$^ Represents all dependency files
$< Represents the first dependency file

Thus the second version is out

Second Version

CC:=gcc
TARGET:=app     # Target variable
OBJ:=sub.o mult.o div.o add.o main.o
$(TARGET):$(OBJ)
 ${CC} $(OBJ) -o $(TARGET)
.PHONY:clean
clean:
 $(RM) $(OBJ) $(TARGET)

Isn’t it much more concise?

Pseudo-instructions

In both the first and second versions of the makefile, I have written the rule .PHONY:clean, and when making, this rule is not executed. In fact, .PHONY means that clean is a “phony target”, and it does not exist in the execution commands of make, it will only be executed when specified, such as: make clean

A more robust way of writing pseudo-instructions is:

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

Adding a small minus sign before the rm command means that if some files have issues, don’t worry, continue with the following tasks.

Functions

In Makefile, functions can be used to process variables, making our commands or rules more flexible and intelligent. Below are three commonly used functions.

Text Processing Function

“wildcard”

$(wildcard PATTERN...)

Function: This function expands to a list of all files that exist and match this pattern, separated by spaces.

“Example”

Get a list of all .c files in the working directory

SRC:=$(wildcard *.c)

String Replacement Function

“patsubst”

$(patsubst <pattern>,<replacement>,<text>)

Function: Find words in <text> (words separated by “space”, “Tab” or “newline”) that match the pattern <pattern>, if matched, replace with <replacement>.

“Example”

SRC:=$(wildcard *.c)
OBJ:=$(patsubst %.c,%.o,$(SRC)) # Replace .c files in SRC with .o files

Shell Function

The shell function is different from other functions. As the name suggests, its parameter should be the command of the operating system Shell.

$(shell <option>)

“Example”

SRC = $(shell find . -name "*.c")

Assign all files with a suffix of “.c” in the current directory and its subdirectories to SRC, separated by spaces.

Final Version

First, let’s summarize what has been discussed so far

The Makefile mainly contains five things: “Explicit Rules”, “Implicit Rules”, “Variable Definitions”, “Functions”, “Comments”.

  1. Explicit rules. Explicit rules specify how to generate one or more target files. This is clearly indicated by the writer of the Makefile, including the files to be generated, the files’ dependencies, and the commands to generate them.
  2. Implicit rules. Because our make has the function of automatic inference, implicit rules allow us to write Makefile more concisely, which is supported by make.
  3. Variable definitions. In Makefile, we need to define a series of variables, which are generally strings, somewhat like macros in C language; when Makefile is executed, the variables will be expanded to the corresponding reference positions.
  4. Functions. This section mainly introduces three functions: one is to extract the list of all .c files in the working directory, another is to convert the extracted .c list into .o list, and finally the shell function, which can execute any shell operation.
  5. Comments. Makefile only has line comments, like UNIX shell scripts, its comments are marked with the # character, similar to // in C/C++. If you want to use the # character in your Makefile, you can escape it with a backslash, like: \#.

Then “Final Version Determined” can be used as a “Template”

TARGET=app
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c,%.o,$(SRC))
DEFS =-DDEBUG
CFLAGS =-g 
CC =gcc
LIBS =  
$(TARGET):$(OBJ)
 $(CC) $(CFLAGS) $(DEFS) -o $@ $^ $(LIBS)
.PHONY:
clean:
 rm -rf *.o $(TARGET)

Multi-directory Makefile

As a robust makefile, how can all the code be placed in one folder? Excellent engineers place modules in a standardized manner.

First, let’s look at the directory tree structure

├── add
│   ├── add.c
│   └── Makefile
├── div
│   ├── div.c
│   └── Makefile
├── include
│   └── head.h
├── main.c
├── Makefile
├── Makefile.build
├── mult
│   ├── Makefile
│   └── mult.c
└── sub
    ├── Makefile
    └── sub.c

The Makefile of the sample program is divided into three categories:

  1. Top-level directory Makefile
  2. Top-level directory Makefile.build
  3. Makefile for each subdirectory

“Makefile for Each Subdirectory”

This is the simplest, just need to obj-y+= to add all .o files or subdirectories, for example, the makefile in the sub folder

obj-y += sub.o

“Top-level Directory Makefile”

In addition to defining obj-y to specify the files to be included in the program from the root directory and the subdirectories, it mainly defines the toolchain and compilation parameters

CFLAGS = -g       # Compiler parameters
CFLAGS += -I $(shell pwd)/include # Specify the include search directory
LDFLAGS := -lm      # Linker parameters

export CFLAGS LDFLAGS 

TOPDIR := $(shell pwd)
export TOPDIR

TARGET := app

obj-y += main.o
obj-y += sub/
obj-y += mult/
obj-y += div/
obj-y += add/

all : 
 make -C ./ -f $(TOPDIR)/Makefile.build
 $(CC) $(LDFLAGS) -o $(TARGET) built-in.o

clean:
 rm -f $(shell find -name "*.o")
 rm -f $(TARGET)

distclean:
 rm -f $(shell find -name "*.o")
 rm -f $(shell find -name "*.d")
 rm -f $(TARGET)

“Top-level Directory Makefile.build”

This is the most complex part; its function is to compile all the necessary files from a certain directory and its subdirectories into .o files.

This is not easy to demonstrate, but they are templates that can be used directly by modifying some parameters. You can send the keyword makefile to get the “Test Source Code” and “Makefile Files”.

Keyword: Tetris

End

Author: Meng Fan
As long as there is a dream, it will not be ordinary~
Your views are the greatest affirmation of me,
Can you give a thumbs up~
The Ultimate Guide to Makefile
Programming Learning Base
Come back to the base often

Leave a Comment