Here's the Makefile I've added to the documentation (currently in review so I'll post it here) :
# Set project directory one level above the Makefile directory. $(CURDIR) is a GNU make variable containing the path to the current working directory
PROJDIR := $(realpath $(CURDIR)/..)
SOURCEDIR := $(PROJDIR)/Sources
BUILDDIR := $(PROJDIR)/Build
# Name of the final executable
TARGET = myApp.exe
# Decide whether the commands will be shown or not
VERBOSE = TRUE
# Create the list of directories
DIRS = Folder0 Folder1 Folder2
SOURCEDIRS = $(foreach dir, $(DIRS), $(addprefix $(SOURCEDIR)/, $(dir)))
TARGETDIRS = $(foreach dir, $(DIRS), $(addprefix $(BUILDDIR)/, $(dir)))
# Generate the GCC includes parameters by adding -I before each source folder
INCLUDES = $(foreach dir, $(SOURCEDIRS), $(addprefix -I, $(dir)))
# Add this list to VPATH, the place make will look for the source files
VPATH = $(SOURCEDIRS)
# Create a list of *.c sources in DIRS
SOURCES = $(foreach dir,$(SOURCEDIRS),$(wildcard $(dir)/*.c))
# Define objects for all sources
OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))
# Define dependencies files for all objects
DEPS = $(OBJS:.o=.d)
# Name the compiler
CC = gcc
# OS specific part
ifeq ($(OS),Windows_NT)
RM = del /F /Q
RMDIR = -RMDIR /S /Q
MKDIR = -mkdir
ERRIGNORE = 2>NUL || true
SEP=\
else
RM = rm -rf
RMDIR = rm -rf
MKDIR = mkdir -p
ERRIGNORE = 2>/dev/null
SEP=/
endif
# Remove space after separator
PSEP = $(strip $(SEP))
# Hide or not the calls depending of VERBOSE
ifeq ($(VERBOSE),TRUE)
HIDE =
else
HIDE = @
endif
# Define the function that will generate each rule
define generateRules
$(1)/%.o: %.c
@echo Building $$@
$(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$<) -MMD
endef
# Indicate to make which targets are not files
.PHONY: all clean directories
all: directories $(TARGET)
$(TARGET): $(OBJS)
$(HIDE)echo Linking $@
$(HIDE)$(CC) $(OBJS) -o $(TARGET)
# Include dependencies
-include $(DEPS)
# Generate rules
$(foreach targetdir, $(TARGETDIRS), $(eval $(call generateRules, $(targetdir))))
directories:
$(HIDE)$(MKDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)
# Remove all objects, dependencies and executable files generated during the build
clean:
$(HIDE)$(RMDIR) $(subst /,$(PSEP),$(TARGETDIRS)) $(ERRIGNORE)
$(HIDE)$(RM) $(TARGET) $(ERRIGNORE)
@echo Cleaning done !
Main features
- Automatic detection of
C
sources in specified folders
- Multiple source folders
- Multiple corresponding target folders for object and dependency files
- Automatic rule generation for each target folder
- Creation of target folders when they don't exist
- Dependency management with
gcc
: Build only what is necessary
- Works on
Unix
and DOS
systems
- Written for
GNU Make
How to use this Makefile
To adapt this Makefile to your project you have to :
- Change the
TARGET
variable to match your target name
- Change the name of the
Sources
and Build
folders in SOURCEDIR
and BUILDDIR
- Change the verbosity level of the Makefile in the Makefile itself or in make call (
make all VERBOSE=FALSE
)
- Change the name of the folders in
DIRS
to match your sources and build folders
- If required, change the compiler and the flags
In this Makefile Folder0
, Folder1
and Folder2
are the equivalent to your FolderA
, FolderB
and FolderC
.
Note that I have not had the opportunity to test it on a Unix system at the moment but it works correctly on Windows.
Explanation of a few tricky parts :
Ignoring Windows mkdir errors
ERRIGNORE = 2>NUL || true
This has two effects :
The first one, 2>NUL
is to redirect the error output to NUL, so as it does not comes in the console.
The second one, || true
prevents the command from rising the error level. This is Windows stuff unrelated with the Makefile, it's here because Windows' mkdir
command rises the error level if we try to create an already-existing folder, whereas we don't really care, if it does exist that's fine. The common solution is to use the if not exist
structure, but that's not UNIX-compatible so even if it's tricky, I consider my solution more clear.
Creation of OBJS containing all object files with their correct path
OBJS := $(subst $(SOURCEDIR),$(BUILDDIR),$(SOURCES:.c=.o))
Here we want OBJS to contain all the object files with their paths, and we already have SOURCES which contains all the source files with their paths.
$(SOURCES:.c=.o)
changes *.c in *.o for all sources, but the path is still the one of the sources.
$(subst $(SOURCEDIR),$(BUILDDIR), ...)
will simply subtract the whole source path with the build path, so we finally have a variable that contains the .o files with their paths.
Dealing with Windows and Unix-style path separators
SEP=\
SEP = /
PSEP = $(strip $(SEP))
This only exist to allow the Makefile to work on Unix and Windows, since Windows uses backslashes in path whereas everyone else uses slashes.
SEP=\
Here the double backslash is used to escape the backslash character, which make
usually treats as an "ignore newline character" to allow writing on multiple lines.
PSEP = $(strip $(SEP))
This will remove the space char of the SEP
variable, which has been added automatically.
Automatic generation of rules for each target folder
define generateRules
$(1)/%.o: %.c
@echo Building $$@
$(HIDE)$(CC) -c $$(INCLUDES) -o $$(subst /,$$(PSEP),$$@) $$(subst /,$$(PSEP),$$<) -MMD
endef
That's maybe the trick that is the most related with your usecase. It's a rule template that can be generated with $(eval $(call generateRules, param))
where param
is what you can find in the template as $(1)
.
This will basically fill the Makefile with rules like this for each target folder :
path/to/target/%.o: %.c
@echo Building $@
$(HIDE)$(CC) -c $(INCLUDES) -o $(subst /,$(PSEP),$@) $(subst /,$(PSEP),$<) -MMD