Published Mar 29, 2020 - Author: zgxsin
For those who wants to understand GNU Make better, I found this article very helpful and useful.
Here I write down some essentials from the mentioned article.
Large projects can contain thousands of lines of code, distributed in multiple source files, written by many developers and arranged in several sub-directories.
Each project needs a Makefile — a script that describes the project structure, namely, the source code files, the dependencies between them, compiler arguments, and how to produce the target output (normally, one or more executables). Whenever the make command is executed, the Makefile in the current working directory is interpreted, and the instructions executed to produce the target outputs. The Makefile contains a collection of rules, macros, variable assignments, etc. (‘Makefile’ or ‘makefile’ are both acceptable.)
The general syntax of a Makefile rule is as follows:
target: dependency1 dependency2 ...
[TAB] action1
[TAB] action2
...
A simple Makefile for a small project is as follows:
all: main.o module.o
gcc main.o module.o -o target_bin
main.o: main.c module.h
gcc -I . -c main.c
module.o: module.c module.h
gcc -I . -c module.c
clean:
rm -rf *.o
rm target_bin
where we have four targets in this file:
all
is a special target that depends on main.o
and module.o
, and has the command (from the “manual” steps earlier) to make GCC link the two object files into the final executable binary.main.o
is a filename target that depends on main.c
and module.h
, and has the command to compile main.c
to produce main.o
.module.o
is a filename target that depends on module.c
and module.h
; it calls GCC to compile the module.c
file to produce module.o
.clean
is a special target that has no dependencies, but specifies the commands to clean the compilation outputs from the project directories.The make command accepts a target parameteImprovedr (one of those defined in the Makefile), so the generic command line syntax is make <target>
. However, make also works if you do not specify any target on the command line, saving you a little typing; in such a case, it defaults to the first target defined in the Makefile. In our Makefile, that is the target all
, which results in the creation of the desired executable binary target_bin
!
There are different ways of assigning variables in a Makefile. Though variable assignments can occur in any part of the Makefile, on a new line, most variable declarations are found at the beginning of the Makefile.
We can assign values (RHS) to variables (LHS) with this operator, for example: CC := gcc
. With simple assignment (:=
), the value is expanded and stored to all occurrences in the Makefile when its first definition is found.
Recursive assignment involves variables and values that are not evaluated immediately on encountering their definition, but are re-evaluated every time they are encountered in an action that is being executed.
Conditional assignment statements assign the given value to the variable only if the variable does not yet have a value.
The appending operation appends texts to an existing variable. For example:
CC = gcc
CC += -W
CC
now holds the value gcc -W
.
The %
character can be used for wildcard pattern-matching, to provide generic targets. For example:Improved
%.o: %.c
[TAB] actions
When %
appears in the dependency list, it is replaced with the same string that was used in the target.
Inside actions, we can use special variables for matching filenames. Some of them are:
$@
(full target name of the current target)$?
(returns the dependencies that are newer than the current target)$*
(returns the text that corresponds to %
in the target)$<
(name of the first dependency)$^
(name of all the dependencies with space as the delimiter)We can change the behavior of the actions we use by prefixing certain action modifiers to the actions. Two important action modifiers are:
ImprovedPrefixing this to any action causes any error that occurs while executing the action to be ignored. Looking at the Makefile from our sample: in the clean target, the rm target_bin
command will produce an error if that file does not exist (this could happen if the project had never been compiled, or if make clean
is run twice consecutively). To handle this, we can prefix the rm
command with a minus, to ignore errors: -rm target_bin
.
It suppresses the standard print-action-to-standard-output behavior of make, for the action/command that is prefixImproveded with @
. For example, to echo a custom message to standard output, we want only the output of the echo command, and don’t want to print the echo command line itself. @echo Message
will print “Message” without the echo command line being printed.
Remember the all
and clean
special targets in our Makefile? What happens when the project directory has files with the names all
or clean
? The conflicts will cause errors. Use the .PHONY
directive to specify which targets are not to be treated as files For example: .PHONY: all clean
.
At times, maybe when developing the Makefile, we may want to trace the make execution (and view the logged messages) without actually running the actions, which is time consuming. Simply use make -n
to do a “dry run”.
Sometimes we need to use the output from one command/action in other places in the Makefile — for example, checking versions/locations of installed libraries, or other files required for compilation. We can obtain the shell output using the shell command. For example, to return a list of files in the current directory into a variable, we would run: LS_OUT = $(shell ls)
.
CC = gcc # Compiler to use
OPTIONS = -O2 -g -Wall # -g for debug, -O2 for optimise and -Wall additional messages
INCLUDES = -I . # Directory for header file
OBJS = main.o module.o # List of objects to be build
.PHONY: all clean # To declare all, clean are not files
all: ${OBJS}
@echo "Building.." # To print "Building.." message
${CC} ${OPTIONS} ${INCLUDES} ${OBJS} -o target_bin
%.o: %.c # % pattern wildcard matching
${CC} ${OPTIONS} -c $*.c ${INCLUDES}
list:
@echo $(shell ls) # To print output of command 'ls'
clean:
@echo "Cleaning up.."
-rm -rf *.o # - prefix for ignoring errors and continue execution
-rm target_bin
[thumbs-rating-buttons]