/nrmake

Primary LanguageMakefileBSD 2-Clause "Simplified" LicenseBSD-2-Clause

nrmake

A non-recursive make build system designed with two goals in mind: minimalism and efficiency. Supports C and C++ projects using gcc and/or clang. The only dependency required is GNU Make of at least v3.82.

See the nrmake-example repository for an example of how to use this build system.

For each "module" (i.e.,executable, static lib, shared lib) you want to build, you simply need to create a file called Module.mk in the directory. The simplest Module.mk requires a single line.

Example Module.mk

$(call add-executable-module,$(get-path))

This builds an executable with the same name as the directory and puts it in the <root>/bin directory. For documentation on how to provide individual CPPFLAGS, LDFLAGS, CXXFLAGS, etc., see nrmake/module_example.mk.

How to use nrmake in your project

The best way to use nrmake in your project is to include it as a git submodule. From your project's root directory, do the following:

git submodule add -- https://gitlab.com/btmcg/nrmake.git
cd nrmake
git checkout v2.2.1
cd ..
ln --relative --symbolic nrmake/Makefile

Then add Module.mk files to the directory of each executable or library you would like to build. See the example one-line file cited above or use nrmake-example as an example. A description of all supported variables are listed in nrmake/module_example.mk.

Project structure

<root>/benchmark/
Project benchmarking code, ideally for use with google-benchmark. Not required.
<root>/nrmake/
Build-related makefiles provided by this repository.
<root>/src/
Project source code.
<root>/test/
Project unit testing code, ideally for use with catch. Not required.
<root>/third_party/
Third party dependencies for project, such as catch and google-benchmark libraries. The goal being to keep the entire project self-contained. Not required.
<root>/bin/
Final location of all of the project's compiled executables.
<root>/include/
Final location of all of the project's exported headers.
<root>/lib/
Final location of all of the project's compiled libraries.

Supported build options

DEBUG
Disables optimizations and removes the NDEBUG flag. By default, all compile- and link-time optimizations are turned on and -DNDEBUG is set.
COMPILER
Supports either clang or gcc (default gcc).
ASAN
Compiles with address sanitization static analysis.
MSAN
Compiles with memory sanitization static analysis (currently only supported by clang).
UBSAN
Compiles with undefined behavior sanitization static analysis.
PGO_GEN
Builds binaries with profile-guided optimization instrumentation. The profile data will be written after running a compiled binary.
PGO_USE
Uses the profile data generated by PGO_GEN=1.

Example usage

make help
Show available targets and descriptions.
make -j
Builds the project using gcc with full optimizations and places all binaries and libraries in bin/ and lib/.
make dist
Generate a distributable tarball package containing bin, include, and lib. It will be placed in the root directory.
make COMPILER=clang DEBUG=1 -j
Builds the project using clang with optimizations turned off (and NDEBUG defined).
make tidy
Runs clang-tidy on the source tree.
make benchmark
Builds the benchmarking code and executes bin/benchmark-runner.
make COMPILER=clang DEBUG=1 ASAN=1 test -j
Builds the testing code with clang and address sanitization turned on.

Default targets

all
Builds every module in the tree, including the "special" targets test-runner and benchmark-runner. This is built by default when no arguments are given to make.
benchmark
Builds (if necessary) the benchmarking code (assuming google-benchmark has been installed) and executes bin/benchmark-runner.
clean
Removes all build artifacts from the tree; this includes: object code, libraries, and executables. Top-level bin/ and lib/ directories are preserved.
dist
Create a tarball for distribution. All files in bin/, include/, and lib/ will be included.
distclean
Calls clean and additionally removes dependency files, the version file, bin/, lib/, and include/ directories.
format
Runs clang-format on src/, test/, and benchmark/ directories (if they exist). Assumes a .clang-format file exists in root.
help
Shows version of nrmake as well as defined targets and modules.
list-modules
Prints to stdout every module the build system is aware of, along with its associated build and link flags.
tags
Runs ctags on the src/ directory.
test
Builds (if necessary) the unit testing code (assuming catch is installed) and executes bin/test-runner.
tidy
Runs clang-tidy on src/. (Assumes a .clang-tidy file exists in root.)

Third-party libraries

nrmake was designed with catch and google-benchmark in mind. Including these two projects is fairly simple.

catch

catch is best included as a submodule with your project's test code in <root>/test. To make the header available to your code, an edit of nrmake/third_party.mk is required. Boilerplate provided.

git submodule add -- https://github.com/catchorg/Catch2.git third_party/catch2/2.13.2
cd third_party/catch2/2.13.2
git checkout v2.13.2
cd -
vim nrmake/third_party.mk

google-benchmark

google-benchmark needs to be compiled for both gcc and clang. The following steps will install the header and libraries in separate directories under <root>/third_party. To make the library available to your code, an edit of nrmake/third_party.mk is required. Boilerplate is provided.

# from your repository root
git clone --branch=v1.5.2 --depth=1 https://github.com/google/benchmark.git gb
cd gb
cmake \
    -DBENCHMARK_ENABLE_LTO:BOOL=ON \
    -DBENCHMARK_ENABLE_TESTING:BOOL=OFF \
    -DCMAKE_BUILD_TYPE:STRING=RELEASE \
    -DCMAKE_CXX_COMPILER:STRING=g++ \
    -DCMAKE_INSTALL_PREFIX:PATH=../../third_party/google-benchmark/gcc-10.2.0/1.5.2 \
    -DGCC_AR:STRING=gcc-ar \
    -DGCC_RANLIB:STRING=gcc-ranlib \
    -S . -B _build
cmake --build _build --config Release --target install --parallel

# now build with clang
rm -rf _build

cmake \
    -DBENCHMARK_ENABLE_LTO:BOOL=ON \
    -DBENCHMARK_ENABLE_TESTING:BOOL=OFF \
    -DBENCHMARK_USE_LIBCXX:BOOL=ON \
    -DCMAKE_BUILD_TYPE:STRING=RELEASE \
    -DCMAKE_CXX_COMPILER:STRING=clang++ \
    -DCMAKE_INSTALL_PREFIX:PATH=../../third_party/google-benchmark/clang-10.0.1/1.5.2 \
    -DLLVMAR_EXECUTABLE:STRING=llvm-ar \
    -DLLVMNM_EXECUTABLE:STRING=llvm-nm \
    -DLLVMRANLIB_EXECUTABLE:STRING=llvm-ranlib \
    -S . -B _build
cmake --build _build --config Release --target install --parallel

cd ..
rm -rf gb
vim nrmake/third_party.mk

Rationale

After years of using less-than-efficient build systems (GNU Make-based or otherwise) in various jobs and personal projects, I wanted to create a simple environment that I could replicate over and over again that would do exactly what I needed it to do. I wanted it to use make (due to its ubiquity), require zero dependencies (including additional build binaries or libraries), correctly handle internal dependency graphs, and provide a mechanism for running unit tests and benchmarks. Every time I started a new project, I didn't want to waste time thinking about how to build and structure the code and tests, I just wanted to get some prototype on the disk. What started as a Makefile that I would copy to each new project turned into more of a "system" (or collection of .mk files) that provided various features that I used on a regular basis. I finally decided to make this repo public, write this README, and provide this code for anyone else like me that has suffered with clumsy C++ build systems in the past.