Makefile Basics: Beginner-Intermediate
First of all, I can’t recommend enough reading the GNU user manual for make enough, it gives a very complete overview of the tool and is full of incredible tips. I learned most of what I’m explaining in this tutorial there, and I can’t possibly cover everything from the manual here.
I’ve read this awesome article in StackOverflow written by user Neyl JABER. Give all credits to him. Hereby I just edited a little for more people to see this super brief and detailed article. So, let us dive in.
First steps
The most basic Makefile you could make probably would look like a bit like that :
SRC = main.c function.c #...NAME = nameall: $(NAME)$(NAME):
gcc -Wextra -Werror -Wall $(SRC) -o $(NAME)clean:fclean: clean
rm $(NAME)re: fclean all
There are a lot of problems with this Makefile, so let’s start with the most obvious ones. Currently, our Makefile doesn’t detect changes on our source code, so let’s add our sources as a prerequisite :
$(NAME): $(SRC)Prerequisites are a list of files needed by a rule before it is executed. A rule is executed only if the name of the rule doesn’t correspond to an existing file, or if the file is older then one of it’s prerequisites, this applies to every rule, including rules like all. It basically means that if due to some reason an "all" file is added to the project, the Makefile might be confused. Though there is a walk around solution to prevent this.
Thus, here's going second update:
.PHONY: all clean fclean reThe .PHONY rule simply states that the rules listed shouldn't be considered as files and should be executed no matter what. One important thing to keep in mind is:
If a "phonied" rule appears as a prerequisite of a "non-phonied" rule, the "non-phonied" rule will always be executed, because it has no way to know if changes were actually made or not.
The next step is changing our compilation method so our sources are first compiled into “.o” files, then linked together into our final program. This means that our $(NAME) will now depend on ".o" files, so we need to get a list of those files. We can do that using substitutions :
OBJ = $(patsubst %.c,%.o,$(SRC))
# This line can alternatively be written as
OBJ = $(SRC:%.c=%.o) # Be careful, here we use "SRC:", not "$(SRC):"And make the appropriate change to our $(NAME) rule:
$(NAME): $(OBJ)
gcc -Wextra -Werror -Wall $(OBJ) -o $(NAME)Now, a lot of you may be familiar with the following rule :
%.o: %.c
gcc -Wextra -Werror -Wall -c $< -o $@But what you might not realize is that this rule isn’t actually required, and our Makefile would work just fine without it! Why? Because `make` has a set of predefined “implicit” rules, one of which looking an awful lot like our previous rule :
%.o: %.c
$(CC) $(CFLAGS) -c $^ -o $@There is a lot to unpack with these 2 rules, so let’s take it step by step.
First, the %.o: %.c part. This is a special rule called a "pattern" rule, the "%" in the rule will try to match any targets required by the Makefile.
Second, the $(CC), and $(CFLAGS) variables. These variables contain the compiler command and its arguments, the default compiler is cc, but we can easily override it by changing the variable. This way of writing command is way more versatile then hard-coding the command each time, so we'll take inspiration and change a bunch of things right now :
SRC = main.c function.c #...
OBJ = $(SRC:%.c=%.o)NAME = nameCC = gcc
CFLAGS = -Wall -Wextra -Werror
LDFLAGS =RM = /bin/rm -f
RMDIR = /bin/rmdir.PHONY: all clean fclean reall: $(NAME)$(NAME): $(OBJ)
$(CC) $(LDFLAGS) $(OBJ) -o $(NAME)clean:
$(RM) $(wildcard $(OBJ)) # I also modified the clean rulefclean: clean
$(RM) $(NAME)re: fclean all
Finally, there are still these strange variables we haven’t talked about $^ and $@. These are "Automatic variables", they are automatically assigned depending on the rule : $@ takes the name of the rule, meaning our compilation target, and $^ takes the name of all the prerequisite of the rule, another useful one is $<, which only takes the first prerequisite. Although they are less readable, these variable are preferable (imo) because they are less prone to "oops I forgot to change part of the rule" mistakes and way more versatile as we’ll see later. For now we'll use them in the compilation of our main target:
$(NAME): $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@Let me also take a moment to come back to this line :
$(RM) $(wildcard $(OBJ))The wildcard command is a very poweful tool that will take a list of files (possibly containing * characters, but since programers are all decent people who never do anything purely out of laziness, we won't be using this feature), and will return filter-out all the files that don't match an existing file in the project.
Before we continue on, I’d like to clean up a few things, these are mostly based on personal preference but I’ll try to give my reasoning.
Sources and object files are going to be in a sub-directory from now on. I like to name commonly-used variable for directory names with a single letter. This (imo) enhances readability because single letter variables don’t need parenthesis around them, it also means less typing — and less typing is always nice. By adding a forward slash after the directory name, you can prevent even more typing, and as long as you have syntax highlighting on in your editor, it’s still very readable, and since double slashes are treated the same as a single slash, you’re also able to add slashes after the directories whenever you feel like it.
At the same time, I’ll be changing .o files from %.c=%.o to %=%.o meaning for example main.c.o. This makes the rule more universal, and makes it much easier for projects with multiple file extensions/languages. This means we can't use the implicit rule anymore, but this was already the case since we put sources and objects into separate directories.
S = src/
O = obj/SRC = $Smain.c #(Or $S/main.c) \
$Sfunction.c \
# Backslashes allow breaking declarations into multiple lines,
# this increases readability a lot, and also allows for
# faster editing (on vim at least)OBJ = $(SRC:$S%=$O%.o)#...$O%.o: $S%
$(CC) $(CFLAGS) -c $< -o $@#...
Since it’s not very clean to ship and empty “obj” directory with your project, we’re going to create and delete the directory with the objects, we just need to make a rule :
$O:
@mkdir -p $@
# The @ at the beginning of the line prevents make
# from displaying the line before executing itAnd a modify our cleanup:
cleanobj:
$(RM) $(wildcard $(OBJ))cleanobjdir: cleanobj
$(RMDIR) $Oclean: cleanobjdir
You might now be tempted to add the prerequisite as follow :
$O%.o: $S% $OHowever, this has some nasty side-effects; whenever a file is created in the directory obj/, the directory is also updated, so whenever we execute make again, each objects will see that one of its prerequisite is more recent then it is, and they’re each going to be compiled again. The solution is to declare the directory as what’s called an “order-only prerequisite”, meaning its existence is required, but its update time wont be checked, and its rule will only be called if it doesn’t exist (it also won’t be included in the $^ automatic variable).
$O%.o: $S% | $OLet’s include some
You may have noticed that this Makefile doesn’t support header files yet, and that’s because headers a very difficult beast to tackle when you don’t know what you’re doing — yes, I’ve experienced the pain too.
But before we solve the concerns that come with headers, we’ll start with another kind of include : Makefile includes! Yes, you can include files in Makefiles just like you would in C! These includes are usually prefixed with .mk, and you can include them like this :
include file1.mk file2.mk ...The basic form — OOo, foreshadowing! — acts basically like a #include directive in C (on the surface at least), except that you can use variables as the file names.
Since our goal is to have a general purpose Makefile that fits into any project, we can use this to extract anything that might vary and put it in a separate file :
S = src/
O = obj/RM = /bin/rm -finclude settings.mkOBJ = $(SRC:$S%=$O%.o).PHONY: all clean fclean re#...
Anything before the include acts as “default values”, and can still be overridden by the settings file.
Get A. Yigit Ogun’s stories in your inbox
Join Medium for free to get updates from this writer.
The settings.mk file now contains this:
ifndef SETTINGS_MK # Stay safe
SETTINGS_MK = 1NAME = name
SRC = $Smain.c \
$Sfunction.c \CC = gcc
CFLAGS += -Wall -Wextra -Werror # += appends to the variable
# if it already exists
LDFLAGS +=endif
One problem we encounter is that we can’t put custom rules in our settings.mk file, since if we did, it would appear before the all: rule and override the priority. But there's an easy fix: don't. Just use another include at the end of the file.
#...re: fclean allinclude custom_rules.mk
Let’s use this time to talk about another very interesting feature in Makefiles: although each rule can only define a recipe (the shell commands part) once, it can define prerequesites at any number of points in the makefile, possibly using a combination of patterns. For example, we could be doing something like this:
ifndef CUSTOM_RULES_MK
CUSTOM_RULES_MK = 1$(OBJ): | $O
# Imagine this prerequisite was remove frome the main fileendif
Let’s now go back to our infamous header files. If you’ve never experienced the header file problem, here’s how it goes : you are going along your day as normal, compiling your project, then you edit a header and an object file, and recompile. And then — AH — a sudden segfault. You’re searching for the cause for a few hours before realizing that another object file that was dependent on the header wasn’t recompiled, and from the mess of incompatible binary codes was birthed an eldritch abomination.
Now, solving this problem seems like a really difficult tasks. Some people will try to channel their inner bravery(/stubbornness), and constantly update the list manually whenever they change their include tree. Others will go through tons of complicated shell commands to try and extract the list of includes from a file automatically. But the solution is much simpler than this. It turns out many very skilled coders had encountered this problem before. Some were skilled enough to be working on compilers. Yep gcc (and other compilers) can just hand you over a file containing all the dependencies of a given source file, recursively, by simply using the “-MM” option.
So let’s fix our Makefile and add header support :
S = src/
O = obj/
I = inc/
D = dep/CFLAGS += -I$Iinclude settings.mkOBJ = $(SRC:$S%=$O%.o)
DEP = $(SRC:$S%=$D%.d)RM = /bin/rm -f
RMDIR = /bin/rmdir.PHONY: all clean fclean reall: $(NAME)$O:
@mkdir $@$(OBJ): | $O$O%.o: $S%
$(CC) $(CFLAGS) -c $< -o $@$D:
@mkdir $@$(DEP): | $D$D%.d: $S%
$(CC) $(CFLAGS) -MM -MF $@ -MT $O$*.o $<
# -MF sets the file to write to, -MT sets the target of the rule,
# $* takes the value of the pattern part of the rule (the % part)$(NAME): $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@cleanobj:
$(RM) $(wildcard $(OBJ))cleanobjdir: cleanobj
$(RMDIR) $Ocleandep:
$(RM) $(wildcard $(DEP))cleandepdir: cleandep
$(RMDIR) $Dclean: cleanobjdir cleandepdirfclean: clean
$(RM) $(NAME)re: fclean allinclude $(DEP) # Dependencies are included hereinclude custom_rules.mk
An interesting thing to note is that, when including a file, the Makefile will try creating/remaking the file using rules, this means that we don’t actually need to add the .d file as a prerequisite of anything.
However, there is another problem : if the .d file doesn’t exist yet, an annoying error message is displayed to the console, and even worse, if one of our rule for creating dependencies fails for whatever reason (ie. because of an error in one of your source) the make command simply fails. This is where the --previously foreshadowed -- alternative include form comes in handy; writen like this -include, it simply ignores any file that it can't find or create.
There’s also a few other problems : we don’t really want dependency files to be compiled the first time an object is compiled, since it doesn’t exist, since we don’t need to check its dependencies to decide whether it should be recompiled. Additionally, we don’t want dependencies to be compiled if the associated object file isn’t going to be compiled in the current make (imagine compiling a dependency when calling the clean rule). Finally, we'd like the have the dependency file be recompiled whenever one of the dependency of the source file changes, since the dependency tree might have changed in that case.
The last problem can be solved quite easily by changing -MT $O$*.o to -MT "$O$*.o $@"
The other 2 problems are a little bit more complicated, and I’d like to make an answer dedicated to them instead, because it can get quite a bit more involved.
Last thing before ending this longpost, let’s clean up our Makefile a bit by using a special type of rule, written like this : list: pattern-target: pattern-prerequisites, this rule is basically like a pattern rule, but, but only works on files listed in list, this is helpful to prevent unintended consequences, here is our final Makefile :
S = src/
O = obj/
I = inc/
D = dep/CFLAGS += -I$Iinclude settings.mkOBJ = $(SRC:$S%=$O%.o)
DEP = $(SRC:$S%=$D%.d)RM = /bin/rm -f
RMDIR = /bin/rmdir.PHONY: all clean fclean reall: $(NAME)$O:
@mkdir $@$(OBJ): | $O$(OBJ): $O%.o: $S%
$(CC) $(CFLAGS) -c $< -o $@$D:
@mkdir $@$(DEP): | $D$(DEP): $D%.d: $S%
$(CC) $(CFLAGS) -MM -MF $@ -MT "$O$*.o $@" $<$(NAME): $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@cleanobj:
$(RM) $(wildcard $(OBJ))cleanobjdir: cleanobj
$(RMDIR) $Ocleandep:
$(RM) $(wildcard $(DEP))cleandepdir: cleandep
$(RMDIR) $Dclean: cleanobjdir cleandepdirfclean: clean
$(RM) $(NAME)re: fclean all-include $(DEP)include custom_rules.mk
So this is it for what I’d consider to be the basics of making a good Makefile. As it was written earlier in this article, cover everything here is quite impossible since MAKE has 300 pages manual, I strongly recommend you to read whole document.









