All about Makefile

·

8 min read

We have to compile programs of languages like C/C++, Swift, Go, and Rust whenever we run, build, test codes. The process is not only troublesome, but also varies from person to person in terms of which files to compile, how to compile them, and how to output them. Therefore, it is desirable to define the compilation method for each project and minimize this process as much as possible.

This is where "Makefile" comes in handy. Today, I will show you what "Makefile" is and how to use it in C with some examples.

What "Makefile" is

Assume that there is a program like the following:

// sample program named "example.c"

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
}

What will you do to check if the program correctly outputs "Hello, World!"?

You would run these commands:
1. gcc example.c -o a.out
2. ./a.out

Then you will be able to check the output. However, it is annoying to run the commands every time you want to test the program, isn't it?

Note
  • "gcc" or "cc" command compiles c file (shorthand)

  • "-o" option of "gcc" or "cc" defines the name of the compiled file

  • "./a.out" runs the compiled file named "a.out"

"Makefile" will reduce your frustration. Here is an example:

test:
    gcc example.c -o a.out
    ./a.out
    rm -f a.out

With the Makefile, you can test the program and then delete compiled program only by running the make test command.

Note
  • "rm" command is a one for removing a file or directory

  • "-f" option of "rm" means force delete

In this way, Makefile automates build process by defining the commands and names for building. You can utilize it not only for "C/C++" but also "Rust", "Go", and so on.

How to use

1. Target: Defining tasks

As I mentioned above, you can define your custom tasks in Makefile like this:

example:
    # some commands to automate
    gcc example.c -o a.out
    ./a.out
    rm -f a.out

And you can run it with make example. The commands are called "target" ("example" is a target in this case).

2. Variable: Defining names to commands and files

You can give names to commands or files as you want, like this:

CC = gcc
CFLAGS = -Wall -Wextra -Werror
OBJS = example.c
OUTPUT = a.out
RM = rm -f

test:
    $(CC) $(CFLAGS) $(OBJS) -o $(OUTPUT)
    ./$(OUTPUT)
    $(RM) $(OUTPUT)

By defining names for commands and files, you can reduce the effort of writing same prompts and the possibility of human error by avoiding repetition. It looks like variables in C and many other programming languages, so it is called "variable".

In the example, I use "$()" to refer to the variables, but "${}" is also valid; there is no difference between them.

Note
  • "-Wall" is a compile option that enables all warning messages e.g, unused variables.

  • "-Wextra" is a compile option that enables warning that are not enabled by "-Wall".

  • "-Werror" is a compile option that converts all warnings into errors.

  • Though I declare all the variables in uppercase, they can be lowercase or the combination of uppercases and lowercases. However, it will be better to declare variables all with uppercases because it is a common convention. It is important to note that the variables are case-sensitive; "FOO", "Foo", and "foo" all refer to different variables.

3. Prerequisite:

You can define dependencies of a target with what is called a "prerequisite". Prerequisite can be targets and file names. Here is an example:

compile_example: example.c
    $(CC) $(CFLAGS) example.c -o $(OUTPUT)

In this example, "example.c" in the first line is a "prerequisite".

When you run make compile_example for the first time, the defined task will be executed. But the next time you run make compile_example, make will check if "example.c" has been edited since the last make compile_example execution. If "example.c" has been edited, make will re-execute compile_example, while make will skip re-execution of compile_example if "example.c" has NOT been edited.

In that sense, "prerequisite" is a kind of dependency.

4. Rules in Makefile

Here are basic rules in Makefile:

  • Indents in Makefile must be tab not space, or you will not be able to run Makefile commands.

  • Running just the make command without any target, the target that is defined first in a Makefile will be executed.

5. Special characters in Makefile

Here are some characters that work in some special ways in Makefile:

(i) * -- wildcard

The character "*" matches any string, even an empty string. With the character, you can specify a set of files or prerequisite.

clean:
    rm -f *.o

The "clean" command will remove all the files that ends with ".o" in the current directory.

(ii) % -- wildcard

The character "%" is also a wildcard. Let's look at an example first:

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

The difference from "*" is that "%" searches for patterns and substitutions based on those patterns. In this example, the "%" is used to define a pattern that matches any target ends with ".o" with the same string ends with ".c". Moreover, the "%" wildcard searches a non-empty string that matches a pattern.

(iii) $ -- automatic variable

The "$" symbol is used for referring to variables, but it can be also used as an automatic variable. "$" with some symbols have special meanings.
1. $@ represents the target name of the current rule.
2. $< represents the first prerequisite of the current rule.
3. $^ represents all the prerequisites of the current rule, separated by spaces.

Assume that there are a target and a prerequisite like the following:

main.o: main.c header.h

In this case,
$@ will be "main.o" (the target),
$< will be "main.c" (the first prerequisite),
$^ will be "main.c" and "header.h" (all the prerequisites).

Therefore, you can define the rule like this:

main.o: main.c header.h
    $(CC) $(CFLAGS) -c $< -o $@
    # It is the same as the below in this example
    # $(CC) $(CFLAGS) -c main.c -o main.o

Moreover, the "$" also acts as "substitution reference".
The syntax is $(variable:pattern=replacement).

SRCS = file1.c file2.c file3.c
OBJS = $(SRCS:.c=.o)

In this case, all the files that end with ".c" in the "SRCS" will be replaced with new files end with ".o". Therefore, the "OBJS" will be "file1.o", "file2.o", and "file3.o".

Furthermore, the "$" symbol play a role as function invocation.
The syntax is $(build_in_function arg1, arg2, ...).

For example, $(wildcard *.c) in the Makefile following will be replaced with a list of all ".c" files in the current directory.

SOURCES := $(wildcard *.c)

Note

  • "-c" option with "gcc" or "cc" compiles source files without linker.

  • Makefile has many kinds of built-in functions like "wildcard", "foreach", and "basename".

(iv) := -- simply expanded variable

You can assign a value to a variable with the "=" operator, as I mentioned. The ":=" operator acts like the "=" operator but with a difference. When a variable defined with the "=" is referred in a rule, the right-hand value will be assigned at the time. On the other hand, the right hand value will be assigned when a variable is defined with the ":=".

VAR1 = $(VAR2)
VAR2 = some_value

VAR3 := $(VAR4)
VAR4 := some_value

In this case, the "VAR1" will be the "VAR2" when the "VAR1" is referred because when it is called, the assignment will be executed at the time. However, "VAR3" will be empty because "VAR4" is not defined yet when it is defined.

6. target convention

There are some common targets in Makefile. Here are the list.

  • all: builds the entire project by compiling all the source files and generating the executable or library.

  • clean: removes the object files and any other intermediate files generated during the building process.

  • fclean: removes not only the object files and intermediate files but also the executable and library.

  • test: runs the unit tests or any other tests defined for the project.

  • install: installs the built executable, library, or other artifacts to a specified location.

  • run: runs the built executable with default or specified arguments.

It will be recommended to define these common targets in your project's Makefile.

7. Defining a special target as a rule

Assume that there is a Makefile like the following:

SRCS = main.c helper.c
OBJS = $(SRCS:.c=.o)
EXEC = program

$(EXEC): $(OBJS)
    gcc $(OBJS) -o $(EXEC)

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

In this example, %.o: %.c is indeed a kind of target, but you can NOT run make %.o just like a normal target. It just defines the rule of how to get object files from ".c" files.

In this case, when running make program, make will compile to get object files defined in the prerequisite "$(OBJ)" if it is necessary, and then compile to create an executable named "program".

8. ".PHONY": a special built-in target

There is a special target called ".PHONY".

The main purposes of using ".PHONY" are:

  • To avoid conflicts with file or directory names: Make can get confused, if you have a file or directory with the same as a target. By declaring a target in the ".PHONY", Make is able to correctly execute the target.

  • To ensure that the target are always executed: Make will skip the execution of a target if its prerequisite is not modified since the last execution. However, the targets declared in the ".PHONY" are always executed, regardless of the modification times of any file.

In other words, the ".PHONY" force execution of all the targets declared within it.

It is common to define the ".PHONY" at the top level of the Makefile and declare "all", "clean", "install", and "test".

Example:

.PHONY: all clean install test

Note that ".PHONY" is a special built-in target; make .PHONY does NOT make sense at all. The ".PHONY" just adds the special attribute to the targets within it.