Aquefir

Massively multipurpose software.

Slick Makefiles

Penned on the 4th day of September, 2020. It was a Friday.

Last updated on the 30th day of October, 2020. It was a Friday.

What are these?

These Makefiles are used for building systems software projects. They support C and C++ compilation, proper assembly, and compilation of a variety of asset formats used by software into object code. The Slick Makefiles are a living example showing how meta-build systems like CMake and SCons are unnecessary to target many platforms. They are a vastly simpler answer to the problem of building software, and they make no sacrifices in ability to output.

There are two files comprising the distribution: base.mk and targets.mk. In the project root where the Makefile resides, a folder etc/ is created to contain these two files. In the Makefile, base.mk is included at the top, before anything else; targets.mk is included last, after the declaration of sources, flags and such.

Base rules in base.mk

This Makefile provides multi-platform build normalisation for the C and C++ compilation toolchains. It is included at the top of the main Makefile. Before anything else, it ensures that make is GNU make, and that it is at least version 3.82. After this, it defines a long list of variables, grouped into several categories:

After setting up all of these variables, base.mk performs a printout of all the variables as-configured to standard output. This eases build diagnostics, as options used are viewable so they can be copied and modified into environmental overrides as necessary. base.mk lastly exports all of the variables so other modules can see them.
Note
The printout of variables can be silenced by setting the SLICK_NOPRINT environment variable in your session.

Environment-exposed variables

CC
The C compiler. Defaults to the native C compiler of the default toolchain on the host platform (see $TC). Note that TCC is supported as an override with gnu and llvm toolchains.
AS
The assembler. Defaults to the native assembler of the default toolchain on the host platform (see $TC). Beware that on macOS, this is the LLVM IR assembler!
CXX
The C++ compiler. Defaults to the native C++ compiler of the default toolchain on the host platform (see $TC).
AR
The static library archiver. Defaults to the native archiver of the default toolchain on the host platform (see $TC).
OCPY
The object code copier. Defaults to the native obcopy of the default toolchain on the host platform (see $TC).
STRIP
The symbol stripper. Defaults to the native symbol stripper of the default toolchain on the host platform (see $TC).
CFLAGS
Command flags passed to the C compiler. These have fully dynamic defaults that consider the host platform, the target platform, the toolchain in use, and even the kind of target being built.
ASFLAGS
Command flags passed to the assembler. These have fully dynamic defaults like $CFLAGS.
CXXFLAGS
Command flags passed to the C++ compiler. These have fully dynamic defaults like $CFLAGS.
LDFLAGS
Command flags passed to the linker. These have fully dynamic defaults like $CFLAGS.
ARFLAGS
Command flags passed to the static archiver. These have fully dynamic defaults like $CFLAGS.
FMT
Command for the clang-format compatible code autoformatter. Usually just resolves to clang-format.
DEFINES
List of identifiers to pass to the CPP and the assembler as defined on the command line. Note that this does not support setting a value, only defining them, for consistent behaviour.
UNDEFINES
List of identifiers to explicitly undefine on the command line for the CPP. Note that unlike $DEFINES this does not work for the assembler.

Supplemental variables

UNAME
The host platform, as output by uname -s. Usually either Darwin or Linux.
TP
The target platform, which defaults to the host platform (it will compare equal to $UNAME). Valid values are: Darwin, Linux, Win32, Win64, and GBA.
TC
The toolchain. On Darwin, this defaults to llvm (installed through Homebrew!); on Linux, this defaults to gnu. Valid values are: gnu, dkarm (GBA only), mingw32 (Windows targets only), llvm, and xcode (macOS only).
SO
The shared library suffix, or file extension, for the target platform. Evaluates to .so on POSIX platforms, .dylib on macOS, .dll on Windows targets, and a bad dummy value for the GBA where it is unsupported.
TROOT
Sysroot path for the target platform. This is provided as a prefix so that projects can access the right include/ and lib/ folders for different target platforms in the same development environment.
SYNDEFS
Synthetic #defines. As noted earlier, these are synthetic stand-ins for the variables in an autogenerated config.h file, allowing code to be made more portable according to the specific quirks and limits of the target system.
EXE
Executable file suffix. on Unix-like platforms this variable is empty, but on Windows targets it becomes .exe. Targeting the GBA makes it become .elf.

Synthetics

Synthetic #defines are provided by base.mk to the CPP and the assembler to allow code to be made more amicable to the specifics of different machines. They are always prefixed with CFG_.

DARWIN
Apple macOS.
LINUX
GNU/Linux.
WINDOWS
Microsoft Windows, 32-bit and 64-bit.
GBA
The Nintendo Game Boy Advance.
IBMPC
Baremetal x86-based IBM-PC compatibles.
AMD64
The AMD 64-bit extensions to Intel’s IA-32 architecture.
IA32
Intel’s i386 architecture, and later updates up to and including Pentium Pro (i686).
I86
Intel’s 8086 architecture.
I186
Intel’s iAPX 186 architecture.
I286
Intel’s i286 architecture.
ARMV4T
ARMv4 architecture with Thumb-1 extensions.
LILENDIAN
Little endian word arrangement.
BIGENDIAN
Big endian word arrangement.
WIN32
32-bit version of the Win32 APIs.
WIN64
64-bit version of the Win32 APIs.
WORDSZ_16
The size of a CPU word (register) is 16 bits.
WORDSZ_32
The size of a CPU word (register) is 32 bits.
WORDSZ_64
The size of a CPU word (register) is 64 bits.
HAVE_I32
Machine has 32-bit integer primitives.
HAVE_I64
Machine has 64-bit integer primitives.
HAVE_FP
Target supports floating-point instructions.
FP_HARD
Machine has hardware floating-point support.
FP_SOFT
Target has software floating-point support.
LONGSZ_32
The size of a long is 32 bits.
LONGSZ_64
The size of a long is 64 bits.

Make targets in targets.mk

This file provides the actual recipes for building the program. Most importantly, it provides half a dozen phony targets for different styles of builds: debug, release, check, cov, asan and ubsan. These of course support fully parallel job control with invocations of the sort make -j, and non-phony targets can be built and rebuilt at will. base.mk’s printout will show what the default options are for a given target. There are also format and clean targets which provide C/C++ autoformatting and build artefact cleaning, respectively.

Note
Optionally, a project can declare their own phony all target in the main Makefile and set whichever default target they wish (the first target in any Makefile is the default), but otherwise debug will be the default target.

Variables that must be manually defined

Several variables are intentionally left undefined in base.mk despite being required by targets.mk, as they are the variables projects are supposed to define in their Makefiles! These are:

CFILES
List of C source files to compile.
CPPFILES
List of C++ source files to compile.
SFILES
List of assembly source files to assemble.
HFILES
List of C header files. These are only used in make format.
HPPFILES
List of C++ header files. These are only used in make format.
INCLUDES
List of <system> include directories for the CPP.
INCLUDEL
List of "local" include directories for the CPP.
LIBS
List of libraries to link to.
LIBDIRS
List of directories to search for libraries in.
FWORKS
List of frameworks to link into. This is only used when targeting macOS.
PROJECT
Alphanumeric identifier for the project. This is interpolated into the name of output binaries and such.
EXEFILE
Set to 1 to have an executable built out of the project.
AFILE
Set to 1 to have a static library built out of the project.
SOFILE
Set to 1 to have a shared library built out of the project.
3PLIBS
List of 3rdparty libraries (see below).
3PLIBDIRS
List of 3rdparty library directories (see below).
TES_CFILES
List of test battery C source files to use with teslib for test-driven development.
TES_HFILES
List of test battery C headers. These are only used in make format.
TES_CPPFILES
List of test battery C++ source files to use with teslib for test-driven development.
TES_HPPFILES
List of test battery C++ headers. These are only used in make format.

3rdparty in-tree libraries

While usage of sysroots is supported and encouraged since development is bound to Unix-like hosts, sometimes it is useful to bring dependencies in-tree. The Slick Makefiles provide a straightforward way to do this with a folder named 3rdparty.

Say you have a library with $PROJECT name uni. By cloning or symlinking it into another project’s 3rdparty/ subfolder under the name unilib and appending uni to the $3PLIBS variable in the Makefile, that library will have its #includes and code brought in automatically for free. The catch is, the libraries must adhere to ADP 1 so that directories and names can resolve.

$3PLIBS provides a list of such libraries to integrate. $3PLIBDIRS provides additional directories besides 3rdparty/ to search through for them, if desired.

Additional materials for getting started

There are example Makefiles in the repository for both executable programs and libraries, located in the src/ folder. With this documentation and those files as a helping hand, you can start using Slick to build your programs quickly and easily.