Apendix A Build Tools - itzjac/cpplearning GitHub Wiki

make

During the learning process of a programming language, dealing with large projects and massive amounts of files is generally not a problem. Most often, you will contain a massive amount of small exercises divided perhaps on chapters or lessons, this is where we can introduce make as a great tool to help us automate builds without having to repeat the execution of several command sequences.

make is a an utility that automatically builds executable programs and libraries from source code by reading files called makefiles which specify how to derive the target program http://en.wikipedia.org/wiki/Make_(software).

A great advantage of this utility is that can be used on the most popular OS like Linux, Windows and OS X. For this course we will focus on using the latest version of the utility GNU Make & Microsoft NMake.

Instructions in a makefile are always of the form target: (dependencies) (commands)

**target **which defines an artifact or goal to complete in the makefile **dependencies **or components (optional) required to generate the target **commands **or rules (optional) which define how to transform the dependencies into the target The default target name, typically used as an entry point of a makefile is the all target.

Commands to makefile

The first makefiles will only produce 1 executable given 1 file. Locate the path where the source file is located and create an empty file called makefile which contains

all: 
    g++ -main.cpp 

The instruction is defined: 1 target, 0 dependencies and 1 command. The command contains only known symbols: is possible to compile, generate exe and complete the target. The first attempt of automating the build, it will appear to be really simple, we are in essence moving the command line to a file.

The execute the makefile, open a command line terminal where the source file and the makefile are located and run

$make

The command will look for a file named "makefile" located in the current path, once found it will look for the "all:" target and start executing the dependencies and/or commands found.

NOTE: What if we want to use another name for the makefile different than the default one, like another_makefile, use the option -f. Use the manual of the make utility installed on your OS for more details.

make -f another_makefile

Breaking up the compilation process

Having moved the compilation command into a file doesn't seem to be useful or productive, but next we are going to elaborate the makefile so that the compilation process is broken into steps. With an step by step process we create a makefile that can support several executable programs, it can control which executable is compiled depending on appropriate source files and changes, and it can define other targets that include cleaning or recompiling a whole project. Consider a source file classes.cpp that will produce classes.out executable, our goal is to achieve the classes.out executable program.

all: classes.out

The instruction is defined: 1 target, 1 dependencies and 0 commands. We are responsible on defining new targets to unknown dependencies until they are fully resolved. Once a target contains only known dependencies, the make file will roll back one by one until it gets the initial target.

To resolve each of the unknown dependencies, we will use the artifacts generated by the compilation process (source file -> object file -> executable). Following the process, we can infer that the previous artifact next to the executable is the object file, it can be read like "having an object file we can generate the executable". We use this artifact for the next instruction

classes.out: classes.obj
    g++ -o classes.out classes.obj

The instruction is defined as: 1 target, 1 dependencies and 1 command.

Yet again, there exists another unknown dependency, classes.obj. From the compilation process, the previous artifact to a object file is the source file, classes.cpp, resulting

classes.obj: classes.cpp
    g++ -c classes.cpp classes.obj

The instruction is defined as: 1 target, 1 dependencies and 1 command. In this case, the rule contains only known dependencies, classes.cpp, a file which exists and can be used to execute the command. makefile will execute it, generate the classes.obj and start to roll back to the previous unknown dependency and until it gets the initial target, classes.out. With all instructions together

all: classes.out

classes.out: classes.obj
    g++ -o classes.out classes.obj

classes.obj: classes.cpp
    g++ -c classes.cpp classes.obj

Adding more executables

Let's consider we have a second program named numbers.out in this path, based on the current makefile it will be really easy to extend it to include numbers.out.

all: classes.out numbers.out

classes.out: classes.obj
    g++ -o classes.out classes.obj

classes.obj: classes.cpp
    g++ -c classes.cpp classes.obj

numbers.out: numbers.obj
    g++ -o numbers.out numbers.obj

numbers.obj: numbers.cpp
    g++ -c numbers.cpp numbers.obj

all target will now generate two dependencies which are two different executables. Let's consider that adding a third, fourth or n exe's can be done in the same way. Try changing one of the programs and run again make, only the program affected will be re-compiled the rest remains untouched, saving us precious compiling time.

Using special symbols

For our last example, notice how rules get repeated: only the file names are changed.

g++ -c numbers.cpp numbers.obj
g++ -c classes.cpp classes.obj

make provide us with special symbols to avoid duplicated instructions. Using the instruction with the special symbols

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

The target/dependencies means "any object file of name % will depend on the file of same name %". The rule or command means "use the input file (file being processed compiled or linked) $< to generate the file $@". Applying this special symbols to the makefile is

all: classes.out numbers.out

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

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

Using MACROS

Another common practice to group certain artifacts producing during the execution of the makefile are MACROS. We can use it to group the final objects, executable files

OBJECTS = classes.out numbers.out

To use the MACRO we use the symbol $(OBJECTS)

all: $(OBJECTS)

It is also convenient to create MACROS defining the compiler and compilation flags, in this case we are using the flag -std=c++0x which enables the ISO C++11

CC = g++
CCFLAGS = -std=c++0x

Assigning this macros into the makefile

OBJECTS = classes.out numbers.out
CC=g++
CCFLAGS= -std=c++0x

all: $(OBJECTS)

%.obj:%.cpp
    $(CC) $(CCFLAGS) -c $< -o $@

%.exe: %.o 
   $(CC) $(CCFLAGS) $< -o $@

Grouping components of the compilation process like this, it will be easier to create another makefile version for another compiler.

CMake

Have you thought about distributing your code to several platforms? Wouldn't be easier if there exists one tool that will generate appropriate makefiles to whatever compiler is installed? That is just what CMake does.

Pitfalls

  • Once you go cmake , there is no return. When generating VC sln files, the paths are hardcoded. If then you want to get the pure solution distributed bypassing CMake, you are going to get into a big headache.