Paradox
Paradox

Reputation: 2035

Compiling multiple test files efficiently with Makefile

I have a C++ project with the following structure:

/Project
    Makefile
    /src (.cpp source files)
     ...
    /include (.h header files)
     ...    
    /libs 
     ...
    /build (.o object files)
     ...    
    /tests (target .cpp files I want to compile)
     test1.cpp
     test2.cpp
     test3.cpp
     ...    
    /bin (output directory for compiled files)
     ...

For the tests inside my test file, I would like to be able to

But I would like to be able to do this without needing to define new variables (e.g. TARGET1, TARGET2,...) for each new test file, nor add a bunch of new lines to my makefile for each new test file.

For example, right now I have something like:

CXX = g++
SRC_DIR = ./src
BUILD_DIR = ./build
LIB = -I libs
INC = -I include 

SRCS = $(shell find $(SRC_DIR) -type f -name *.cpp)
OBJS = $(patsubst $(SRC_DIR)/%, $(BUILD_DIR)/%, $(SRCS:.cpp=.o))


TARGET1 ?= test1.cpp
TARGET2 ?= test2.cpp
TARGET3 ?= test3.cpp

all: $(OBJS)
        $(CXX) ./tests/$(TARGET1).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET1)
        $(CXX) ./tests/$(TARGET2).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET2)
        $(CXX) ./tests/$(TARGET3).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET3)

$(TARGET1): $(OBJS)
        $(CXX) ./tests/$(TARGET1).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET1)

$(TARGET2): $(OBJS)
        $(CXX) ./tests/$(TARGET2).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET2)

$(TARGET3): $(OBJS)
        $(CXX) ./tests/$(TARGET3).cpp $(LIB) $(INC) $^ -o ./bin/$(TARGET3)

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
        $(CXX) $(INC) -c -o $@ $<

which does the job, but isn't very scalable. How could I do this scalably?

Upvotes: 1

Views: 1774

Answers (1)

Renaud Pacalet
Renaud Pacalet

Reputation: 29280

Make has some more tricks that you can use (not tested):

CXX = g++
SRC_DIR = src
BUILD_DIR = build
TEST_DIR = tests
BIN_DIR = bin
LIB = -I libs
INC = -I include

SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))

TESTS = $(wildcard $(TEST_DIR)/*.cpp)
TARGETS = $(patsubst $(TEST_DIR)/%.cpp,$(BIN_DIR)/%,$(TESTS))

all: $(TARGETS)

$(TARGETS): $(BIN_DIR)/%: $(OBJS)
    $(CXX) $(TEST_DIR)/$*.cpp $(LIB) $(INC) $^ -o $@

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
    $(CXX) $(INC) -c -o $@ $<

The main trick here is the static pattern rule for $(TARGETS): in the recipe $* expands as the stem of the pattern. The other tricks are a simpler use of patsubst and the use of wildcard instead of the less efficient shell find. Note that this last one works only if your source files are flat in src, not if they are organized in a hierarchy of sub-directories.

But this does not answer your most tricky request: a way to invoke make testX instead of make bin/testX. So, here is the most tricky part:

SHORTERTARGETS = $(patsubst $(TEST_DIR)/%.cpp,%,$(TESTS))

.PHONY: $(SHORTERTARGETS)

# $(1): short target
define TARGETS_rule
$(1): $(BIN_DIR)/$(1)
endef
$(foreach t,$(SHORTERTARGETS),$(eval $(call TARGETS_rule,$(t))))

You can even use this foreach-eval-call to factorize other parts of your Makefile:

CXX = g++
SRC_DIR = src
BUILD_DIR = build
TEST_DIR = tests
BIN_DIR = bin
LIB = -I libs
INC = -I include

SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))

TESTS = $(wildcard $(TEST_DIR)/*.cpp)
TARGETS = $(patsubst $(TEST_DIR)/%.cpp,$(BIN_DIR)/%,$(TESTS))
SHORTERTARGETS = $(patsubst $(TEST_DIR)/%.cpp,%,$(TESTS))

.PHONY: all $(SHORTERTARGETS)

all: $(TARGETS)

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
    $(CXX) $(INC) -c -o $@ $<

# $(1): short target
define TARGETS_rule
$(1): $(BIN_DIR)/$(1)
$(BIN_DIR)/$(1): $(OBJS)
    $(CXX) $(TEST_DIR)/$(1).cpp $(LIB) $(INC) $$^ -o $$@
endef
$(foreach t,$(SHORTERTARGETS),$(eval $(call TARGETS_rule,$(t))))

The most difficult to understand in this last version is the need of $$in the recipe (double expansion). But here the GNU make manual is your friend.

Upvotes: 1

Related Questions