Makefile

Photo by Lanju Fotografie on Unsplash
Photo by Lanju Fotografie on Unsplash
Makefile is the most commonly used compilation tool in Linux. Stuart Feldman created it at Bell Labs in 1967. Although it may be older than you and me, it is still active nowadays.

Makefile is the most commonly used compilation tool in Linux. Stuart Feldman created it at Bell Labs in 1967. Although it may be older than you and me, it is still active nowadays. This article will introduce some basic syntax of Makefile.

Rules

Makefile actually consists of many rules. A rule can depend on other rules. Therefore, we can write complex rules to compile our projects. The syntax of a rule is as follows:

targets ... : prerequisites ...
        recipes
        ...
  • targets: One or more file names, separated by spaces. These files are often executable or object files.
  • prerequisites: One or more file names, separated by spaces. These files are often input files to generate targets, such as source files. That is, targets depend on prerequisites.
  • recipes: One or more actions. These actions often generate target files.

When prerequisites are changed, recipes will be executed to generate targets. It is based on the file modification time of targets and prerequisites. If the time of prerequisites is after targets, it means that prerequisites has been changed.

For example, in the following Makefile, targets are the hello executable file, prerequisites are the hello.c source code file, and recipes use gcc to compile hello.c into hello.

hello : hello.c
        gcc -c hello.c -o hello

When we execute make hello in the command line, it will execute the rule whose target is hello in the Makefile. When it finds that hello.c has been changed, that is, when the file modification time of hello.c is after hello, it will execute gcc to regenerate hello.

Variables

Makefile allows us to declare variables. The value of a variable is a string, and you can use variables in targets, prerequisites, and recipes. When using variables, we must wrap the variable with $() or ${}, such as $(foo) or ${foo}.

Makefile provides several ways to set values ​​to variables.

OperatorsDescirptions
=Recursively expanded variable assignment
:=Simply expanded variable assignment
?=Conditional variable assignment
+=Appending more text
Variable Assignments

Recursively Expanded Variable Assignment

Suppose variable A contains a reference to variable B. When we get the value of variable A, the reference of variable B will be expanded, even if variable B is declared after variable A. This is recursively expanded variable assignment, and the symbol used is =.

foo = foo $(bar)
bar = bar $(ugh)
ugh = Huh?

all:
        echo $(foo)

# Output:
# foo bar Huh?

Simply Expanded Variable Assignment

Suppose variable A contains a reference to variable B, and variable B is declared after variable A. When variable A is declared, the reference to variable B is expanded. But at this time, variable B has not been declared yet, so it is expanded to an empty string. After that, even if variable B is declared, the value of variable A will not change anymore. Therefore, the value of variable A is fixed when it is declared, which is different from recursively expanded variable assignment. The symbol used for simply expanded variable assignment is :=.

foo := foo $(bar)
bar := bar $(ugh)
ugh := Huh?

all:
        echo $(foo)

# Output:
# foo

Conditional Variable Assignment

Conditional variable assignment will only be executed when the variable has not been declared. The symbol it uses is ?=.

foo = foo
foo ?= $(bar)
bar = bar $(ugh)
ugh := Huh?

all:
        echo $(foo)

# Output:
# foo
foo ?= foo $(bar)
bar = bar $(ugh)
ugh := Huh?

all:
        echo $(foo)

# Output:
# foo bar Huh?

Appending More Text

If variable A has been declared and we want to append more strings, we can use the symbol +=. When using += to append a string to variable A, will it be recursively expanded variable assignment or simply expanded variable assignment? This is not determined by +=, but depends on whether = or := is used when variable A is declared.

foo = foo
foo += $(bar)
bar = bar $(ugh)
ugh := Huh?

all:
        echo $(foo)

# Output:
# foo bar Huh?
foo := foo
foo += $(bar)
bar = bar $(ugh)
ugh := Huh?

all:
        echo $(foo)

# Output:
# foo

Pattern Rules

Pattern Rules

Pattern rules are similar to ordinary rules, except that they contain the % character. % is called the wildcard character, which can match any non-empty string. For example, %.c matches any file name ending in .c. Moreover, the substring matched by % is called stem. For example, when we use %.c to match hello.c, hello is a stem.

We can use % in targets and prerequisites. When % is used in both targets and prerequisites, they will match the same stem. As in the example below, it will match hello.o and hello.c.

%.o : %.c
        recipe

Static Pattern Rules

We have learned about ordinary rules and pattern rules. The ordinary rules are strict in that they require the full file name to be specified. Relatively speaking, pattern rules are very flexible and can use wildcard characters to match all possible file names. However, static pattern rules are somewhere in between.

The syntax of static pattern rules is as follows. The targets are the same as the ordinary rules, and the full file name must be specified. Then, use target-pattern and prerequisite-patterns to match any file name in targets.

targets ... : target-pattern : prerequisite-patterns ...
        recipes
        ...

As in the example below, it will only match foo.o, foo.c, bar.o, and bar.c. It will not match hello.o and hello.c.

objects = foo.o bar.o
$(objects) : %.o : %.c
        recipes

Automatic Variables

When we use pattern rules, how do we refer the matched file names in recipes? At this time, we have to use the automatic variables of Makefile. Automatic variables are several variables predefined by Makefile. We can use them to obtain the matching targets or prerequisites. Here are some commonly used automatic variables. For others, please refer to Automatic Variables.

Automatic VariablesDescriptions
$@The name of whichever target caused the rule’s recipe to be run.
$<The name of the first prerequisite.
$?The names of all the prerequisites that are newer than the target, with spaces between them. If the target does not exist, all prerequisites will be included.
$^The names of all the prerequisites, with spaces between them, and the duplicates are removed.
$+Like $^, but but the duplicates are included.
$*The stem which a pattern rule matches.
Automatic Variables

The following example shows how to use automatic variables to write recipes for pattern rules.

EXEC = hello
OBJS = hello.o lib.o

all : $(EXEC)

$(EXEC) : $(OBJS)
        $(CC) -o $@ $^

%.o : %.c
        $(CC) -c $< -o $@ $(CFLAGS)

Recursive Use of make

Assume that there is a Makefile in the current folder, and there is also a Makefile in the subfolder. We want that after executing make in the current folder, it will recursively execute make in the subfolder. There are two ways to execute make recursively. One is to switch to the subfolder and execute $(MAKE).

subsystem:
        cd subdir && $(MAKE)

The other is to use $(MAKE) -C.

subsystem:
        $(MAKE) -C subdir

If we want to pass a variable to sub-make, we can use export.

# Top-level Makefile
foo = foo
bar = $(foo) bar

export bar

all :
        cd sub && $(MAKE)
# Makefile under sub/ directory
all :
        echo $(bar)

Functions

Makefile provides many built-in functions. We only list some commonly used functions. For other functions, please refer to Functions for Transforming Text.

FunctionsDescriptions
$(wildcard pattern …)Find all files matching the pattern.
$(subst from,to,text)Replace from by to in the text.
$(patsubst pattern,replacement,text)
$(var:pattern=replacement)
$(var:suffix=replacement)
Find words in text matching pattern, and replace them with replacement.
$(filter pattern …,text)Returns words in text matching pattern.
$(if condition,then,else)Returns then if condition is not empty; otherwise, returns else.
$(foreach var,list,text)Convert each word in list to text.
$(shell command …)Execute command.
Functions

wildcard

The wildcard function searches for all file names matching pattern in the current directory, separated by spaces.

# wildcard syntax
$(wildcard pattern ...)

# Expand all file names matching *.c, sorted.
SRCS = $(wildcard *.c)

# Expand all file names matching *.c, sorted, 
# followed by all file names matching *.h, sorted.
SRCS = $(wildcard *.c *.h)

subst

The subst function replaces from string in text with to.

# subst syntax
$(subst from,to,text)

# Replace EE with ee
txt1 = feet on the street
txt2 = $(subst ee,EE,feet on the street)
# txt2 = fEEt on the strEEt

patsubst

The patsubst function searches text for words matching pattern and replaces them with replacement.

It has two other shorthand to write it. The first way is to search for words that match pattern on a specific variable and replace them with replacement. This approach allows us to omit the patsubst name. The second method is for suffix replacement. Not only does this omit the patsubst name, it also omits the % character.

# patsubst syntax
$(patsubst pattern,replacement,text)
$(var:pattern=replacement)
$(var:suffix=replacement)

# Replace .c by .o for all source file names
SRCS = foo.c bar.c
OBJS1 = $(patsubst %.c,%.o,$(SRCS))
# OBJS1 = foo.o bar.o

# Use the first shorthand
OBJS2 = $(SRCS:%.c=%.o)
# OBJS2 = foo.o bar.o

# Use the second shorthand
OBJS3 = $(SRCS:.c=.o)
# OBJS3 = foo.o bar.o

filter

The filter function returns the words in the list that match the pattern.

# filter syntax
$(filter pattern ...,text)

# Filter out header files
SRCS = foo.c foo.h bar.c bar.h
foo : $(SRCS)
        $(CC) $(filter %.c,$(SRCS)) -o foo

if

If condition is a non-empty string, if returns then. Otherwise, if condition is an empty string, it returns else.

# if syntax
$(if condition,then,else)

con1 := not-empty
echo $(if $(con1),then part,else part)
# Output: then part

con2 :=
echo $(if $(con2),then part,else part)
# Output: else part

foreach

The foreach will convert each word in the list into textvar is the word to be converted each time.

# foreach syntax
$(foreach var,list,text)

#
foo = foo1 foo2
bar = $(foreach w,$(foo),$(w)_bar)
# bar = foo1_bar foo2_bar

# Find all file names in the list of dirs.
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

shell

The shell function allows us to execute a command, and it will replace the newline characters in the returned value with whitespace.

# shell syntax
$(shell command ...)

contents := $(shell cat hello.c)

Conditionals

Makefile also provides conditional-directives that allow us to conditionally choose whether to execute a part of Makefile. The syntax is as follows.

conditional-directive-1
text-if-1-is-true
else conditional-directive-2
text-if-2-is-true
else
text-if-1-and-2-are-false
endif

There are four conditional-directives provided by Makefile.

# If text arg1 and arg2 are equal
ifeq (arg1, arg2)

# If text arg1 and arg2 are equal
ifneq (arg1, arg2)

# If variable is defined
ifdef variable-name

# If variable is not defined
ifndef variable-name
ifeq ($(CC),gcc)
       libs = -lgnu
else
       libs =
endif

ifdef libs
       echo $(libs)
endif

foo: $(objects)
        $(CC) -o foo $(objects) $(libs)

Special Built-in Target Names

The Makefile predefines some targets. For more details, please refer to Special Built-in Target Name .

TargetsDescirptions
.PHONYPrevent Make from confusing the phony target with a file name.
.PHONY : all clean
Special Built-in Target Names

Conclusion

This article only introduces part of the Makefile syntax. There are many things that have not been introduced, such as Implicit Rules. Although many more advanced tools are now available, there are still many projects that use Makefile. Moreover, for small projects, Makefile is a good choice because make is installed on many Linux platforms.

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like
Photo by Timothée Geenens on Unsplash
Read More

x86 Memory Map

After the x86 PC boots, it will be in real mode. At this time, we can access memory below 1 MB. However, the BIOS also uses some memory. Therefore, we must know which areas the BIOS occupies in order to avoid them.
Read More
Photo by Patrick on Unsplash
Read More

x86-64 Calling Conventions

Calling conventions refers to the specifications that the two functions should follow when one function calls another function. For example, how to pass parameters and a return value ​​between them. Calling conventions are part of the application binary interface (ABI).
Read More