Universal Makefile

Build System Frustration

Like any developer, I like to use the right tool for the job – when it comes to coding projects, the choice of language can greatly impact the performance and workflow of the project in the future. For quick CLI’s and web projects, I like to use Python; for short single-use and domain-specific scripts, shell is nice; for native concurrency primitives and a simple language, I use Golang. Many languages that are catching the world by a storm, like Golang, Rust, Kotlin, Crystal, and Nim have built-in or otherwise generally accepted build systems that are relatively effortless to leverage in making easily buildable and installable packages. However, as a student in Electrical Engineering who frequently drops down to lower level languages like C and C++, project build systems pose a larger barrier to entry. To solve this problem, I have a relatively simple “universal” Makefile for C and C++ (and mixed), which can be easily adaptable for many projects.

I have also experimented with CMake as a meta-build system. CMake and utilities like it do have their advantages – they make cross-compilation easier, they can be used with many different build systems (even at the same time) like XCode, Make, Ninja, etc. However, I’ve found CMake to be overkill for small projects with few (0-5) executable targets. This Makefile plays more nicely with version control (fewer things to put in .gitignore), and is a lower cognitive load to understand – a review of implicit Make rules covers pretty much everything.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Flags
CFLAGS = -std=c99 -pedantic -O1

#if shared library target
#CFLAGS += -shared -undefined dynamic_lookup

TARGET_EXEC ?= project-name
BUILD_DIR ?= ./build
SRC_DIRS ?= ./src

SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

DEPS := $(OBJS:.o=.d)

INC_DIRS := $(shell find $(SRC_DIRS) -type d)
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

CPPFLAGS = $(INC_FLAGS) -MMD -MP -ggdb
CXXFLAGS = -std=c++14 -stdlib=libc++
LDFLAGS = -stdlib=libc++

# main target (C)
#$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
# $(CC) $(OBJS) -o $@ $(LDFLAGS)

# main target (C++)
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
$(CXX) $(OBJS) -o $@ $(LDFLAGS)

# assembly
$(BUILD_DIR)/%.s.o: %.s
$(MKDIR_P) $(dir $@)
$(AS) $(ASFLAGS) -c $< -o $@

# c source
$(BUILD_DIR)/%.c.o: %.c
$(MKDIR_P) $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# c++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
$(MKDIR_P) $(dir $@)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean

clean:
$(RM) -r $(BUILD_DIR)

-include $(DEPS)

MKDIR_P ?= mkdir -p


.PHONY: run
run: $(BIN)
./build/project-name

In writing this Makefile, I pulled ideas heavily from the following projects:

My version adds CXXFLAGS and CFLAGS, as well as a debugger switch for GCC to compile with debugging symbols.

Update:

In my development of the samurai-c++ project, I found that I wanted two targets: one command line utility that will eventually be the end product, and one unit test binary to run during development. I had tried to accomplish this split before, but came accross issues with having multiple lists of objects and dependencies which would interfere with eachother in the -include(DEPS) call. I came up with the following solution, which maintains all objects in the overall objects list, all files’ dependencies, but keeps the two files with main() functions away from eachother to avoid linker errors.

I modified the variable declarations to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
TARGET_EXEC = project-name
TEST_EXEC = unittest
BUILD_DIR = ./build
SRC_DIRS = ./src
#HEAD_DIRS ?= ./include

SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
SRCS_TARGET := $(shell find $(SRC_DIRS) -name *.cpp ! -name unittest.cpp -or -name *.c -or -name *.s)
SRCS_TEST := $(shell find $(SRC_DIRS) -name *.cpp ! -name project-name.cpp -or -name *.c -or -name *.s)
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
OBJS_TARGET := $(SRCS_TARGET:%=$(BUILD_DIR)/%.o)
OBJS_TEST := $(SRCS_TEST:%=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)

And I modified the two binary targets as follows:
1
2
3
4
5
6
7
.PHONY: all
all: $(BUILD_DIR)/$(TARGET_EXEC) $(BUILD_DIR)/$(TEST_EXEC)
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS_TARGET)
$(CXX) $(OBJS_TARGET) -o $@ $(LDFLAGS)

$(BUILD_DIR)/$(TEST_EXEC): $(OBJS_TEST)
$(CXX) $(OBJS_TEST) -o $@ $(LDFLAGS)

This way each binary links against the correct set of objects.

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×