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.

This guide is divided into parts on purpose – only tackle the next one if you've understood (and used) the content of the one before. It is here to make your life easier, not harder.
Also, view it as a buffett. Take what you like, and ignore what you don't :)

#0. The Base

Okay. What do we need for the absolute minimum?

We need the rules $NAME, all, clean, fclean, and re, and that it does not relink.

#Rules

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.

<target> : <prerequisite 1> <prerequisite 2>
        <command 1>
        <command 2>

#Variables

A variable consists of a name and a (or multiple) value(s), chained together by either = or :=.

  • The name is written in UPPERCASE by convention.
  • The values are whitespace-separated, and consist of strings.

VARNAME = string1
# ...
VARNAME2 = file1.c file2.c somefolder/subfile0.c bla.c \
        foo.c bar.c

The \ at the end is a line-separator for make :)

#all

  • Highly likely to be the first rule in your Makefile, as your first rule gets called implicitly when just calling make without parameters.
  • Consists usually only of $(NAME) (as a prerequisite), but also all the other startup chores you want/need to do.

NAME := libft.a
# ...
all : $(NAME)

#$(NAME)

  • The rule where the compilation happens.
  • Since it needs the object (.o) files, state them as prerequisites.
  • And then follows your compilation, library creation, whatever you are doing :)

SRCS := a.c
OBJS := a.o
# ...
$(NAME) : $(OBJS)
        ar rcs $(NAME) $(OBJS)

$(OBJS) :
        cc -Wall -Werror -Wextra -c $(SRCS)

#clean

Should clean all the object files, but not your output file(s). Therefore, pretty staightforward:

clean :
        rm -f $(OBJS)

#fclean

Should clean all the object files + your output file(s).

fclean : clean
        rm -f $(NAME)

#re

And, last but not least, re. It should run fclean, then recompile.

re : fclean
        $(MAKE) all

#1. Helpers

#Automatic object file names

You probably don't want – whenever you add a new .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

There are a few commonly used variables to make your life easier. Namely:

all : $(NAME)
         echo $@ # Outputs the target name: "all"
         echo $^ # Outputs the prerequisites: Content of $(NAME)
         echo $? # Outputs all prerequisites newer than target

#Muting outputs

Normally you don't want to output all the commands you execute to stdout – you can mute individual commands with appending @:

clean :
        @rm -f $(OBJS)
        echo "ran clean."

#Appending to variables

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 + to =:

CFLAGS := -Wall -Wextra

# append if rule 'checkup' is executed
checkup : CFLAGS += -Werror

#2. Calling external stuff

#Recursive $(MAKE) calls

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.
And we're doing it by calling make [options...] on other Makefiles from your Makefile!

General syntax:

$(MAKE) -C <the folder the makefile is in> <your make options>

Example:

$(MAKE) -C libft bonus

#Automatic library cloning

But that's definitely not enough. When you're able to execute make automatically for your libft, why don't we also clone it automatically?

Well, I suggest that we do.

$(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

Last but not least, a tool definitely worth it to include – 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

#3. Automation

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.)
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 ;)

With that, let's go:

#make run

Why exactly should we make an executable and run it in seperate steps?

run : all
        ./$(NAME)

I don't know either.

#make test

I very much hope you're dutifully writing your tests – if you are, then why execute them by hand?

test : $(TOBJS) $(OBJS)
        $(CC) $(TOBJS) $(OBJS) -o $(TEST)
        ./$(TEST)

I wouldn't like to.

#make checkup

When I think I'm finished, right before pushing the projects to Vogsphere for the first time, I normally:

  • Run all the tests
  • Check for memory leaks
  • Take a look at whether I missed something
  • Run norminette

Same here:

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

And, finally: Submitting to Vogsphere. For that, we add a new 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

And with that, quite a large extent of your repetive work should have become automated.

Happy Coding! :)

#Pointers

← Additions to the Core

Found something to improve? Edit this page on Github!

VSCode Debugger →