Skip to content

Instantly share code, notes, and snippets.

@satyx
Created February 7, 2022 19:32
Show Gist options
  • Save satyx/40e2c079cbdd15e11e3d9eee98ae34f8 to your computer and use it in GitHub Desktop.
Save satyx/40e2c079cbdd15e11e3d9eee98ae34f8 to your computer and use it in GitHub Desktop.

GNU Make : Guide for an absolute beginner

Introduction

How beautiful will it be, if there could be something which could save us from writing long commands for compiling our c/c++ projects. Like, build myDreamProject and Voilà! The executable is ready and your system itself took care of building all the necessary files, compiler flags, which compiler to use corresponding to each component … and the requirements are endless. Thanks to GNU MAKE, this is possible. Though it's syntax looks scary and beginners tend to skip it, with basic understanding it can literally be your best friend.

Probably you must have encountered a makefile if you ever went through any C/C++ project. Makefiles essentially contains all the rules to build target file(s). A proper makefile saves a lot of effort while building any project. Oh can't we use the divine knowledge of scripting? Well GNU MAKE only builds those files whose dependencies (referred as prerequisites) are modified, thus eliminating a redundancy and making your build faster compared to other naive script files. For me, I came across a lot of makefiles but the real need for this was felt when I was developing an OS kernel as my hobby project. There were so many source files having different functionality, different file types including .c .s .o etc, custom linker script for linking object files ... Well, obviously use of makefile was inevitable and we will see in a moment why ...

Rules

A rule is used for determining which and how the target file should be built. The general syntax of rule:

target: prerequisites
    recipe

Here target is most of the time, name of the target file (Not necessary, refer to PHONY targets) which will be generated after the rule is executed. Prerequisites are the files on which the target depends upon. Recipe is the action that should be taken. Now, the recipe is only executed if any of the prerequisite files are modified. How does GNU MAKE determine this? We will discuss this at the end.

How to generate a target file? Run

make <targetname>

Let's start with a naive example. Consider project repository as

MyProject
  bar.cpp
  makefile

To compile, you can execute

g++ bar.cpp

To do this with a makefile, create a file named makefile with the following data

output: bar.cpp
    g++ bar.cpp -o output

output is the target file, bar.cpp is the prerequisite and g++ bar.cpp is the recipe. So whenever you are like hey!Please build and generate file output for me. MAKE will first check if the target file exists? If yes, it checks if their prerequisites exist. They do! If any of them has been modified? If no, don't build again, else execute the recipe of the target. If the prerequisite itself a target file, then same procedure will be followed before executing the recipe of the current target file.

Note 1: If the target file doesn’t exist. MAKE will execute the recipe. So, if the target is different from the file getting generated through the rule, GNU will consider every time that the target file is not present and build it again even if it’s prerequisite files aren’t modified.

Note 2: If the prerequisite doesn’t exist and the target file if present will be considered up to date and the recipe corresponding to the rule won’t ever be executed. Such targets are generally used not for generating file(s) but to take certain actions like file deletions (refer PHONY targets).

Note 3: The recipe line always starts with a tab and it is there for syntactical purpose and not indentational purpose. You can replace tab with your choice of character but you then need to set the .RECIPEPREFIX variable (supported since GNU MAKE 3.82).

Execution

As mentioned earlier, execute make . In this case,

make output

GNU make will find the rule with target output in the makefile and execute it’s recipe based upon the above mentioned condition. If you wish to have a different name for your makefile, you have to use -f flag followed by the makefile name. Eg: customMakeFile

make output -f customMakeFile

Multiple Rules

Obviously you won't be requiring makefile for a project with single cpp file. Consider your project directory

MyProject
  foo.cpp
  foo.h
  bar.cpp

Typical makefile for this would be:

output: foo.cpp bar.cpp
    g++ -std=c++11 -I. -o output foo.cpp bar.cpp

Based on the above explanation, this makefile is self explanatory.

Note 4: There's a small intentional error here. You see, if there is any change in the prerequisite, only then the rule’s recipe will be executed. So in our case, if you need to modify the header file(foo.h), MAKE will only check the prerequisites mentioned blindly (here only the .cpp files) which in this case aren't changed at all, hence the rule's recipe won't be executed even if it should (morally at least). So it's your responsibility that you mention all the files which directly or indirectly affect your target. Also, to make it more efficient, we should build foo and bar separately with different rules and then link their object files as change in foo.cpp won't affect the object file obtained from bar.cpp.

Modifying our makefile to add more rules,

output: foo.o bar.o
    g++ -o output foo.o bar.o
foo.o: foo.cpp foo.h
    g++ -std=c++11 -I. -o foo.o -c foo.cpp
bar.o: bar.cpp
    g++ -std=c++11 -I. -o bar.o -c bar.cpp

Now to build your project, execute

make output

This will check its prerequisite foo.o and bar.o (after checking if the target file exists, refer to Note 1). Corresponding to foo.o it will check it’s prerequisites foo.cpp and foo.h files. If any of them are modified, foo.o will be built first. Similarly for bar.o . Now if foo.o and/or bar.o changed, the recipe corresponding to target (in our case, output) will be executed and target file will be built again.

Variables

You can also define your own variable at the beginning and thus you can use it throughout the file (like storing source filenames in a variable. Therefore if there is any change in its name, you don't have to modify it everywhere, just change the value of variable). To get the value of the variable, use $(variable).

Adding few variables in our makefile

CC=g++
CFLAGS=-std=c++11 -I. -c
DEPS = foo.o bar.o

output: $(DEPS)
    $(CC) -o output foo.o bar.o

foo.o: foo.cpp foo.h
    $(CC) $(CFLAGS) -o foo.o foo.cpp
bar.o: bar.cpp
    $(CC) $(CFLAGS) -o bar.o bar.cpp

The value corresponding to variable is replaced at $(variable) and the rest of it is similar to previous makefile examples.

Automatic variables and Pattern Rules

Another great thing about GNU MAKE is, rather than writing individual rules for each target, you can write a single general rule for similar targets with similar prerequisites which will further make your writing makefile less pathetic. Let's consider a rule:

%.o: %.c
    g++ $< -o $@

Take a deep breath, I know this is where it starts being Latin but hold on. This rule applies to all targets with .o extension. So, the targets are matched against the pattern. The character ‘%’ can match with any part of the string and is extracted out (called stem). Let’s consider target foo.o . It will match with the pattern %.o with stem being foo. Then % in prerequisite is replaced with the stem extracted from the target. So this rule for foo.o would appear to be like

foo.o: foo.c
    g++ $< -o $@

Now the evil $@, this replaces itself with the left hand side of the colon(target) which in our case is foo.o. @< replaces itself with the FIRST prerequisite (present on the right hand side of the colon). Thus, the rule finally appears like

foo.o: foo.c
    g++ foo.c -o foo.o

Here we could replace $&lt; with $^. $^ replaces itself with EVERYTHING present on the right hand side of the colon(prerequisite list). These variables(&lt;,@,^) are also called automatic variables and ‘$’ is used to extract their value.

Thus modifying our makefile.

CC=g++
CFLAGS=-std=c++11 -I. -c
DEPS = foo.o bar.o

output: $(DEPS)
    $(CC) -o $@ $^

%.o: %.cpp
    $(CC) $(CFLAGS) -o $@ $<

Note 5: The above makefile still has the error which earlier was introduced "intentionally". Prerequisite of foo.o should also include foo.h. The purpose of this makefile is purely demonstrational.

String Transformation with patsubst

Lets restructure our project directory

MyProject/
  include/
    foo.h
  src/
    foo.cpp
    bar.cpp
    makefile
  obj/
    foo.o
    bar.o

Now the location of header and object files are changed, we accordingly need to incorporate those changes in the makefile. Firstly, we will define a variable IDIR which will store the location of header files and ODIR which will store location of object files. Now we have to reflect these changes in the rules. One way could be to replace foo.o and bar.o with ../obj/foo.o and ../obj/bar.o , but this will kill the whole purpose of defining variables. Suppose tomorrow again we need to restructure our project, we again have to change ../obj with the location of the object file throughout the makefile. So what we can do, we will let GNU MAKE do such replacements.

So, for each object file you need to add its location. Let us define a variable ODIR which stores the location of object files and a variable _DEPS_F which will store just the filename. Now consider,

$(patsubst %,$(ODIR)/%,$(_DEPS_F))

Essentially this is pattern substitution, $(patsubst pattern,repStr,text). It will replace all the occurence of pattern with repStr from the text. Since we need to do this with every file (foo.o and bar.o), we used ‘%’ and this will be replaced by ../obj/% where % this time would be the object filename

Similarly, we can do this for including location of header files. Reflecting those changes in our Makefile:

IDIR=../include
ODIR=../obj
CC=g++
CFLAGS=-std=c++11 -I $(IDIR) -c
_DEPS_F = foo.o bar.o
DEPS_F = $(patsubst %,$(ODIR)/%,$(_DEPS_F))

_DEPS_FOO = foo.h
DEPS_FOO = foo.cpp $(patsubst %,$(IDIR)/%,$(_DEPS_FOO))

DEPS_BAR = bar.cpp

output: $(DEPS_F)
    $(CC) -o $@ $^

$(ODIR)/foo.o: $(DEPS_FOO)
    $(CC) $(CFLAGS) -o $(ODIR)/$@ $<

$(ODIR)/bar.o: $(DEPS_BAR)
    $(CC) $(CFLAGS) -o $(ODIR)/$@ $<

PHONY targets

Sometimes, you don’t want any file to get generated but an action to be taken corresponding to a rule. For these, the prerequisite would and should be empty. Let's consider one such action clean for object file deletion

clean:
    rm *.o

When you execute clean, GNU MAKE will find no file named clean exist so the recipe will be executed (Refer Note 1). What if a file named clean exists? In that case, it will check if it’s prerequisites are modified. But as we don’t have any prerequisites, GNU MAKE will consider it to be up-to-date and no action will be taken (Refer Note 2).

To handle this case we can use PHONY targets. GNU MAKE won’t confuse PHONY targets with files and so the recipe will be executed. Lets modify our makefile

IDIR=../include
ODIR=../obj
CC=g++
CFLAGS=-std=c++11 -I $(IDIR) -c
_DEPS_F = foo.o bar.o
DEPS_F = $(patsubst %,$(ODIR)/%,$(_DEPS_F))

_DEPS_FOO = foo.h
DEPS_FOO = foo.cpp $(patsubst %,$(IDIR)/%,$(_DEPS_FOO))

DEPS_BAR = bar.cpp

output: $(DEPS_F)
    $(CC) -o $@ $^

$(ODIR)/foo.o: $(DEPS_FOO)
    $(CC) $(CFLAGS) -o $(ODIR)/$@ $<

$(ODIR)/bar.o: $(DEPS_BAR)
    $(CC) $(CFLAGS) -o $(ODIR)/$@ $<


.PHONY: clean
clean:
    rm output $(ODIR)/*

Diving Deep

Coming back to How GNU MAKE decides if the prerequisites are modified? Unlike the Version Control Systems, it doesn’t maintain versions for each file, rather it uses a very simple but efficient technique. It compares the last modified date of target with the prerequisites. If the target is older compared to any of its prerequisites, it is built again. Well this means you can play around with the last modified date and fool GNU MAKE.

After executing make output, use the linux command touch with any of the prerequisites. What it does essentially is, if the file doesn’t exist it creates, otherwise it simply modified the last-modified-date of the file without making any change in its data. Now again execute make output. You can witness, unlike the rest of the time, the target is built again, even if data present in any of the prerequisites isn’t changed. Now, add a comment in any of the prerequisites. Now it is newer than the target and data is changed. Therefore ideally the target file should be built again, but wait for a moment. Execute touch output to simply modify the last-modified-date of file named output(the target file for this rule) which now technically makes it newer than its prerequisites. Try building it again using make output. This time you get the message

make: `output' is up to date.

Even though the data of the prerequisite was changed, GNU MAKE couldn't detect it.

Conclusion

Enough theory! It’s your time to get your hands dirty. Try writing some makefiles for your project(s) or maybe simply create a sample project for it. Getting comfortable with the syntax requires delta practice . This blog is a good starting point but for any advanced topic, refer to GNU MAKE’s documentation which is written in extremely simple, understandable and elegant language even for absolute beginners. Peace!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment