How to Write a makefile


Nathan Osman's Gravatar

Nathan Osman
published Nov. 3, 2011, 9:37 p.m.


Yesterday, I wrote an article on how to write a manpage. After hearing a lot of positive feedback on that article, I decided to tackle something else that can be a challenge for those just getting started with software development: writing a makefile. As was the case with writing a manpage, I decided to learn how to write a makefile after needing one for a project I was working on. So without any further interruptions, I humbly present to you the instructions for writing a makefile.

Before We Begin...

In order to do help make all of the concepts clear that I will be explaining later, we will begin by creating an example application in C++ which we will write a makefile for. Here is the sample application:

#include <iostream>

int main(int argc, char ** argv)
{
    std::cout << "Hello world!" << std::endl;
    return 0;
}

This example is simple enough - it outputs the classic message "Hello world!" to the console and then terminates. After saving the file (perhaps using the filename "hello.cpp"), we can compile it on Ubuntu using the following command (assuming you have the g++ package installed):

g++ hello.cpp -o hello

This will create a file named "hello" in the same directory that we can then run using the command ./hello.

What Does This Have to Do with Writing a makefile?

We have a simple application. We have a simple command that can compile it. Why do we need to use a makefile? Well, let's make a slight adjustment to our sample application that will help us see the need for using a makefile. Let's suppose that we want to tell the user which version of Ubuntu they are using. Although there are many other ways of doing this, let's implement this by having the source file assume a constant named UBUNTU_VERSION is defined:

#include <iostream>

int main(int argc, char ** argv)
{
    std::cout << "Hello " << UBUNTU_VERSION << "!" << std::endl;
    return 0;
}

Our command for compiling the application now becomes:

g++ -DUBUNTU_VERSION=\"`lsb_release -cs`\" hello.cpp -o hello

The command we use for compiling the application is starting to get a little complicated now. But wait - there's more. Consider the following realistic possibilites:

  • the application consists of multiple source files
  • you want to pass other options to the compiler / linker such as -Wall to turn on all warnings
  • the compilation process depends on shell macros
  • you only want certain files included in the compilation under certain conditions
  • there are a lot of source files yet few of them are modified at a time

Without a makefile, some of the above scenarios would either be very difficult to implement or completely impossible. Hopefully by this point you see just how important a makefile can be. The next question becomes:

How Do I Write the makefile?

We are now ready to begin writing our makefile. Let's take a look at the makefile we would use for the first version of our sample application. Save the following in a file with the name "Makefile":

all:
    g++ hello.cpp -o hello

Note that it is extremely important that the file have the name "Makefile". This allows make to find our makefile automatically without us having to specify where it is.

What does all of this mean and what does it do? Well, we have a rule named "all" that provides a set of commands for building the target "all". Why do we use "all"? Well, that's the customary name for the default target that builds the application. In fact, when invoked with no arguments, the make command will attempt to build the first rule.You are free to change the "all" to something else if you like. Of particular importance is the indentation on the second line - we indent the commands using a tab and not spaces, which some text editors are a little too happy to do automatically for you.

Awesome! Now Tell Me How That Saves Time.

So far, we have created a makefile that really doesn't offer much of an improvement over compiling the application by hand. Well that's about to change. Consider a hypothetical network application that has the following files:

  • main.cpp processes command line arguments and acts on them accordingly
  • http.h / http.cpp makes HTTP requests and outputs the response
  • ftp.h / ftp.cpp connects to FTP server, authenticates, and uploads / downloads files

Compiling each of those on the command line every time we make a change is not going to be fun - especially when conditional compilation comes into play. We are going to need a makefile.

Creating a makefile for this application is going to consist of a bit more work than the previous example. Here is what our first attempt at writing a makefile looks like. Note that this is not the final product and we will learn how to improve it in the next step.

all:
    g++ -c main.cpp
    g++ -c http.cpp
    g++ -c ftp.cpp
    g++ main.o http.o ftp.o -o network_tool

This makefile runs four commands: it compiles the three source files and then links them in the final step to produce the network_tool binary. This makefile sure makes things easier - now we can compile the entire application simply by running make. However, there is a problem: what happens if we make a change to a single file? Does the entire application really need to be rebuilt from scratch? With our current makefile, the answer is unfortunately "yes". However, we can avoid this problem by moving each step to its own rule:

all: network_tool
network_tool: main.o http.o ftp.o
    g++ main.o http.o ftp.o -o network_tool
main.o:
    g++ -c main.cpp
http.o:
    g++ -c http.cpp
ftp.o:
    g++ -c ftp.cpp

This example probably prompts a few questions. First of all, what's with the naming conventions for rules? Well, because we are using actual filenames for the names of the targets, make will take note of that and keep track of any modifications to the files. Then the next time you run make, any files that are already up to date will be skipped. The next question is probably about the filenames on the right side of the colon on line 2 - what are they? Well, those are dependencies - targets that must be made before that target itself can be made. In our case, this means that main.o, http.o, and ftp.o must all be made before they can be linked.

And That's All There Is to It?

Well, not exactly - but we're getting there. What happens when the compiler needs a bunch of options, such as include paths, etc.? What about linker options? Well, thankfully makefiles have you covered there too. Consider the following snippet:

CXXFLAGS = -Wall
main.o:
    g++ $(CXXFLAGS) -c main.cpp

We have declared a variable on the first line that contains flags for the C++ compiler and then we reference it on the third line. Take careful note of the syntax - this helps us avoid repeating common flags in individual commands. You can also assign the output of shell commands to variables like so:

VARIABLE = $(shell uname)

This assigns the output of the uname command to the variable VARIABLE, which you can later reference in commands using $(VARIABLE).

Do We Know Everything Yet?

There's still a lot more to learn about makefiles - special variables, dummy targets, etc. But hopefully this article gives you a good introduction to writing them. Remember (as I mentioned in the last article) that you can always get help at Stack Overflow if you should ever run into trouble writing your makefile.