Writing Makefiles (at 42)
by bhagenlo
#Why Makefiles?
Makefiles are there to automate the build process for us. They get used by make
for compiling programs according to the rules in them.
The idea is that, in the end, we only have to write
λ make
...
instead of
λ gcc a.c b.c c.c ... libft.a -o my_program
...
And trust me, it is worth it – especially when there are recursive calls to other Makefiles involved.
Also, view it as a buffett. Take what you like, and ignore what you don't :)
Okay. What do we need for the absolute minimum? A rule consists of a target, prerequisites for that target, and commands to execute. The commands are regular commands you could execute on a shell. A variable consists of a name and a (or multiple) value(s), chained together by either The Should clean all the object files, but not your output file(s). Therefore, pretty staightforward: Should clean all the object files + your output file(s). And, last but not least, #0. The Base
#Rules
<target> : <prerequisite 1> <prerequisite 2>
<command 1>
<command 2>
#Variables
=
or :=
.VARNAME = string1
# ...
VARNAME2 = file1.c file2.c somefolder/subfile0.c bla.c \
foo.c bar.c
\
at the end is a line-separator for make
:)#
all
make
without parameters.$(NAME)
(as a prerequisite), but also all the other startup chores you want/need to do.NAME := libft.a
# ...
all : $(NAME)
#
$(NAME)
.o
) files, state them as prerequisites.SRCS := a.c
OBJS := a.o
# ...
$(NAME) : $(OBJS)
ar rcs $(NAME) $(OBJS)
$(OBJS) :
cc -Wall -Werror -Wextra -c $(SRCS)
#
clean
clean :
rm -f $(OBJS)
#
fclean
fclean : clean
rm -f $(NAME)
#
re
re
. It should run fclean
, then recompile.re : fclean
$(MAKE) all
You probably don't want – whenever you add a new There are a few commonly used variables to make your life easier. Namely: Normally you don't want to output all the commands you execute to And, if for some reason you would want to append to a variable – you might want to do that under certain conditions – just append a #1. Helpers
#Automatic object file names
.c
file to your $(SRCS)
– also add the corresponding *.o
file to your $(OBJS)
. Neither do I.
Here's how we fix that, without violating the 'no wildcards' rule of the Norm:SRCS := a.c b.c c.c
OBJS := ${SRCS:.o=.c}
#Automatic Variables
all : $(NAME)
echo $@ # Outputs the target name: "all"
echo $^ # Outputs the prerequisites: Content of $(NAME)
echo $? # Outputs all prerequisites newer than target
#Muting outputs
stdout
– you can mute individual commands with appending @
:clean :
@rm -f $(OBJS)
echo "ran clean."
#Appending to variables
+
to =
:CFLAGS := -Wall -Wextra
# append if rule 'checkup' is executed
checkup : CFLAGS += -Werror
At some point in the curriculum, you'll want to use your libft, and also regularly add minor helper functions to it that might be of use not only for your current project. That means your libft can change during a project. Since you definitely don't want to compile it each time you changed something by hand, we'll automate that, too. General syntax: Example: But that's definitely not enough. When you're able to execute Well, I suggest that we do. Last but not least, a tool definitely worth it to include – the LeakSanitizer:#2. Calling external stuff
#Recursive
$(MAKE)
calls
And we're doing it by calling make [options...]
on other Makefiles from your Makefile!$(MAKE) -C <the folder the makefile is in> <your make options>
$(MAKE) -C libft bonus
#Automatic library cloning
make
automatically for your libft, why don't we also clone it automatically?$(LIBFT) :
if [ ! -d "libft"]; then git clone <your libft repo here>; fi
$(MAKE) -C <the folder the makefile is in> <your make options>
#Using the LeakSanitizer
lsan : $(LSAN)
lsan : CFLAGS += -Wno-gnu-include-next -ILeakSanitizer -LLeakSanitizer -llsan -lc++
$(LSAN) :
if [ ! -d "LeakSanitizer"]; then git clone https://github.com/mhahnFr/LeakSanitizer.git; fi
$(MAKE) -C LeakSanitizer
Well, there are a few steps common to most of the projects we use. One could argue whether they're really part of the build process, but they're definitely part of our build pipeline for finally delivering our software. (For more on that: Take a look at DevOps and CI/CD.) With that, let's go: Why exactly should we I don't know either. I very much hope you're dutifully writing your tests – if you are, then why execute them by hand? I wouldn't like to. When I think I'm finished, right before pushing the projects to Vogsphere for the first time, I normally: Same here: And, finally: Submitting to Vogsphere. For that, we add a new And with that, quite a large extent of your repetive work should have become automated. Happy Coding! :)#3. Automation
You definitely don't need what's in here. However, if you dislike tedious repetition as much as I do, it might be worth a look ;)#
make run
make
an executable and run it in seperate steps?run : all
./$(NAME)
#
make test
test : $(TOBJS) $(OBJS)
$(CC) $(TOBJS) $(OBJS) -o $(TEST)
./$(TEST)
#
make checkup
norminette
checkup :
echo "Testing..."
$(MAKE) test
echo "Checking for memory leaks..."
$(MAKE) lsan
echo "Did you read the subject again, and have checked/asked for common mistakes?"
norminette *.c $(NAME).h
#
make submit
remote
repository named submit
to our repo.submit :
ifdef REPO
git remote add submit $(REPO)
git remote -v
else
@echo -e "You have to provide a repo:\n\n make REPO=<the vogsphere repo> submit\n"
endif