Even though its creation dates back to late 70s, make is still a widely used and useful tool.
While the syntax of makefiles may seem weird at first, I hope this entry will make it easier to understand the core principles of them.
Basic Behaviour
Makefiles mostly consist of rules and their recipes.
A simple rule looks like this:
foo.txt:bar.txt
@echo "Hello World!"
cat bar.txt >> foo.txt
In a rule, left side of the colon specifies the target file created by the rule, right side specifies the dependencies needed. (Generally called prerequisites)
Everything after the first line is its recipe, all of the actual commands required in order to create the target.
Important note:
Recipes must be indented with a tab in order for a makefile to function, but dev.to replaces my tabs in the editor with 4 spaces.
If you;
- Put the example above in a file called
Makefile
- Create a file named
bar.txt
- Run
make
You would notice make has created a file called foo.txt by following that file's recipe.
If you try to run make
once again, it would output this:
make: 'foo.txt' is up to date.
This brings us to our next point:
Make only runs rules if the dependencies have changed.
It compares the target's and depedencies' last edit dates, if the dependencies have a newer last edit date that would mean rules needs to run.
This helps with big projects, as you don't have to compile the whole project when making small changes.
Complicating Things
Now that we got the basic structure out of the way we can delve into a bit more useful example and add to it as we go.
mainexe:main.o
gcc -o mainexe main.o
main.o:main.c
gcc -c main.c
Unless stated otherwise, make starts with the first target in the file. That would be mainexe:main.o
in our case.
Since that rule's dependency main.o
doesn't exist yet, make finds main.o
's rule, which is main.o:main.c
.
It first creates main.o
and then creates mainexe
using their corresponding recipes.
Macros
You can define variables that keep string information in your makefiles.
CC=gcc
CFLAGS = -Wall -O2
DATESTRING = Today is: ` date "+%Y.%m.%d" `
#This is a comment
mainexe:main.o
@echo $(DATESTRING)
$(CC) -o mainexe main.o
main.o:main.c
$(CC) $(CFLAGS) -c main.c
This way user can substitute their own compiler and flags easily.
Also, you might have noticed there can be shell commands in macros too, everything between two back sticks get treated as shell commands.
@ symbol before commands tells make to not print the command itself.
So in our case, it prints:
Today is: 2022.09.12
instead of:
echo Today is: ` date "+%Y.%m.%d" `
Today is: 2022.09.12
Pattern Rules
If you have more than one .c files, you don't have to write a compile rule for all of them one by one.
CC=gcc
CFLAGS = -Wall -O2
DATESTRING = Today is: ` date "+%Y.%m.%d" `
#This is a comment
mainexe:main.o functions.o
@echo $(DATESTRING)
$(CC) -o mainexe main.o functions.o
%.o : %.c
$(CC) $(CFLAGS) -c $<
This last rule is called a "Pattern Rule". It is like a general rule for files that have a .o extension.
$<
is an internal macro that refers to the first dependency
Even though it is not used here:
-
$@
refers to the target file. -
$?
refers to the dependencies that require attention. -
?^
refers to all of the dependencies.
Search Directory
Notice how we need to edit the makefile every time we add a new .c file, that can be taken care of easily with automation too.
CC=gcc
CFLAGS = -Wall -O2
DATESTRING = Today is: ` date "+%Y.%m.%d" `
C_FILES := $(wildcard *.c)
OBJS := $(patsubst %.c, %.o, $(C_FILES))
#OBJS := $(C_FILES:.c=.o)
#These two lines of code do the same thing
mainexe:$(OBJS)
@echo $(DATESTRING)
$(CC) -o mainexe $(OBJS)
%.o : %.c
$(CC) $(CFLAGS) -c $<
- We get all of the file names with the .c extension in the current directory and store it in
C_FILES
variable - We replace .c with .o and store the names in
OBJS
variable - We use
OBJS
variable as the dependency of mainexe rule
Another thing to note is introduction of :=
.
When you initialize DATESTRING with =
that is called a "Lazy evaluation". It is kept as is and doesn't get expanded until it is needed.
With :=
they are expanded instantly, you are probably used to this from other languages.
?=
sets variables only if they aren't set yet.
Phony
Phonies are functions that don't refer to files.
CC=gcc
CFLAGS = -Wall -O2
DATESTRING = Today is: ` date "+%Y.%m.%d" `
C_FILES := $(wildcard *.c)
OBJS := $(patsubst %.c, %.o, $(C_FILES))
.PHONY:all
all:mainexe
mainexe:$(OBJS)
@echo $(DATESTRING)
$(CC) -o mainexe $(OBJS)
%.o : %.c
$(CC) $(CFLAGS) -c $<
.PHONY:clean
clean:
-rm -f $(OBJS)
Here, all and clean rules don't refer to a file in the system. We can tell that to make by declaring them as "Phony" rules.
Clean function can be called with make clean
, which will delete all of the objects created while building.
-
sign before the rm
command suppresses errors.
Important Caveats
- Every command runs in their own shell. If you want to print the contents of the parent directory, you need to do:
printParent:
cd .. && \
ls
#Correct
instead of:
printParent:
cd ..
ls
#Incorrect
- First rules in makefiles are usually called
all
. It is nothing more than tradition, I thought it was worth mentioning to avoid confusion.
Conclusion
I hope this guide helped you learn the basics of make. This barely even scratches the surface of what you can do with it.
To learn more, you can check out this much more comprehensive tutorial:
https://makefiletutorial.com/
Top comments (0)