Advanced Makefile

I use Make to build many programs from the command line. There are more advanced tools around, but I feel that beginning programming students need to be exposed to Make so they can see how powerful such build tools are, and be able to use them in their programming projects. Make has been around so long, and it is part of so many projects, they just have to learn a bit about it.

I usually start students off in exploring Make with very simple patterns, but eventually they need to see the real power of such a tool.

So, in developing a Graphics library for students to use in my programming class, I decided to add a few more powerful features of Make to the project Makefile.

In this note, I will develop a Makefile suitable to use in a test driven development based C++ project. As part of this development, we will design a project directory structure suitable for building a library, intended to be used in other C++ projects. The Makefile will build, test, and install that library code on all major development systems: Windows, Linux, and Mac.

Finding Source Files

Simple student Makefile setups begin with a list of the course files in a project. These files are usually collected into a common project directory, along side of other directories for testing, building and documentation. Here is a starting point for a good project directory layout:

project_directory\
    |
    +- src\ - holds main project code
    |
    +- include\ library header files
    |
    +- lib\ holds library code files
    |
    +- bin\ - holds all files generated by building the project
    |
    +- bin\prod - holds all files generated for production
    |
    +- bin\debug - holds all files generated for testing
    |
    +- test\ - holds project test code
    |
    +- docs\ documentation for the project ( sphinx based for me!)

The first part of the project Makefile lists the two programs this file will build. One is the main application, and the second is a test program that runs a set of tests we will use to make sure the code works properly. More on that later

# Makefile - for CALassembler

APP_TARGET  =   demo
TEST_TARGET =   run_tests

Next, we name the major directories we use in this project. Creating names makes it easy to modify this file for another project later:

# directories ---------------------------------------------
SRC_DIR     = src
LIB_DIR     = lib
TEST_DIR    = tests
INC_DIR     = include

In the next section, we set up a few names that will hold options we use on various commands later. Botice that “+=” operator. You can add things to a name in a later step using this trick.

# system dependencies
CFLAGS = -I $(INC_DIR)
CFLAGS += -MMD

This next section tries to deal with building the application on different operating systems. This code will check the operating system and set a few names differently depending on what system we are running on. The big difference here is that programs on WIndows need to be named something.exe`, but on Mac/Linus, they are just named ``something. We set up a name called EXT and set it to .exe on windows, and nothing on other systems. You will see this at work later.

ifeq ($(OS), Windows_NT)
    EXT = .exe
    RM = del
    CFLAGS += -std=c++11
    CXX = C:\usr\local\mingw32\bin\g++.exe
    PREFIX =
else
    EXT =
    PREFIX = ./
    RM = rm -f
    CXX = g++
    UNAME_S = $(shell uname -s)
    ifeq ($(UNAME_S), Darwin)
        CFLAGS +=
    endif
    ifeq ($(UNAME_S), Linux)
        CFLAGS +=
    endif
endif

Now, we can have Make search the project directories for any source files that will need to be processed:

# filw lists ----------------------------------------------
SRC_FILES   = $(wildcard $(SRC_DIR)/*.cpp)
LIB_FILES   = $(wildcard $(LIB_DIR)/*.cpp)
TEST_FILES  = $(wildcard $(TEST_DIR)/*.cpp)
SRC_OBJS    = $(SRC_FILES:.cpp=.o)
LIB_OBJS    = $(LIB_FILES:.cpp=.o)
TEST_OBJS   = $(TEST_FILES:.cpp=.o)
ALL_OBJS    = $(SRC_OBJS) $(LIB_OBJS) $(TEST_OBJS)
DEPENDS = $(ALL_OBJS:.o=.d)

In this section we have set up something that makes projects esier to manage. The g++ compiler can be told to read all the source files and figure out what each one depends on. It does this by looking at the include lines. The output of this step is a file called something.d (for depends). We will use these files to make sure Make can build your code in the most efficient manner.

The last part of the Makefile sets up the rules needed to build all the project components. These rules are similar to thse we went over earlier, and should be easy enough to figure out. Exactly what options are used for each build tool is somehting we can worry about later for this example.

# Build targets -------------------------------------------

.PHONY:
all:    $(APP_TARGET)$(EXT) $(TEST_TARGET)$(EXT)

$(APP_TARGET)$(EXT):    $(SRC_OBJS) $(LIB_OBJS)
	$(CXX) -o $@ $(LFLAGS) $^

$(TEST_TARGET)$(EXT):   $(TEST_OBJS) $(LIB_OBJS)
	$(CXX) -o $@ $(LFLAGS) $^

.PHONY:
clean:
	$(RM) $(APP_TARGET)$(EXT) $(TEST_TARGET)$(EXT)
	$(RM) $(ALL_OBJS) $(DEPENDS)

.PHONY:
run:    $(APP_TARGET)$(EXT)
	$(PREFIX)$(APP_TARGET) -d test.cal

.PHONY:
test:   $(TEST_TARGET)$(EXT)
	$(PREFIX)$(TEST_TARGET)

.PHONY:
docs:
	cd documentation && make html

.PHONY:
view:
	open -a Firefox documentation/_build/index.html

.PHONY:
spelling:
	cd documentation && make spelling

# implicit rules ------------------------------------------

%.o:    %.cpp
	$(CXX) -c $(CFLAGS) $< -o $@

-include $(DEPENDS)

Learning More

This gives you a sense of what Make can do. You can learn a lot by scanning projects on Github and looking to see how they use Makefiles to build their programs.