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
TARGET_EXEC = main
TEST_EXEC = unittest
BUILD_DIR = build
SRC_DIRS = src
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 main.cpp -or -name *.c -or -name *.s)
OBJS_TARGET := $(SRCS_TARGET:%=$(BUILD_DIR)/%.o)
OBJS_TEST := $(SRCS_TEST:%=$(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
CFLAGS = -std=c99 -pedantic -O1
#if shared library target
#CFLAGS += -shared -undefined dynamic_lookup
CXXFLAGS = -std=c++2a
LDFLAGS =

.PHONY: clean run all

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

# unittest target (C++)
$(BUILD_DIR)/$(TEST_EXEC): $(OBJS_TEST)
$(CXX) $(OBJS_TEST) -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 $@

all: $(BUILD_DIR)/$(TARGET_EXEC) $(BUILD_DIR)/$(TEST_EXEC)

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

-include $(DEPS)

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. It defines two targets, for a main binary and a unittest binary which could be linked against a testing library of your choice.

Your browser is out-of-date!

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

×