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.
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.
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.
- Put the example above in a file called
- Create a file named
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.
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
It first creates
main.o and then creates
mainexe using their corresponding recipes.
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
echo Today is: ` date "+%Y.%m.%d" ` Today is: 2022.09.12
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.
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
- We replace .c with .o and store the names in
- We use
OBJSvariable 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.
:= they are expanded instantly, you are probably used to this from other languages.
?= sets variables only if they aren't set yet.
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.
- 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
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.
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: