A really simple makefile based build system.
- Motivation
- First Project
- Projecct Configuration
- Project Settings Section
- Project Flags Section
- Project Rules Section
- Toolchain Settings Section
- Cleaning and Rebuilding
- Multi Project Setup
- Debug and Release Builds
- Useful Variables set by
zpp.mk
- Requires just GNU make.
- Minimal amount of settings.
- Direct access to compiler flags.
- Very flexible and customizable.
- Short - just 200 lines of code.
Given the following file:
#include <iostream>
int main()
{
std::cout << "Hello World\n";
}
Create the following directory tree:
project
- src
- main.cpp
- zpp.mk
- zpp_project.mk
The zpp.mk
file contains the build system logic which is not intended to be modified.
The zpp_project.mk
file contains project settings, compilation flags, and toolchain configuraion.
Run make -f zpp.mk
or execute ./zpp.mk
to build the project.
The output will be like so:
Building 'default/output' in 'debug' mode...
Compiling 'src/main.cpp'...
Linking './out/debug/default/output'...
Built 'default/output'.
More on the debug
/default
directory names will be given later.
In order to configure the project, we need to understand and edit the contents of zpp_project.mk
file.
The first section of zpp_project.mk
file contains some basic project configuration variables:
ifeq ($(ZPP_PROJECT_SETTINGS), true)
ZPP_TARGET_NAME := output
ZPP_TARGET_TYPES := default
ZPP_LINK_TYPE := default
ZPP_CPP_MODULES_TYPE :=
ZPP_OUTPUT_DIRECTORY_ROOT := ./out
ZPP_INTERMEDIATE_DIRECTORY_ROOT = ./obj
ZPP_SOURCE_DIRECTORIES := ./src
ZPP_SOURCE_FILES :=
ZPP_INCLUDE_PROJECTS :=
ZPP_COMPILE_COMMANDS_JSON := compile_commands.json
endif
The content of this section must be enclosed with ZPP_PROJECT_SETTINGS
condition
as shown to be selectively included when appropriate.
ZPP_TARGET_NAME
is name of the output file that is produced from the build:
Example:
ZPP_TARGET_NAME := hello_world_app
One or more target types for the build can be set via ZPP_TARGET_TYPES
.
This can be used to repeat the build process with multiple configurations or architectures.
The build will loop the ZPP_TARGET_TYPES
variable, allowing to access the current target
type using the ZPP_TARGET_TYPE
variable. The ZPP_TARGET_TYPE
is used as the name
of the output subdirectory of every target.
Example:
ZPP_TARGET_TYPES := x86_64 aarch64
And checking is done via:
ifeq ($(ZPP_TARGET_TYPE), x86_64)
# Configure to compile for x86.
else ($(ZPP_TARGET_TYPE), aarch64)
# Configure to compile for aarch64.
endif
You may pick a specific target type to build by adding target_type=value
to the make
command.
Next, is the ZPP_LINK_TYPE
, which determines the strategy to link the final target file.
This variable may contain one of the following values:
- default - to use the compiler for linking, which is the most common way to link.
- ld - to use ld like interface directly for linking.
- link - to use link like interface for linking.
- ar - to use ar like interface for linking.
Example:
ZPP_LINK_TYPE := default
The ZPP_CPP_MODULES_TYPE
controls implementation type of C++20 modules. If empty, C++ modules
are disabled, otherwise, the only supported value is ZPP_CPP_MODULES_TYPE := clang
which enables
clang modules. You will need to also set the ZPP_CXXFLAGS
to include the -fmodules
flag.
The current implementation is highly experimental, and turned off by default. The following are known limitations of the current implementation:
- Only clang is supported (tested clang-11 and above).
- There is no optimization around finding module interfaces, any
C++
file is searched to check if it a module interface (thecppm
extension however was added as a validC++
file)
Example:
ZPP_CPP_MODULES_TYPE := clang
The ZPP_OUTPUT_DIRECTORY_ROOT
and ZPP_INTERMEDIATE_DIRECTORY_ROOT
control the root directory
of the target output files and the intermediate files (object files, dependency, etc) respectively.
Example:
# Note that this can be used to move the output directory to
# the parent folder to be able to organize multiple subproject outputs
# to the same place.
ZPP_OUTPUT_DIRECTORY_ROOT := ../out
ZPP_INTERMEDIATE_DIRECTORY_ROOT = ./obj
The ZPP_SOURCE_DIRECTORIES
is a list of directories that will be searched by the build system
for source files ending with the following extensions: *.cpp;*.cc;*.c;*.S
Example:
ZPP_SOURCE_DIRECTORIES := ./src ../some_library/src
The ZPP_SOURCE_FILES
allows to explicitly add additional source files to the build:
Example:
ZPP_SOURCE_FILES := ../external/src/external_lib.cpp ./src/main.cpp
The ZPP_INCLUDE_PROJECTS
allows to use the main zpp.mk
makefile to directly build multiple
projects and thereby ignore any other field that appears in the zpp_project.mk
, while immediately
building the mentioned projects.
Example:
# Each project in this example is a subdirectory.
ZPP_INCLUDE_PROJECTS := project1 project2 project3
The ZPP_COMPILE_COMMANDS_JSON
allows to control whether or not a compile_commands.json
is generated.
This variable accepts the value of the compile commands file relative path.
If this variable is empty, no compile commands is generated.
It is possible to ask to generate the compile commands in the intermediate directories, for this, assign
the intermediate
as the value of the variable.
Example:
# Place the compile commands inside the source tree.
ZPP_COMPILE_COMMANDS_JSON := compile_commands.json
# Place the compile commands in the intermediate directories.
ZPP_COMPILE_COMMANDS_JSON := intermediate
# Do not generate compile commands.
ZPP_COMPILE_COMMANDS_JSON :=
This section which must be enclosed by the ZPP_PROJECT_FLAGS
variable,
controls the compiler and linker flags, leaving most of the work to the
user so that it is as flexible as possible.
The following examples contains some basic flags for simple projects.
ifeq ($(ZPP_PROJECT_FLAGS), true)
ZPP_FLAGS := \
$(patsubst %, -I%, $(shell find . -type d -name "include")) \
-pedantic -Wall -Wextra -Werror -fPIE
ZPP_FLAGS_DEBUG := -g
ZPP_FLAGS_RELEASE := \
-O2 -flto -ffunction-sections \
-fdata-sections -fvisibility=hidden
ZPP_CFLAGS := $(ZPP_FLAGS) -std=c11
ZPP_CFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_CFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
ZPP_CXXFLAGS := $(ZPP_FLAGS) -std=c++17 -stdlib=libc++
ZPP_CXXFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_CXXFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
ZPP_CXXMFLAGS := -fPIE
ZPP_CXXMFLAGS_DEBUG := -g
ZPP_CXXMFLAGS_RELEASE :=
ZPP_ASFLAGS := $(ZPP_FLAGS) -x assembler-with-cpp
ZPP_ASFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_ASFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
ZPP_LFLAGS := $(ZPP_FLAGS) $(ZPP_CXXFLAGS) -pie -Wl,--no-undefined
ZPP_LFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_LFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE) \
-Wl,--strip-all -Wl,-flto -Wl,--gc-sections
endif
The ZPP_FLAGS
variable typically contains generic flags that will be passed to compilers.
This is a placeholder to be reused with the more specific C/C++/Assembly/Linker flags.
The following example shows how to add all directories named include
with proper
include directory flags, as well as providing more generic compilation flags.
ZPP_FLAGS := \
$(patsubst %, -I%, $(shell find . -type d -name "include")) \
-pedantic -Wall -Wextra -Werror -fPIE
The ZPP_FLAGS_DEBUG
/ZPP_FLAGS_RELEASE
are similar to ZPP_FLAGS
only they are
specific to debug
/release
configurations, more on that later.
# Generate debug information.
ZPP_FLAGS_DEBUG := -g
# Use optimizations and more typical release flags.
ZPP_FLAGS_RELEASE := \
-O2 -flto -ffunction-sections \
-fdata-sections -fvisibility=hidden
Next, are the ZPP_CFLAGS
, ZPP_CFLAGS_DEBUG
, and ZPP_CFLAGS_RELEASE
which behave
similarily to ZPP_FLAGS
, except they are to be used to compile C files.
Note that it is very common to add the generic flags prior to the C specific flags:
ZPP_CFLAGS := $(ZPP_FLAGS) -std=c11
ZPP_CFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_CFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
The ZPP_CXXFLAGS
, ZPP_CXXFLAGS_DEBUG
, and ZPP_CXXFLAGS_RELEASE
are similar to their
C counterparts, only for C++:
ZPP_CXXFLAGS := $(ZPP_FLAGS) -std=c++17 -stdlib=libc++
ZPP_CXXFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_CXXFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
The ZPP_CXXMFLAGS
, ZPP_CXXMFLAGS_DEBUG
, and ZPP_CXXMFLAGS_RELEASE
are used when
translating precompiled module files to object files.
ZPP_CXXMFLAGS := -fPIE
ZPP_CXXMFLAGS_DEBUG := -g
ZPP_CXXMFLAGS_RELEASE :=
And again, for assembly files:
ZPP_ASFLAGS := $(ZPP_FLAGS) -x assembler-with-cpp
ZPP_ASFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
ZPP_ASFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE)
The ZPP_LFLAGS
, ZPP_LFLAGS_DEBUG
and ZPP_LFLAGS_RELEASE
are the flags
passed in the final link phase, again, generic ones, and debug/release specific ones:
ZPP_LFLAGS := $(ZPP_FLAGS) $(ZPP_CXXFLAGS) -pie -Wl,--no-undefined
ZPP_LFLAGS_DEBUG := $(ZPP_FLAGS_DEBUG)
# In release, strip the output, and use link time optimizations.
ZPP_LFLAGS_RELEASE := $(ZPP_FLAGS_RELEASE) \
-Wl,--strip-all -Wl,-flto -Wl,--gc-sections
The project rules section is a reserved space for custom rules that needs to take
place in some cases. This section must be enclosed with the ZPP_PROJECT_RULES
variable.
Example of use of this section is to rebuild certain files whenever a manual dependency
on a file is needed, such as when an assembly file uses the .incbin
directive and includes
a file from the filesystem:
; ./src/photo.S
photo:
.incbin "../resources/photo.jpg"
The idea is that whenever ../resources/photo.jpg
changes, the assembly file has to
be rebuilt.
The following rules section achieves that:
ifeq ($(ZPP_PROJECT_RULES), true)
$(ZPP_INTERMEDIATE_DIRECTORY)/./src/photo.o: \
./resources/photo.jpg
endif
This section which must be enclosed by the ZPP_TOOLCHAIN_SETTINGS
variable, has to export
the following functional tools:
ZPP_CC
- a C compiler.ZPP_CXX
- a C++ compiler.ZPP_AS
- an Assembly compiler.ZPP_LINK
- a linker.ZPP_AR
- the archiver.
A simple configuration to use normal installed clang compiler would be:
ifeq ($(ZPP_TOOLCHAIN_SETTINGS), true)
ZPP_CC := clang
ZPP_CXX := clang++
ZPP_AS := $(ZPP_CC)
ZPP_LINK := $(ZPP_CXX)
ZPP_AR := ar
ZPP_PYTHON := python3
ZPP_POSTLINK_COMMANDS :=
endif
Note that here we use the ZPP_CC
as assembly compiler as well, and
that we use ZPP_CXX
as the linker.
Although this section is quite short, it can be made more complicated and allow
nice cross compilation solution.
One way to complicate this section without much overhead is to add an include
to the a proper toolchain configuration, for instance:
ifeq ($(ZPP_TOOLCHAIN_SETTINGS), true)
ifeq ($(ZPP_TARGET_TYPE), x86_64-windows)
include win64_toolchain.mk
else ifeq ($(ZPP_TARGET_TYPE), aarch64-android)
include android_aarch64_toolchain.mk
else
include default_toolchain.mk
endif
endif
The ZPP_PYTHON
is only required if the project settings ask to build the compile_commands.json
file.
The ZPP_POSTLINK_COMMANDS
allows to run custom shell commands after linking,
for your convenience.
At the appendix section there is an example for a possible windows toolchain.mk
file.
To clean or rebuild the project, use make -f zpp.mk clean
,
or make -f zpp.mk rebuild
commands respectively.
Multi project set up is quite natural using this utility, it can even be done in multiple ways. The idea is to have a top level makefile that calls the bottom ones.
The easiest way is to just have the following tree:
solution:
- project1
- include
- src
- zpp_project.mk
- project2
- include
- src
- zpp_project.mk
- zpp.mk
Invoke the following command make -f zpp.mk projects='project1 project2
.
Another way is to have a zpp_project.mk
in the top level directory and define the
ZPP_INCLUDE_PROJECTS
appropriately.
A more manual do-it-yourself way is to have the following project tree:
solution:
- project1
- include
- src
- zpp_project.mk
- project2
- include
- src
- zpp_project.mk
- zpp.mk
- makefile
The contents of the top level makefile can be:
PROJECTS = project1 project2
all:
@for project in $(PROJECTS) ; do \
$(MAKE) -s -f ../zpp.mk -C $$project; \
done
clean:
@for project in $(PROJECTS) ; do \
$(MAKE) -s -f ../zpp.mk -C $$project clean; \
done
rebuild:
@for project in $(PROJECTS) ; do \
$(MAKE) -s -f ../zpp.mk -C $$project rebuild; \
done
Remember you can set the following variable in every
zpp_project.mk
file such that the output will be on the solution
folder rather than a separate output directory for each project.
ZPP_OUTPUT_DIRECTORY_ROOT := ../out
Use make -j
and observe the output.
Compiling 'src/main.cpp'...
Linking '../out/debug/default/output1'...
Built 'default/output1'.
Building 'default/output2' in 'debug' mode...
Compiling 'src/main.cpp'...
Linking '../out/debug/default/output2'...
Built 'default/output2'.
You can add mode=debug
or mode=release
when compiling using the make
command.
This way you can change between debug and release configuration. If unspecified, debug
configuration is selected.
Here are some useful variables to use inside the zpp_project.mk
ZPP_CONFIGURATION
- is either debug or release depending on the build configuration. Available in all sections.ZPP_INTERMEDIATE_DIRECTORY
- the intermediate files directory, not available on the project settings section.ZPP_OUTPUT_DIRECTORY
- the output directory, not availble on the project settings section.