Makefile Tutorial
For Repeated Compilation
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.
Download
MakefileExampleV1.0.tar.gz | MakefileExampleV1.1.tar.gz | MakefileExampleV1.2.tar.gz | MakefileExampleV1.3.tar.gz |
Demonstration
Let's start with a 'dead' simple program which we wish to compile:
- #include <iostream>
- int main(int argc, char *argv[]) {
- std::cout << "Goodbye, cruel world!" << std::endl;
- return 0;
- }
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):
- # Makefile for our example program (this is a comment)
- # V1.0
- Example.exe:
- g++ -o Example.exe main.cpp
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":
- #ifndef PARROT_H
- #define PARROT_H
- #include <ostream>
- #include <string>
- // Give it a phrase and you'll never hear the end of it.
- class Parrot {
- public:
- Parrot(std::string Phrase) : phrase(Phrase) {}
- // Echoes the provided phrase to a given output stream
- void Speak(std::ostream &os);
- private:
- std::string phrase;
- };
- #endif
- #include <ostream>
- #include <string>
- #include "Parrot.hpp"
- // Echoes the provided phrase to a given output stream
- void Parrot::Speak(std::ostream &os) {
- os << phrase << std::endl;
- }
- #include <iostream>
- #include "Parrot.hpp"
- int main(int argc, char *argv[]) {
- Parrot avianBeing = Parrot("Polly isn't fond of crackers");
- avianBeing.Speak(std::cout);
- return 0;
- }
Now back to our "Makefile":
- # Makefile for our example program
- # V1.1
- Example.exe: main.o Parrot.o
- g++ -o Example.exe main.o Parrot.o
- main.o: Parrot.hpp
- g++ -c main.cpp
- Parrot.o: Parrot.hpp
- g++ -c parrot.cpp
- .PHONY: clean
- clean:
- rm -f Example.exe main.o Parrot.o
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!
- # Makefile for our example program
- # V1.2
- CXX = g++
- OBJECTS = main.o Parrot.o
- Example.exe: $(OBJECTS)
- $(CXX) -o $@ $(OBJECTS)
- main.o: Parrot.hpp
- Parrot.o: Parrot.hpp
- .PHONY: clean
- clean:
- $(RM) Example.exe $(OBJECTS)
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):
- #include <iostream>
- #include <string>
- #include <vector>
- #include <boost/filesystem.hpp>
- #include <boost/program_options.hpp>
- #include "Parrot.hpp"
- namespace {
- const int SUCCESS = 0;
- const int ERROR_IN_COMMAND_LINE = 1;
- const int ERROR_UNHANDLED_EXCEPTION = 2;
- }
- namespace po = boost::program_options;
- int main(int argc, char *argv[]) {
- try {
- int osType;
- std::vector<std::string> phrases;
- // Add additional arguments
- po::options_description desc("Options");
- desc.add_options()
- ("help,h", "Display help")
- ("output_stream,o", po::value<int>(&osType)
- ->value_name("INT")->default_value(1),
- "1 for STDOUT, 2 for STDERR")
- ("phrase", po::value< std::vector<std::string> >(&phrases)
- ->multitoken()->composing()->required(),
- "Phrase to be parroted back")
- ;
- // Add positional arguments
- po::positional_options_description positional;
- positional.add("phrase", -1);
- // Set up variable map
- po::variables_map vm;
- try {
- po::store(po::command_line_parser(argc, argv).options(desc)
- .positional(positional).run(), vm);
- // Check help first in order to exit
- if (vm.count("help")) {
- std::string exe = boost::filesystem::basename(argv[0]);
- std::cout << "USAGE: " << exe << " [options] <phrase>...\n\n"
- << desc << std::endl;
- return SUCCESS;
- }
- // Parse arguments and check constraints
- po::notify(vm);
- } catch (po::required_option &e) {
- std::cerr << "Missing required option.\nTraceback:\n"
- << e.what() << std::endl;
- return ERROR_IN_COMMAND_LINE;
- } catch (po::error &e) {
- std::cerr << "An unexpected argument parsing error occurred.\n"
- << "Traceback\n" << e.what() << std::endl;
- return ERROR_IN_COMMAND_LINE;
- }
- // Check our arguments
- if (vm.count("output_stream")) {
- if (osType < 1 || osType > 2 ) { // Not STDOUT or STDERR
- std::cerr << "Output stream is neither STDOUT or STDERR, "
- << "exiting" << std::endl;
- return ERROR_IN_COMMAND_LINE;
- }
- }
- // Compile our phrase
- std::string phrase = "";
- for (std::vector<std::string>::iterator it = phrases.begin();
- it != phrases.end();
- it++)
- {
- if (phrase == "") {
- phrase = *it;
- } else {
- phrase += " " + *it;
- }
- }
- // Let our pet speak
- Parrot avianBeing = Parrot(phrase);
- avianBeing.Speak((osType == 1) ? std::cout : std::cerr);
- return SUCCESS;
- } catch (std::exception &e) {
- std::cerr << "An unexpected error in main occurred.\nTraceback:\n"
- << e.what() << std::endl;
- return ERROR_UNHANDLED_EXCEPTION;
- }
- }
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).
- # Makefile for our example program
- # V1.3
- DEBUG = -g
- CXX = g++
- CXXFLAGS = -Wall $(DEBUG)
- LDFLAGS = -L/usr/lib/x86_64-linux-gnu \
- -lboost_filesystem \
- -lboost_program_options \
- -lboost_system
- OBJECTS = main.o Parrot.o
- all: Example.exe
- Example.exe: $(OBJECTS)
- $(CXX) -o $@ $(OBJECTS) $(LDFLAGS)
- main.o: Parrot.hpp
- Parrot.o: Parrot.hpp
- .PHONY: all clean
- clean:
- $(RM) Example.exe $(OBJECTS)
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:
- make
- g++ -Wall -g -c -o main.o main.cpp
- g++ -Wall -g -c -o Parrot.o Parrot.cpp
- g++ -o Example.exe main.o Parrot.o -L/usr/lib/x86_64-linux-gnu -lboost_filesystem -lboost_program_options -lboost_system
- ./Example.exe -h
- USAGE: Example [options] <phrase>...
- Options:
- -h [ --help ] Display help
- -o [ --output_stream ] INT (=1) 1 for STDOUT, 2 for STDERR
- --phrase arg Phrase to be parroted back
- ./Example.exe -o 2 Open the pod bay doors, HAL
- Open the pod bay doors, HAL
- make clean
- rm -f Example.exe main.o Parrot.o
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.