Read time: 5.7 minutes (571 words)
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. Notice 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 easier 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 these we went over earlier, and should be easy enough to figure out. Exactly what options are used for each build tool is something 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.