Use the make tool to speed up development

I’m happy to have a new contributor to my project, who did some cleanup to Makefile (thanks, @buoto!). I thought it’s a good time to write a few words on how the make tool works.

Windows users: to check if you have make installed on your machine type in your command line: make --version. If you don’t, I suggest downloading Gow, which will equip your Windows with most useful Linux commands. Windows 10 users can use Windows Subsystem for Linux feature (how to enable).

What is make tool for?

Reading my tutorials, you could notice I compile many of my examples by hand, i.e. I run commands like this every time:

$ clang++ -std=c++14 timepoints.cpp -o timepoints.exe && ./timepoints.exe

It takes time, is repetitive, it’s easy to forget some option or make a typo somewhere. Another problem is, the set of commands working on one machine may not work on another because of e.g. different OS installed.

make provides a unified environment and a framework for building projects. Not only it helps with managing compilation instructions, but also keeps track of what dependencies were already built and what can be reused. For example, C++ files are compiled into object files (.o). If nothing changed in a particular module, then the corresponding code doesn’t have to be recompiled. The bigger the project the more time you save.

GNU make is probably the most popular implementation

Basic Makefile

We have a simple “Hello World” file. We usually compile it like this:

$ g++ main.cpp -o example
$ ./example
Hello, make!

Now we would like to use the new method. Create Makefile in your folder:

# This is a sample Makefile

build:
    g++ main.cpp -o example

The syntax is really easy. You put target name, they semicolon. After that, in new lines (starting with a tab!) go commands. If you run make build, g++ will compile the file. You will see all the commands as if you typed them yourself.

Using macros

You can use variables in your Makefile. Some of them are already provided for you, like CXX (c++ compiler tool) or OS. To set variable name, just use normal assignment. To get variable value, use the dollar sign and braces, e.g. $(BIN_DIR).

# This is a sample Makefile

CXX = clang++

BIN_DIR = bin
EXECUTABLE = example

build:
    @echo Building on $(OS):
    mkdir -p $(BIN_DIR)
    $(CXX) main.cpp -o $(BIN_DIR)/$(EXECUTABLE)

run:
    ./$(BIN_DIR)/$(EXECUTABLE)

What has changed:

  1. We have set the compiler to clang++ (you can use anything)
  2. build target shows what OS we are using (@ at the beginning will suppress writing command to stdout)
  3. Executable is put in bin directory now
  4. We have a new rule to run the executable

We can order our tool to build multiple targets at a time, so we will run our program afterward:

$ make build run
Building on Windows_NT:
mkdir -p bin
clang++ main.cpp -o bin/example
./bin/example
Hello, make!

Dependencies

Let’s say we need to extract some functionality to another file. Here are the modifications:

main.cpp

#include <iostream>

#include "greetings.hpp"

int main() {
 greet();
 return 0;
}

greetings.hpp

#ifndef GREETINGS
#define GREETINGS

#include <iostream>

void greet();

#endif

greetings.cpp

#include "greetings.hpp"

void greet() {
 std::cout << "Hello, make!" << std::endl;
}

Now if we run make build run new files won’t be taken into consideration:

$ make build run
Building on Windows_NT:
mkdir -p bin
clang++ main.cpp -o bin/example
C:\Users\kantoniak\AppData\Local\Temp\main-724567.o:(.text+0x67): undefined reference to `greet()'
clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [build] Error 1

We have to update Makefile with dependencies. Basically, we say that particular rule needs another to finish before. For example, to build executable, we need source files to be compiled and then we just link all of them.

# This is a sample Makefile

CXX = clang++

OBJ_DIR = obj
BIN_DIR = bin
EXECUTABLE = example

$(OBJ_DIR)/greetings.o: greetings.cpp greetings.hpp
 mkdir -p $(OBJ_DIR)
 $(CXX) -c greetings.cpp -o $(OBJ_DIR)/greetings.o

$(OBJ_DIR)/main.o: main.cpp greetings.hpp
 mkdir -p $(OBJ_DIR)
 $(CXX) -c main.cpp -o $(OBJ_DIR)/main.o

build: $(OBJ_DIR)/main.o $(OBJ_DIR)/greetings.o
 @echo Building on $(OS):
 mkdir -p $(BIN_DIR)
 $(CXX) $(OBJ_DIR)/main.o $(OBJ_DIR)/greetings.o -o $(BIN_DIR)/$(EXECUTABLE)

clean:
 rm -rf $(OBJ_DIR)/*
 rm -rf $(BIN_DIR)/*

run:
 ./$(BIN_DIR)/$(EXECUTABLE)

It means that before build target will be made, it needs obj/main.o and obj/greetings.o. Actually, these files get compiled before:

$ make build run
clang++ -c main.cpp -o obj/main.o
clang++ -c greetings.cpp -o obj/greetings.o
Building on Windows_NT:
clang++ obj/main.o obj/greetings.o -o bin/example
./bin/example
Hello, make!

Need more?

The basics are covered. But make tool is much more, and there are even more tools extending its functionality, adding GUIs, etc. You will find other resources here: