Search code examples
c++makefile

Makefile doesn't choose the right folder to build even though specified


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.


Solution

  • 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.

    Approach 1: recursive Make

    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 $@.

    Approach 2: code generation

    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.