Learning and Using Makefile: A Comprehensive Guide

1. Introduction to Makefile

Makefile is used in conjunction with the make tool to organize and manage the compilation and linking of project source code.

The make tool identifies modified files, finds the affected related files based on dependencies, and finally compiles these files according to the rules.

The Makefile records the dependencies and compilation rules.

The essence of Makefile: no matter how complex the syntax is, it is all aimed at better solving the dependency relationships between project files.

2. Introduction to Makefile Rules

A rule in Makefile consists of a target, dependencies, and commands, with the following syntax structure:

target: dependent files or other targets
	command1
	command2
	...

A rule can have multiple command lines, each command occupies one line. Note: Each command line must begin with a [Tab] character, which tells make that this line is a command line. Make performs the corresponding actions according to the commands. This is a common and subtle error when writing Makefiles.

3. Makefile Phony Targets

Phony targets do not represent a real filename; you can specify this target when executing make to perform the commands defined in its rules, and sometimes a phony target can also be referred to as a label.

There are two reasons to use phony targets:

(1) To avoid name conflicts between actual files in the working directory and targets defined in our Makefile that only execute commands (the purpose of this target is to execute a series of commands without needing to create this target).

(2) To improve the efficiency of executing make, especially for a large project, the efficiency of compilation may also concern you.

To declare a target as a phony target, it is declared as a dependency of the special target ‘.PHONY’. When writing phony target rules, you first need to declare that the target is a phony target, followed by the rule definition for the phony target. The writing format for the target “clean” should be as follows:

.PHONY: clean

clean:
	rm *.o temp

4. Variables in Makefile

In Makefile, a variable is a name (like a macro in C language) that represents a text string. When referencing a variable in the target, dependencies, or commands in Makefile, the variable is replaced by its value.

Variable names cannot include any string with “:”, “#”, “=”, leading spaces, or trailing spaces. It is best to define variable names using letters, numbers, and underscores; using other special symbols in variable names is not recommended.

Variable names are case-sensitive. The variables “foo”, “Foo”, and “FOO” refer to three different variables.

The way to reference a variable is: "$(VARIABLE_NAME)" or "${VARIABLE_NAME}" to reference the definition of a variable.

Variables in Makefile include system environment variables, custom variables, and automatic variables.

4.1. System Environment Variables

When using system environment variables, it is important to note the following points:

(1) A variable defined in Makefile or defined in the command line form of make will override any system environment variable of the same name (note: it does not change the definition of the system environment variable; the modified environment variable is only valid during the execution of make). When make is executed with the “-e” parameter, variables defined in Makefile and command line will not override environment variables of the same name; make will use the definitions from the system environment variables.

(2) During recursive calls of make, all system environment variables will be passed to the next level of make. By default, only system environment variables and variables defined via the command line will be passed to the child make process. Ordinary variables defined in Makefile need to be declared with the “export” directive to be passed to the child make.

4.2. Custom Variables

Defining custom variables in Makefile is quite simple; here is a brief distinction of the meanings of various symbols:

(1) Deferred assignment: “=”;

When defining a variable with the “=” sign, if the defined variable references other variables, the defined variable will not immediately expand the references to other variables, but will expand when the variable is actually used. For example, in the following example, only when executing echo “results inbeingexpandedto456” is the variable A’s value expanded, although A’s value was 123 before defining variable B, when executing echo “$(B)”, A’s final value is 456, thus outputting 456.

(2) Immediate assignment: “:=”;

Immediate assignment is the opposite of deferred assignment; when defining a variable with the “:=” sign, if the defined variable references other variables, the defined variable will immediately expand the references to other variables. This can be understood with the following example.

(3) Conditional assignment (empty assignment): “?=”;

When defining a variable with the “?=” sign, the variable will only be assigned if it has not been assigned previously. This can be understood with the following example.

(4) Appending assignment: “+=”;

Using the “+=” sign allows you to append to the variable’s value. This can be understood with the following example.

Example:

# Deferred assignment
A=123
B=$(A)
A=456

# Immediate assignment
C=123
D:=$(C)
C=456

# Empty assignment
E?=123
E?=456

# Appending assignment
F?=123
F+=456

.PHONY:all

all:
	echo "$(B)" #result:456
	echo "$(D)" #result:123
	echo "$(E)" #result:123
	echo "$(F)" #result:123 456

4.3. Automatic Variables

Common automatic variables in Makefile are described as follows:

$<: first dependency file;

$^: all dependency files;

$@: target;

5. Conditional Branches in Makefile

The syntax for conditional branches is as follows:

ifeq (var1,var2)
...
else
...
endif

ifneq (var1,var2)
...
else
...
endif

Example:

ARCH ?= x86

ifeq ($(ARCH), x86)
 CC=gcc
else
 CC=arm-linux-gnueabihf-gcc
endif

6. Common Functions in Makefile

Makefile also provides a variety of functions for us to use; here are a few commonly used functions.

6.1. patsubst

$(patsubst PATTERN, REPLACEMENT, TEXT)

Function name: Pattern replacement function – patsubst.

Functionality: Searches for words separated by spaces in “TEXT” and replaces those matching the “PATTERN” with “REPLACEMENT”. The parameter “PATTERN” can use the wildcard “%” to represent several characters in a word. If the parameter “REPLACEMENT” also contains a “%”, then the “%” in “REPLACEMENT” will represent the string corresponding to the “%” in “PATTERN”. In “PATTERN” and “REPLACEMENT”, only the first “%” is treated as a pattern character; subsequent occurrences are treated as characters. If you need to treat the first occurrence of “%” as a character instead of a pattern character, you can escape it with a backslash “”.

Return value: The new string after replacement. Function description: Multiple spaces between words in the parameter “TEXT” are merged into a single space, and leading and trailing spaces are ignored.

If you find the above description a bit difficult to understand, take a look at the example below, and then the description should be clearer in conjunction with the example:

.PHONY:all

all:
	echo "$(patsubst %.c, %.o, x.c.c bar.c)" #result: x.c.o bar.o

The running result of the above example: replaces words ending with .c in the string “x.c.c bar.c” with characters ending with .o. The function returns “x.c.o bar.o”.

6.2. notdir

$(notdir NAMES…)

Function name: Get filename function – notdir.

Functionality: Extracts the non-directory part from the sequence of filenames “NAMES…”. The directory part refers to the portion before the last slash (“/”). It deletes the directory part from all filenames, keeping only the non-directory part.

Return value: The non-directory part of each file in the sequence “NAMES…”.

Function description: If there are filenames in “NAMES…” that do not contain slashes, those filenames remain unchanged. Filenames ending with a slash are replaced with an empty string; therefore, when there are multiple such filenames in “NAMES…”, the number of spaces separating the filenames in the return result will be uncertain! This is a flaw of this function.

Example:

.PHONY:all

all:
	echo "$(notdir src/foo.c hacks)" #result: foo.c hacks

6.3. wildcard

$(wildcard PATTERN)

Function name: Get matching pattern filenames function – wildcard.

Functionality: Lists all filenames in the current directory that match the pattern “PATTERN”.

Return value: A space-separated list of all filenames in the current directory that match the pattern “PATTERN”.

Function description: “PATTERN” uses shell-recognizable wildcards, including “?” (single character), “*” (multiple characters), etc.

Example:

.PHONY:all

all:
	echo "$(wildcard *.c)" #result: list of all .c source files in the current directory

6.4. foreach

$(foreach VAR, LIST, TEXT)

The function “foreach” is different from other functions. It is a loop function, similar to the for statement in Linux shell.

Function name: Loop function – foreach.

Functionality: The operation of this function is as follows: if needed (there are variable or function references), the variable “VAR” and the list “LIST” are first expanded; while the variable references in the expression “TEXT” are not expanded. During execution, the words in “LIST” separated by spaces are sequentially assigned to the variable “VAR”, and then the expression “TEXT” is executed. This repeats until the last word of “LIST” (ending when empty). The variable or function references in “TEXT” are expanded during execution, so if there are references to “VAR” in “TEXT”, the value of “VAR” will be different for each expansion.

Return value: The result of computing the expression “TEXT” multiple times, separated by spaces.

Note: The parameter “VAR” in the function is a local temporary variable, which is only valid within the context of the “foreach” function; its definition does not affect the value of other parts with the same name.

Example:

.PHONY:all

dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

all:
	echo "$(files)" #result: files is a list of all files in the directories a b c d.

Example analysis: In the example, the expression for “TEXT” is $(wildcard $(dir)/*). The expression expands to $(wildcard a/*) on the first execution; expands to $(wildcard b/*) on the second execution; expands to $(wildcard c/*) on the third execution; and so on.

7. Others

Default rule: .o files are compiled using the corresponding .c files.

Welcome to follow the author’s public account:

If there are any issues in the text, please feel free to point them out, as the author’s level is limited.

If this article has helped you, remember to like and follow the author.

Note: Please indicate the source when reprinting, thank you!

Leave a Comment