Makefile Tutorial

For Repeated Compilation

Makefile Example

Introduction

   Tired of having to type out a long-winded gcc command? Keep forgetting a pesky flag or two? Or maybe an entire source file nestled in your burstly project? Well there's a better way. You've probably used it before especially if you've downloaded some GitHub projects. Make. But how to use such untapped power? Many tutorials are pretty skimpy on details, or too overburdening to get the tangible facts. Perhaps this one is just right.

Features

  • Type once, compile ad infinitum (0 times to be exact).
  • Compatible with any *nix system (or those with make installed).
  • Usability with gcc, g++, gfortran, etc.
  • Ability to be customized by your users.

Demonstration

   Let's start with a 'dead' simple program which we wish to compile:

  1. #include <iostream>
  2. int main(int argc, char *argv[]) {
  3.     std::cout << "Goodbye, cruel world!" << std::endl;
  4.     return 0;
  5. }
More

   Typically we'd run: g++ -o Example.exe main.cpp to get our desired executable. But let's plan for the future as this is will be an expanding example. To run our make command, we need a target "Makefile". So let's make one (pun possibly intended):

  1. # Makefile for our example program (this is a comment)
  2. # V1.0
  3. Example.exe:
  4.     g++ -o Example.exe main.cpp
  5. More

   Now type make and presto. Makefiles are essentially recipes to create a certain object which can be object files (.o), library files (.dll), or the final executable(s) (.exe). In our case, we had one recipe, to make "Example.exe" and to create it, we run g++ -o Example.exe main.cpp. Try running make again. It should say that everything is up to date. Change the source file subtly and calling our one word command again results in a new compiliation. This will be important later. Let's add to this example to further our understanding. Since we're (I'm) writing this in C++, let's add a header file with an accompanying source file. Firstly, here's the new source and header files, "Parrot.hpp" (I wonder what it could do?!?), "Parrot.cpp" and the new "main.cpp":

  1. #ifndef PARROT_H
  2. #define PARROT_H
  3. #include <ostream>
  4. #include <string>
  5. // Give it a phrase and you'll never hear the end of it.
  6. class Parrot {
  7. public:
  8.     Parrot(std::string Phrase) : phrase(Phrase) {}
  9.     // Echoes the provided phrase to a given output stream
  10.     void Speak(std::ostream &os);
  11. private:
  12.     std::string phrase;
  13. };
  14. #endif
More
  1. #include <ostream>
  2. #include <string>
  3. #include "Parrot.hpp"
  4. // Echoes the provided phrase to a given output stream
  5. void Parrot::Speak(std::ostream &os) {
  6.     os << phrase << std::endl;
  7. }
More
  1. #include <iostream>
  2. #include "Parrot.hpp"
  3. int main(int argc, char *argv[]) {
  4.     Parrot avianBeing = Parrot("Polly isn't fond of crackers");
  5.     avianBeing.Speak(std::cout);
  6.     return 0;
  7. }
More

   Now back to our "Makefile":

  1. # Makefile for our example program
  2. # V1.1
  3. Example.exe: main.o Parrot.o
  4.     g++ -o Example.exe main.o Parrot.o
  5. main.o: Parrot.hpp
  6.     g++ -c main.cpp
  7. Parrot.o: Parrot.hpp
  8.     g++ -c parrot.cpp
  9. .PHONY: clean
  10. clean:
  11.     rm -f Example.exe main.o Parrot.o
More

   Jesus H. Christ slow down! Let's go through it bit by bit. Our original "Example.exe" recipe now has two dependencies: "main.o" and "Parrot.o", and the g++ command has adapted to these object files. But how do we make these dependencies? Just add another recipe! So there's one for "main.o" and "Parrot.o" from their respective source files. Let's ignore the ".PHONY" for the time being and move onto "clean". This recipe will remove the previous files that were created, allowing for a fresh start. When in doubt, call make clean to begin anew. This begs a question, if we can write make clean, then could we write make Example.exe? Indeed. You can choose which recipes you wish to follow if you desire. But as seen previously, if you make an edit in one of the files, make will compile those files that have changed, while leaving those that are up to date untouched. Very good. But this is actually quite verbose for a "Makefile" and can be shortened quite a bit. Take 3!

  1. # Makefile for our example program
  2. # V1.2
  3. CXX = g++
  4. OBJECTS = main.o Parrot.o
  5. Example.exe: $(OBJECTS)
  6.     $(CXX) -o $@ $(OBJECTS)
  7. main.o: Parrot.hpp
  8. Parrot.o: Parrot.hpp
  9. .PHONY: clean
  10. clean:
  11.     $(RM) Example.exe $(OBJECTS)
More

   We now introduce variables into the mix. We list the dependencies of "Example.exe" as a variable, which is previously initialized to all our desired object files. Additionally we added "$(CXX)" to allow our user to change up the compiler if they really want to (most likely not). Instead of explicitly writing in "Example.exe" after the -o flag, we can write "$@" which refers to the name of the recipe, namely "Example.exe". Savings. Finally in our "clean" recipe, we have a "$(RM)", which would expand to "rm -f". But what's with the lack of instructions with our object files? Don't we need those? Actually make is smart. It assumes (correctly) that you're going to call $(CXX) -c main.cpp and so forth. So why bother to write it? This reduces clutter, allowing one to see easily just the dependencies for each object file. For simple programs, this format should suffice. But for a more complicated architecture, you'd want to pass some more flags into our compiler. So let's explore that. This next bit uses Boost, which expands C++'s paultry library. Ah its one major downside. Anyways, I'll be using the Program Options and Filesystem libraries as to add some nice command line support. Here's the revised "main.cpp" for reference (don't bother to understand, just note the include's):

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <boost/filesystem.hpp>
  5. #include <boost/program_options.hpp>
  6. #include "Parrot.hpp"
  7. namespace {
  8.     const int SUCCESS = 0;
  9.     const int ERROR_IN_COMMAND_LINE = 1;
  10.     const int ERROR_UNHANDLED_EXCEPTION = 2;
  11. }
  12. namespace po = boost::program_options;
  13. int main(int argc, char *argv[]) {
  14.     try {
  15.         int osType;
  16.         std::vector<std::string> phrases;
  17.         // Add additional arguments
  18.         po::options_description desc("Options");
  19.         desc.add_options()
  20.             ("help,h", "Display help")
  21.             ("output_stream,o", po::value<int>(&osType)
  22.                 ->value_name("INT")->default_value(1),
  23.                 "1 for STDOUT, 2 for STDERR")
  24.             ("phrase", po::value< std::vector<std::string> >(&phrases)
  25.                 ->multitoken()->composing()->required(),
  26.                 "Phrase to be parroted back")
  27.         ;
  28.         // Add positional arguments
  29.         po::positional_options_description positional;
  30.         positional.add("phrase", -1);
  31.         // Set up variable map
  32.         po::variables_map vm;
  33.         try {
  34.             po::store(po::command_line_parser(argc, argv).options(desc)
  35.                       .positional(positional).run(), vm);
  36.             // Check help first in order to exit
  37.             if (vm.count("help")) {
  38.                 std::string exe = boost::filesystem::basename(argv[0]);
  39.                 std::cout << "USAGE: " << exe << " [options] <phrase>...\n\n"
  40.                           << desc << std::endl;
  41.                 return SUCCESS;
  42.             }
  43.             // Parse arguments and check constraints
  44.             po::notify(vm);
  45.         } catch (po::required_option &e) {
  46.             std::cerr << "Missing required option.\nTraceback:\n"
  47.                       << e.what() << std::endl;
  48.             return ERROR_IN_COMMAND_LINE;
  49.         } catch (po::error &e) {
  50.             std::cerr << "An unexpected argument parsing error occurred.\n"
  51.                       << "Traceback\n" << e.what() << std::endl;
  52.             return ERROR_IN_COMMAND_LINE;
  53.         }
  54.         // Check our arguments
  55.         if (vm.count("output_stream")) {
  56.             if (osType < 1 || osType > 2 ) { // Not STDOUT or STDERR
  57.                 std::cerr << "Output stream is neither STDOUT or STDERR, "
  58.                           << "exiting" << std::endl;
  59.                 return ERROR_IN_COMMAND_LINE;
  60.             }
  61.         }
  62.         // Compile our phrase
  63.         std::string phrase = "";
  64.         for (std::vector<std::string>::iterator it = phrases.begin();
  65.              it != phrases.end();
  66.              it++)
  67.         {
  68.             if (phrase == "") {
  69.                 phrase = *it;
  70.             } else {
  71.                 phrase += " " + *it;
  72.             }
  73.         }
  74.         // Let our pet speak
  75.         Parrot avianBeing = Parrot(phrase);
  76.         avianBeing.Speak((osType == 1) ? std::cout : std::cerr);
  77.         return SUCCESS;
  78.     } catch (std::exception &e) {
  79.         std::cerr << "An unexpected error in main occurred.\nTraceback:\n"
  80.                   << e.what() << std::endl;
  81.         return ERROR_UNHANDLED_EXCEPTION;
  82.     }
  83. }
More

   Kinda overkill just to add one option (whether to parrot to STDOUT or STDERR) and one positional argument (what phrase we want to parrot). To be honest, don't worry about understanding any of it; it's just meddling with syntax. But one question remains. How do we compile it? Boost requires the use of a dynamically linked library (.dll) on Windows systems or a shared object (.so) on MacOS(?) or Linux platforms. So this will require the -l flag. IMPORTANT NOTE: Remember when using the -l flag to remove the starting "lib" part and the extension (i.e. libboost_program_options.so -> boost_program_options). But where can we find this file? You may have to a bit of searching (like I did). I personally found mine at "/usr/lib/x86_64-linux-gnu" on a Ubuntu 64 bit machine but yours may be different (and your users).

  1. # Makefile for our example program
  2. # V1.3
  3. DEBUG = -g
  4. CXX = g++
  5. CXXFLAGS = -Wall $(DEBUG)
  6. LDFLAGS = -L/usr/lib/x86_64-linux-gnu \
  7.           -lboost_filesystem \
  8.           -lboost_program_options \
  9.           -lboost_system
  10. OBJECTS = main.o Parrot.o
  11. all: Example.exe
  12. Example.exe: $(OBJECTS)
  13.     $(CXX) -o $@ $(OBJECTS) $(LDFLAGS)
  14. main.o: Parrot.hpp
  15. Parrot.o: Parrot.hpp
  16. .PHONY: all clean
  17. clean:
  18.     $(RM) Example.exe $(OBJECTS)
More

   We add some additional flags to be passed by "$(CXX)", namely those in "$(CXXFLAGS)" and "$(LDFLAGS)". We allow for debugging (using -g which can be switched off by leaving "DEBUG" blank), to witness all warnings (using -Wall), to show where the location of where our .so files are (using -L), and the library files themselves (using -l, sneaky boost/filesystem.hpp requires both libboost_filesystem.so and libboost_system.so). IMPORTANT NOTE: the "$(LDFLAGS)" must as the last argument for "$(CXX)" or else you'll get an error... took me too long to find this one out! So where's "$(CXXFLAGS)" in this compilation command? Well make has a slew of default variables that it uses implicitly (see Implicit Variable Rules for too much info). If you do run this Makefile, you'll see the -Wall and -g flags are indeed used. The new "all" recipe is meant to be for projects with multiple executables, but we only have one sadly. When you run make, it is actually make all so you don't need to type the extra noun. Finally let's address ".PHONY". If you're extremely observant (maybe even more than Holmes), all our recipes had names that became a file. For example, the recipe "main.o" was created through the compilation of "main.cpp" into an object file. This is not a coincidence, but an integral part of make. The "clean" recipe does not create a file called "clean". So what happens if we had a file called "clean"? make would check the dependencies of the "clean" recipe, to which there are none, and conclude that the "clean" file is up to date. The instructions below would never execute and desired programs/object files would not be deleted. Oh dear. However, when we declare "clean" as a dependency to ".PHONY", this overrides the up-to-date-checking and allows its recipe to be conducted. To wrap up, here's a final overview of the commands and showcase of the program we've built:

  1. make
  2. g++ -Wall -g   -c -o main.o main.cpp
  3. g++ -Wall -g   -c -o Parrot.o Parrot.cpp
  4. g++ -o Example.exe main.o Parrot.o -L/usr/lib/x86_64-linux-gnu -lboost_filesystem -lboost_program_options -lboost_system
  5. ./Example.exe -h
  6. USAGE: Example [options] <phrase>...
  7. Options:
  8.   -h [ --help ]                   Display help
  9.   -o [ --output_stream ] INT (=1) 1 for STDOUT, 2 for STDERR
  10.   --phrase arg                    Phrase to be parroted back
  11. ./Example.exe -o 2 Open the pod bay doors, HAL
  12. Open the pod bay doors, HAL
  13. make clean
  14. rm -f Example.exe main.o Parrot.o
More

   Hopefully this tutorial gave you enough insight into Makefiles in order to write one of your own. There is _much_ more to this utility, such as wildcard characters, conditional expressions, functions, etc. but I won't go into them (because I don't quite need that complexity for my needs just yet). If you're curious, take a gander: Make Manual.