Makefile

Makefiles are files in which developers define rules ✍️ to compile their sources, build an executable, and run it.

  • πŸ“ƒ You define the dependencies between source files
  • πŸš€ Only the strict minimum is recompiled
  • 🏑 Users write simple commands such as make build (no "gcc [...]")
  • 🌍 Commonly used to build projects on Linux

Makefiles are mainly used in C/C++, while they can be used for any language. Modern projects usually use CMake (can generate a Makefile).

πŸ‘‰ Official documentation (GNU)

Create an empty Makefile 🌱

$ touch Makefile # alt: "makefile" "GNUmakefile"
$ nano Makefile  # edit

Execute a rule 🌴

# in the directory with the Makefile
$ make # run the first rule
$ make rule_name # run a specific rule

Getting started

Breakdown of a rule 🏑

target: deps
    compilation_command
  • 🎯 target is the file that the command will generate (ex: a.out).
  • πŸͺ΄ deps is a list of targets, that need to be compiled first
  • 🌴 compilation_command is a command that uses the dependencies to build the target.

Example πŸ”₯

We got a file main.c and we want to generate an executable a.out. We will first define the rule for the executable:

# ❌ bad
a.out: main.c
    gcc -o a.out main.c

We can compile and run our program.

But this is not the correct approach. ⚠️

If main.c had many dependencies, then all will be listed as dependencies of a.out. Every time we ask make to compile the latest a.out, every dependency will be checked instead of one.

Instead, we usually use intermediate files, such as .o in C.

# βœ… correct
a.out: main.o
    gcc -o a.out main.o

We don't need to specify how a .o is compiled, as this is one of the compilation rules πŸ“ that Makefile knows. Still, you can do it:

main.o: main.c
    gcc -c main.c -o main.o

For other languages or in some cases, you may have to define your own compilation rules. For instance, in C, we would have:

# define how, from any .c, we can get the .o
%.o: %.c
    gcc -c -o $@ $<

The final makefile content will be something like this:

a.out: main.o
    gcc -o a.out main.o

%.o: %.c
    gcc -c -o $@ $<

Dependencies

Makefile dependencies are files that when changed, imply that we need to recompile our target.

In C, we could describe dependencies like this:

  • 🌱 Usually, each .o is associated with their .c
  • πŸ“„ Usually, each .o is associated with their .h
  • 🌍 A .o can be associated with the .h they use
  • πŸ—ƒοΈ A .o should be associated with the .o matching the .h they use
  • ❌ We don't usually see a .o associated with another .c

Because of the existing compilation rules, some dependencies are implicit and can be omitted. Giving us:

main.o: file1.o file2.o
file1.o: file1.h
file2.o: # none

Makefile Basics

Variables

We usually start a Makefile by defining some variables, to avoid copy-pasting code and ease the maintenance of our Makefile πŸ‘Œ.

Some quite used names for variables are CC and CFLAGS.

CC=gcc                 # C Compiler
CFLAGS=-Wall           # C Compiler flags

CXX=g++                # C++ Compiler
CXXFLAGS=-std=c++11    # C++ Compiler flags

To use them in some rules:

my_program: deps
    $(CC) $(CFLAGS) -o my_program ...

⚠️ Variables such as CC are known to makefile pre-defined rules, so you don't need to write your own rule to set some compilation flags.

Multilines

You can use \ for multiline instructions:

O_FILES = file1.o file2.o \
        main.o

PHONY rules

Makefile only runs a task if the output or its dependencies have been updated. Tasks marked as .PHONY are always executed.

.PHONY: clean xxx yyy zzz

# 'make clean'
clean:
    rm -rf a.out *.o
    
# ...

Advanced Makefiles

Assignment Operators

There are multiple operators to assign variables:

  • =: assign a value to a variable
  • :=: immediately compute the value of the variable
  • ::=: only compute the value when the variable is actually used
  • ?=: assign if the variable isn't defined
  • +=: append our value to the variable
  • !=: execute a command and store the output in the variable

➑️ We usually prefer always using := rather than =.


External Build Folder

To add a build folder (or a source folder), simply use paths inside all targets. Note that the build folder must be created inside the makefile.

You can do that using prerequisite (|) which are rules executed before evaluating any dependencies.

SRC_DIR := src
BUILD_DIR := build

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
    $(CXX) $(CXXFLAGS) -c -o $@ $<
	
# prerequisite: create the build directory
$(BUILD_DIR):
    mkdir -p $@

Makefile utilities

Makefiles support many commands inside $():

  • $(wildcard *.cpp): find files matching the glob-pattern
  • $(strip string): remove leading and trailing whitespaces
  • $(shell command): execute a shell command
  • $(findstring XXX,input_str): execute a shell command
  • $(subst from,to,input_str): replace from with to in input_str
  • $(eval XXX += yyy): you can execute code inside eval
  • $(foreach var,list,op): execute op on each entry of list
  • $(addprefix prefix, list): prefix all elements with prefix
  • $(notdir list): generate a list of all filenames

A common usage is to automate source and object files detection:

# Source files and object files
SRCS := $(wildcard $(SRC_DIR)/*.cpp)
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))

Makefile if statements

You can use ifeq to check if two values are equal (resp. ifneq). You can use ifdef to check if a variable is defined (resp. ifndef).

ifeq (v1,v2)
    # some code
endif

ifdef XXX
    # some code
endif

Random notes

Define macros

You can define macros and call them wherever you want.

# note: for some variables such as $@, $<, etc.
#       you need to add another "$": $$@, $$<, etc.
define my_function
    @echo "Compiling $(2) into $(1)"
endef

my_rule:
    $(call generate_rule, build/main.o, src/main.cpp)

Simple Text Substitution

You can use text:from=to to substitute from with to in text.

Include Another Makefile

You can create separate makefiles, for instance, if you have multiple compilers, you can create a file (often, a .mk) with variables for each compiler, and conditionally import them.

include gcc.mk      # include cannot fail
-include opt.mk     # include can fail

Verbosity

By default, when we run a command from a makefile, it is echoed inside the terminal. We can use @ to disable this "echo".

my_target:
    @echo "This is a command"

πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

$ automake --add-missing
$ autoreconf
$ automake
automake: error: 'configure.ac' is required
$ autoreconf
autoreconf: 'configure.ac' or 'configure.in' is required