It is not published, but there is not really much about it, except exploiting most features provided by gmake.
For easy maintenance, the universal makefile is split into 4 files that are completely universal + 1 makefile with extra definitions per each combination of operating system and CPU type that might be a target for compilation.
The extra files for specific compilation targets, e.g. skylake-linux-gnu, armv7e-m4-none-eabi, i686-cygwin and so on, contain definitions for the names of all the tools that might be used, e.g. compilers for various programming languages, linking commands, librarian commands, binary file modification commands (e.g. objcopy), default locations for library files, default locations for include files, default values for compilation flags.
The 4 completely universal files do not contain directly any command name or command flags or any file name or any directory name. They contain only variable names whose values are either defined in the operating system/CPU specific makefile or whose values are determined by the make command by scanning the project and by transforming the found files, e.g. by replacing the file name extensions.
The 4 completely universal files are:
1. A file for the top directory of a complex project, which descends recursively in all subdirectories and runs there the given make command, if a Makefile exists in the subdirectory
2. A file with make targets. Examples of make targets:
4. A file with definitions, which is the most important in simplifying the makefiles needed for any new project, which are typically almost empty, because they do not need any other line besides including the universal makefile, even if I usually add a definition with a list of source directories, in order to separate the place where the project is built from the place where the source files are stored and possibly some options, e.g. building a debug version instead of a release version, or building for a cross-compilation target.
Example how to determine the directories with source files:
I use many more kinds of source files, so the OBJS above is just a shortened example.
The operating system/CPU type specific makefile includes the 3 universal files with targets, rules and definitions and it also includes the automatically generated dependencies:
include $(OBJS:.o=.d)
so later only the specific makefile needs to be included, depending on the compilation target.
There are many more details, but these are the general principles. They are intended to make me write as little as possible in the Makefile for a new project.
If I do not define in the Makefile from the project directory what shall be built with "make all", then either an executable is built, having the same name as the Makefile directory, or another kind of file is built, if the directory has a special suffix, e.g. a static library for a directory ending in "_lib".
When I add, delete, move or rename source files, I do not have to do anything for project management, except doing a "make clean" before that, because the next "make" will update the lists of source files.
For easy maintenance, the universal makefile is split into 4 files that are completely universal + 1 makefile with extra definitions per each combination of operating system and CPU type that might be a target for compilation.
The extra files for specific compilation targets, e.g. skylake-linux-gnu, armv7e-m4-none-eabi, i686-cygwin and so on, contain definitions for the names of all the tools that might be used, e.g. compilers for various programming languages, linking commands, librarian commands, binary file modification commands (e.g. objcopy), default locations for library files, default locations for include files, default values for compilation flags.
The 4 completely universal files do not contain directly any command name or command flags or any file name or any directory name. They contain only variable names whose values are either defined in the operating system/CPU specific makefile or whose values are determined by the make command by scanning the project and by transforming the found files, e.g. by replacing the file name extensions.
The 4 completely universal files are:
1. A file for the top directory of a complex project, which descends recursively in all subdirectories and runs there the given make command, if a Makefile exists in the subdirectory
2. A file with make targets. Examples of make targets:
clean:
$(RM) $(RMFLAGS) $(MAP) $(BIN) $(IMPLIB) $(EXE) $(LIB) $(WEXE) $(DLIB)
$(RM) $(RMFLAGS) $(OBJS) $(OBJS:.o=.d) $(OBJS:.o=.lst)
and
all : $(EXE) $(LIB) $(WEXE) $(DLIB)
The 4 variables above are the files to be built, respectively CLI executables, static libraries, GUI executables and dynamic libraries.
As I have said everything must be expressed using variables that will be defined elsewhere.
3. A file with make rules, for how to build any kind of file that might be encountered. Examples of rules:
%.o: %.c
$(CC) $(CFLAGS) $(DEFINES) $(INCLUDES) -o $@ -c $<
%.s: %.cpp
$(CXX) $(CXXFLAGS) $(DEFINES) $(INCLUDES) -o $@ -S $<
%.d: %.cpp
$(CXX) $(CXXFLAGS) $(DEFINES) $(INCLUDES) -MD -o $(@:.d=.o) -c $<
4. A file with definitions, which is the most important in simplifying the makefiles needed for any new project, which are typically almost empty, because they do not need any other line besides including the universal makefile, even if I usually add a definition with a list of source directories, in order to separate the place where the project is built from the place where the source files are stored and possibly some options, e.g. building a debug version instead of a release version, or building for a cross-compilation target.
Example how to determine the directories with source files:
ifeq "$(strip $(SOURCE_DIRS))" ""
SOURCE_DIRS := .
endif
ifeq "$(strip $(PREFIX_DIR))" ""
VPATH := $(SOURCE_DIRS)
else
VPATH := $(addprefix $(PREFIX_DIR)/, $(SOURCE_DIRS))
endif
Example of how to scan the source directories for source files written in a certain programming language:
CPP_FILES := $(notdir $(wildcard $(addsuffix /*.cpp, $(VPATH))))
The list for every kind of source files is stored in a corresponding variable like above.
For the source files that are compiled to object files, the list of object files that must be made is computed so:
OBJS := $(CPP_FILES:.cpp=.o) $(C_FILES:.c=.o) $(SS_FILES:.S=.o) $(S_FILES:.s=.o) $(O_FILES)
I use many more kinds of source files, so the OBJS above is just a shortened example.
The operating system/CPU type specific makefile includes the 3 universal files with targets, rules and definitions and it also includes the automatically generated dependencies:
include $(OBJS:.o=.d)
so later only the specific makefile needs to be included, depending on the compilation target.
There are many more details, but these are the general principles. They are intended to make me write as little as possible in the Makefile for a new project.
If I do not define in the Makefile from the project directory what shall be built with "make all", then either an executable is built, having the same name as the Makefile directory, or another kind of file is built, if the directory has a special suffix, e.g. a static library for a directory ending in "_lib".
When I add, delete, move or rename source files, I do not have to do anything for project management, except doing a "make clean" before that, because the next "make" will update the lists of source files.