I'm new to c++ and struggling to understand why my Makefile does not do the expected.
This is the (desired) structure of my project:
MyProject
├── build
│ ├── debug
│ │ ├── main
│ │ └── src
│ │ └── call .o and .d files compiled with debug flag
│ └── release
│ ├── main
│ └── src
│ └── all .o and .d files compiled without additional flags
├── include
│ └── all .hpp files
├── lib
│ └── currently empty
├── src
│ └── all .cpp files
└── Makefile
The folder structure in the build folder is the desired part - that's the part that doesn't work.
My intention is: whenever I call make
, it builds the .o
and .d
files into the release
folder following the src folder structure, and the main executable is placed directly in /release
.
Similarly, whenever I call make debug
it builds into the debug
folder (with the additional debug flags being passed).
ATM calling make
results in this structure:
MyProject
├── build
│ ├── main
│ └── src
│ └── all .o and .d files compiled without additional flags
...
So make
does not result in the usage of the release
folder even though release
should be the default target.
Calling make debug
(and make release
if you replace accordingly) result in this structure:
MyProject
├── build
│ ├── debug
│ │ └── main
│ └── src
│ └── all .o and .d files (not compiled anew! And hence without the debug flags)
...
Only the main
executable is in the debug
folder. The rest is under /build
and not compiled again if I invoked make
before. Therefore, the debug flags are missing and I don't get my debugging output.
So the build works in general, the files are just not where I want them to be and in case of debug I have to run make clean
every time to get the right compilation and my debugging output.
I understand that these blocks are called recipe and everything indented with a tab is passed to the shell. I don't think I understand the general execution flow of a Makefile, though. This part
$(BUILD)/%.o: %.cpp
$(MD) $(@D)
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -MMD -o $@
seems to be executed before anything else. I don't even see from where it is called. Especially this is the part that fails as it does not have the updated BUILD
path.
This is my full Makefile:
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -g
debug: CXXFLAGS += -DDEBUG -g
LFLAGS =
SRC := src
BUILD = build
release: BUILD := $(BUILD)/release
debug: BUILD := $(BUILD)/debug
INCLUDE := include
LIB := lib
ifeq ($(OS),Windows_NT)
MAIN := main.exe
SOURCEDIRS := $(SRC)
INCLUDEDIRS := $(INCLUDE)
LIBDIRS := $(LIB)
FIXPATH = $(subst /,\,$1)
RM := del /q /f
MD := mkdir
else
MAIN := main
SOURCEDIRS := $(shell find $(SRC) -type d)
INCLUDEDIRS := $(shell find $(INCLUDE) -type d)
LIBDIRS := $(shell find $(LIB) -type d)
FIXPATH = $1
RM := rm -f
MD := mkdir -p
endif
INCLUDES := $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))
LIBS := $(patsubst %,-L%, $(LIBDIRS:%/=%))
SOURCES := $(wildcard $(patsubst %,%/*.cpp, $(SOURCEDIRS)))
OBJECTS := $(SOURCES:%.cpp=$(BUILD)/%.o)
DEPS := $(OBJECTS:.o=.d)
all: $(OBJECTS)
$(MD) $(BUILD)
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(BUILD)/$(MAIN) $(OBJECTS) $(LDFLAGS) $(LIBS)
# this seems to be executed whenever calling any "make" command
# this command does not have the updated BUILD path (aka build/release or build/debug)
# Is this a recipe as well? When is it called?
$(BUILD)/%.o: %.cpp
$(MD) $(@D)
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -MMD -o $@
-include $(DEPS)
# I would like for release to be called when simply calling "make" (doesn't happen)
.PHONY: default_target
default_target: release
.PHONY: clean
clean:
$(RM) $(call FIXPATH,$(BUILD)/$(MAIN))
$(RM) $(call FIXPATH,$(OBJECTS))
$(RM) $(call FIXPATH,$(DEPS))
@echo Cleanup complete!
.PHONY: debug
debug: all
.PHONY: release
release: all
.PHONY: debugrun
debugrun: all
./$(call FIXPATH,$(BUILD)/$(MAIN))
.PHONY: run
run: all
./$(call FIXPATH,$(BUILD)/$(MAIN))
Of course I did not come up with this Makefile on my own. I copied from two templates. My first template built all .o
and .d
files into the src
directory. :/ And consulted various other stackoverflow posts but couldn't crack this last bit. This Makefile has seen much worse stages.
Because of the way Make works, you cannot do this with regular targets alone.
You would expect make debug release
to build both flavors of your program, yet any approach that sets variables will see only one flavor being built.
One way to do it is by using a recursive invocation of make:
all: debug
BUILD_DIR=build
BUILD_TYPE ?= unknown
BUILD_PREFIX=$(BUILD_DIR)/$(BUILD_TYPE)
TARGETS=main
SOURCES := src/foo.cpp
OBJECTS := $(SOURCES:%.cpp=$(BUILD_PREFIX)/%.o)
DEPS := $(OBJECTS:.o=.d)
ifeq ($(MAKELEVEL),0)
release:
$(MAKE) BUILD_TYPE=release
debug:
$(MAKE) BUILD_TYPE=debug
else
release: $(addprefix $(BUILD_PREFIX)/, $(TARGETS))
debug: $(addprefix $(BUILD_PREFIX)/, $(TARGETS))
$(BUILD_PREFIX)/main: $(OBJECTS) | $(BUILD_PREFIX)
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) $(LIBS)
$(BUILD_PREFIX):
mkdir -p $(BUILD_PREFIX)
$(BUILD_PREFIX)/%.o: %.cpp
mkdir -p $(@D)
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -MMD -o $@
-include $(DEPS)
endif
The key bit here is that $(MAKELEVEL)
is set to 0 on the outer invocation of make, and to 1 on the recursive invocation. In the former case we define the release and debug targets to do a recursive invocation with BUILD_TYPE
explicitly set.
The only other notable fact is the use of |
in the prerequisites list of $(BUILD_PREFIX)/main
. This is only used for ordering but does not show up in $@
.
You can also keep everything in a single Make invocation but make use of macro expansion to avoid repeating yourself:
all: debug
TARGETS=main
BUILD_DIR = build
SOURCES := src/foo.cpp
OBJECTS := $(SOURCES:%.cpp=%.o)
DEPS := $(OBJECTS:.o=.d)
CXXFLAGS_debug := -DDEBUG -Og -g
CXXFLAGS_release := -DRELEASE -O2
define build_flavor
$(eval BUILD_TYPE=$1)
$(eval BUILD_PREFIX=$(BUILD_DIR)/$(BUILD_TYPE))
$(BUILD_TYPE): $(addprefix $(BUILD_PREFIX)/, $(TARGETS))
$(BUILD_PREFIX)/main: $(addprefix $(BUILD_PREFIX)/, $(OBJECTS)) | $(BUILD_PREFIX)
$$(CXX) $$(CXXFLAGS_$(BUILD_TYPE)) $$(INCLUDES) -o $$@ $$^ $$(LDFLAGS) $$(LIBS)
$(BUILD_PREFIX):
mkdir -p $(BUILD_PREFIX)
$(BUILD_PREFIX)/%.o: %.cpp
mkdir -p $$(@D)
$$(CXX) $$(CXXFLAGS_$(BUILD_TYPE)) $$(INCLUDES) -c $$< -MMD -o $$@
-include $(addprefix $(BUILD_PREFIX)/, $(DEPS))
endef
$(eval $(call build_flavor, debug))
$(eval $(call build_flavor, release))
Note the doubled-up dollar signs $$
for some variables in the define
snippet. This prevents Make from prematurely expanding the variables, especially those used in the rule recipes. Notably BUILD_TYPE
and BUILD_PREFIX
are expanded immediately. If you change the eval
on the final lines to warning
you can see the expansion for yourself.