Basics of Makefile in C Compilation

When compiling a large project, there are often many target files, library files, header files, and the final executable file. There are dependencies between different files. For example, when we compile using the following commands:

$gcc -c -o test.o test.c

$gcc -o helloworld test.o

The executable file helloworld depends on test.o for compilation, while test.o depends on test.c.

Basics of Makefile in C Compilation

Dependencies

When compiling a large project, we often need to call the compiler multiple times to compile the entire project step by step based on the dependencies. This method is bottom-up, meaning we compile downstream files first, then upstream files.

The make tool in UNIX systems is used to automatically record and handle dependencies between files. We do not need to input a large number of “gcc” commands; we can simply call make to complete the entire compilation process. All dependencies are recorded in a makefile text file. We only need to run make helloworld, and make will find all the dependencies needed to compile that file from top to bottom, and then compile from bottom to top.

(There are multiple versions of make; this article will be based on GNU make. Make will automatically search for makefile, Makefile, or GNUmakefile in the current directory.)

Basics of Makefile in C Compilation

Dependencies

Basic Concepts

We will use an example C language file:

#include <stdio.h>
/* * By Vamei * test.c for makefile demo */
int main(){    printf("Hello world!\n");    return 0;}

Below is a simple makefile

# helloworld is a binary file
helloworld: test.o
	echo "good"
	gcc -o helloworld test.o
test.o: test.c
	gcc -c -o test.o test.c

Observe the above makefile

  • Lines starting with # are comment lines

  • target: prerequisite indicates the dependency relationship, where the target file depends on the prerequisite file. There can be multiple prerequisite files separated by spaces.

  • The lines indented with <Tab> after the dependency relationship are the operations performed to fulfill the dependency, which are normal UNIX commands. A dependency can have multiple operations.

In simple terms:

  • Want helloworld? Then you must have test.o and perform the associated operations.

  • If you do not have test.o, you must search for other dependencies and create test.o.

We execute

$make helloworld

to create helloworld.

Make is a recursive creation process:

  • Base Case 1: If the current dependency does not specify prerequisite files, then directly execute the operation.

  • Base Case 2: If the current dependency specifies a target file, and the prerequisite files required for the target file already exist and have not changed since the last make (determined by the most recent write time), then directly execute the operation for that dependency.

  • If the prerequisite files required for the current target file do not exist, or the prerequisite files have changed, then treat the prerequisite files as new target files, find the dependencies, and create the target file.

Basics of Makefile in C Compilation

Dashed line: Dependency retrieval

The above is the core functionality of make. With this functionality, we can record all dependencies and related operations in a project and use make for compilation. The following content is an extension of this core content.

Macros

Macros (MACRO) can be used in make. A macro is similar to a text-type variable. For example, the following CC:

CC = gcc
# helloworld is a binary file
helloworld: test.o
	echo "good"
	$(CC) -o helloworld test.o
test.o: test.c
	$(CC) -c -o test.o test.c

We use CC to represent “gcc”. In the makefile, we call the value of the macro using $(CC). Make will replace $(CC) with the value (gcc) at runtime.

Shell environment variables can be directly called as macros. If a custom macro has the same name as an environment variable, make will prioritize the custom macro.

(You can use $make -e helloworld to prioritize the environment variable)

Similar to C language macros, macros in makefiles can conveniently manage fixed text occurrences and facilitate replacement operations. For example, when we use the ifort compiler in the future, we only need to change the macro definition to:

CC = ifort

and that will suffice.

Internal Macros

Make has internally defined macros that can be used directly. $@ contains the name of the target file in the current dependency, while $^ contains the prerequisite files for the current target:

CC = gcc
# helloworld is a binary file
helloworld: test.o
	echo $@
	$(CC) -o $@ $^
test.o: test.c
	$(CC) -c -o $@ $^

Internal macros Function

$* The name of the target file in the current dependency, excluding the suffix.

$* The prerequisite file that has changed in the current dependency

$$ The character “$”

If the target or prerequisite file is a complete path, we can append D and F to extract the folder part and file name part, for example, $(@F) represents the file name part of the target file.

Suffix Dependencies

In the makefile, use

.SUFFIXES: .c .o

to indicate that .c and .o are suffixes.

We can use suffix dependencies, for example:

CC = gcc
.SUFFIXES: .c .o
.c.o:        $(CC) -c -o $@ $^
#--------------------------
# helloworld is a binary file
helloworld: test.o        echo $@        $(CC) -o $@ $^
test.o: test.c

We define .c and .o as suffixes. There is a suffix dependency .c.o: where the former is the prerequisite and the latter is the target. (Note that the order of suffix dependencies is different from general dependencies)

The above test.o and test.c have a dependency relationship, but there are no operations. Make will find that this dependency matches the .c.o suffix dependency and execute the operations following that suffix dependency.

When the project is very large, suffix dependencies are very useful. Files that match suffix dependencies often have similar operations, and we can represent these operations using suffix dependencies to avoid repetitive input.

Others

The continuation character for makefile is \

Common dependency relationships defined in makefiles include:

all:

If make is not followed by a file name, then this dependency will be executed.

clean:

Commonly used to clean up historical files.

For example:

CC = gcc
.SUFFIXES: .c .o
.c.o:        $(CC) -c -o $@ $^
#--------------------------
all: helloworld        @echo "ALL"
# helloworld is a binary file
helloworld: test.o        @echo $@        $(CC) -o $@ $^
test.o: test.c
clean:        -rm helloworld *.o

Note: The @ before echo and the before rm. The command after @ will not display the command itself. The command after – will ignore errors (for example, deleting non-existent files).

Conclusion

The core function of make is to manage compilation based on dependencies.

The other functions of make allow users to write makefiles more conveniently.

Leave a Comment